@onekeyfe/hd-core 1.2.0-alpha.35 → 1.2.0-alpha.37
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 +78 -0
- package/__tests__/DeviceEventRegistration.test.ts +6 -0
- package/__tests__/deviceUploadNft.test.ts +187 -0
- package/__tests__/get-device-state.test.ts +104 -12
- package/__tests__/homescreen.test.ts +41 -1
- package/__tests__/logBlockEvent.test.ts +1 -1
- package/__tests__/open-wallet-session.test.ts +259 -18
- package/__tests__/pro2Nft.test.ts +108 -0
- package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
- package/__tests__/protocol-v2-unlock-policy.test.ts +86 -35
- package/__tests__/protocol-v2.test.ts +377 -402
- package/__tests__/public-pro2-api-boundary.test.ts +1 -0
- package/dist/api/BaseMethod.d.ts +3 -1
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +30 -0
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
- package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
- package/dist/core/deviceEventRegistration.d.ts +2 -0
- package/dist/core/deviceEventRegistration.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +22 -4
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/events/device.d.ts +4 -0
- package/dist/events/device.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +27 -2
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +88 -7
- package/dist/index.js +937 -604
- package/dist/inject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/uiInteraction.d.ts +3 -3
- package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
- package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +2 -1
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +3 -0
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/homescreen.d.ts +4 -0
- package/dist/utils/homescreen.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 +29 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -0
- package/dist/utils/pro2Wallpaper.d.ts +10 -0
- package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +8 -10
- package/src/api/FirmwareUpdateV4.ts +13 -6
- package/src/api/GetPassphraseState.ts +7 -1
- package/src/api/OpenWalletSession.ts +54 -17
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +32 -11
- package/src/api/device/DeviceUnlock.ts +1 -1
- package/src/api/helpers/protocolV2FileWrite.ts +11 -4
- package/src/api/index.ts +1 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +166 -0
- package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
- package/src/core/deviceEventRegistration.ts +4 -0
- package/src/core/index.ts +111 -74
- package/src/data/messages/messages-protocol-v2.json +10 -0
- package/src/device/Device.ts +221 -24
- package/src/events/device.ts +4 -0
- package/src/events/logBlockEvent.ts +1 -0
- package/src/events/ui-request.ts +32 -2
- package/src/inject.ts +1 -0
- package/src/protocols/protocol-v2/features.ts +9 -2
- package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
- package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
- package/src/protocols/protocol-v2/unlockPolicyRunner.ts +117 -0
- package/src/protocols/protocol-v2/walletSession.ts +93 -18
- package/src/types/api/index.ts +2 -0
- package/src/types/api/protocolV2.ts +13 -0
- package/src/utils/deviceFeaturesUtils.ts +4 -0
- package/src/utils/homescreen.ts +32 -6
- package/src/utils/index.ts +6 -1
- package/src/utils/pro2Nft.ts +91 -0
- package/src/utils/pro2Wallpaper.ts +35 -8
- package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
- package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
- package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
- package/src/protocols/protocol-v2/lockedError.ts +0 -10
- package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
|
@@ -61,6 +61,7 @@ import { createCoreApi } from '../src/inject';
|
|
|
61
61
|
import { Device, preloadSessionCache } from '../src/device/Device';
|
|
62
62
|
import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
|
|
63
63
|
import { DevicePool } from '../src/device/DevicePool';
|
|
64
|
+
import { DEVICE } from '../src/events';
|
|
64
65
|
import { UI_REQUEST } from '../src/events/ui-request';
|
|
65
66
|
import {
|
|
66
67
|
PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
@@ -70,15 +71,18 @@ import {
|
|
|
70
71
|
getProtocolV2RuntimeMode,
|
|
71
72
|
parseProtocolV2BuildFingerprint,
|
|
72
73
|
requestProtocolV2DeviceInfo,
|
|
73
|
-
requestProtocolV2ProtocolInfo,
|
|
74
74
|
requestProtocolV2DeviceStatus,
|
|
75
|
+
requestProtocolV2ProtocolInfo,
|
|
75
76
|
supportsProtocolV2Message,
|
|
76
77
|
} from '../src/protocols/protocol-v2/features';
|
|
77
78
|
import {
|
|
78
79
|
getProtocolV2WalletSession,
|
|
79
80
|
refreshProtocolV2DeviceStatus,
|
|
80
81
|
} from '../src/protocols/protocol-v2/walletSession';
|
|
81
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
createProtocolV2UnlockContext,
|
|
84
|
+
runMethodWithUnlockPolicy,
|
|
85
|
+
} from '../src/protocols/protocol-v2/unlockPolicyRunner';
|
|
82
86
|
import { BaseMethod } from '../src/api/BaseMethod';
|
|
83
87
|
import {
|
|
84
88
|
buildProtocolV1FeaturesPayload,
|
|
@@ -240,7 +244,7 @@ describe('UploadPortfolio', () => {
|
|
|
240
244
|
(method as any).device = device;
|
|
241
245
|
|
|
242
246
|
method.init();
|
|
243
|
-
await
|
|
247
|
+
await runMethodWithUnlockPolicy(method, device as any);
|
|
244
248
|
|
|
245
249
|
expect(unlockDevice).not.toHaveBeenCalled();
|
|
246
250
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
@@ -416,14 +420,16 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
416
420
|
}
|
|
417
421
|
return d.features;
|
|
418
422
|
};
|
|
423
|
+
d.ensureProtocolV2RuntimeContext ??= (timeoutMs?: number) =>
|
|
424
|
+
requestProtocolV2ProtocolInfo({
|
|
425
|
+
commands: d.getCommands?.() ?? d.commands,
|
|
426
|
+
timeoutMs,
|
|
427
|
+
});
|
|
419
428
|
d.probeProtocolV2RuntimeState ??= async (
|
|
420
429
|
deviceInfo: ProtocolV2DeviceInfo,
|
|
421
430
|
timeoutMs?: number
|
|
422
431
|
) => {
|
|
423
|
-
const protocolInfo = await
|
|
424
|
-
commands: d.getCommands(),
|
|
425
|
-
timeoutMs,
|
|
426
|
-
});
|
|
432
|
+
const protocolInfo = await d.ensureProtocolV2RuntimeContext(timeoutMs);
|
|
427
433
|
const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
|
|
428
434
|
if (runtimeMode === 'bootloader' || runtimeMode === 'romloader') {
|
|
429
435
|
return d.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
|
|
@@ -804,7 +810,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
804
810
|
).resolves.toEqual({
|
|
805
811
|
passphraseState: 'state-1',
|
|
806
812
|
newSession: 'session-1',
|
|
807
|
-
unlockedAttachPin:
|
|
813
|
+
unlockedAttachPin: false,
|
|
808
814
|
resumed: false,
|
|
809
815
|
});
|
|
810
816
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
@@ -1552,6 +1558,70 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1552
1558
|
expect(device.getInternalState()).toBeUndefined();
|
|
1553
1559
|
});
|
|
1554
1560
|
|
|
1561
|
+
test.each([false, true])(
|
|
1562
|
+
'rejects a different Attach PIN wallet when Pro2 lock failure is %s',
|
|
1563
|
+
async lockFails => {
|
|
1564
|
+
const deviceId = 'attach-mismatch-device';
|
|
1565
|
+
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1566
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
1567
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1568
|
+
{
|
|
1569
|
+
status: {
|
|
1570
|
+
device_id: deviceId,
|
|
1571
|
+
unlocked: true,
|
|
1572
|
+
passphrase_enabled: true,
|
|
1573
|
+
attach_to_pin_enabled: true,
|
|
1574
|
+
unlocked_by_attach_to_pin: false,
|
|
1575
|
+
},
|
|
1576
|
+
}
|
|
1577
|
+
);
|
|
1578
|
+
device.passphraseState = 'expected-state';
|
|
1579
|
+
|
|
1580
|
+
const typedCall = jest.fn((request: string) => {
|
|
1581
|
+
if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
|
|
1582
|
+
if (request === 'DeviceSessionAskPin') return { message: { message: 'PIN verified' } };
|
|
1583
|
+
if (request === 'DeviceStatusGet') {
|
|
1584
|
+
return {
|
|
1585
|
+
message: {
|
|
1586
|
+
device_id: deviceId,
|
|
1587
|
+
unlocked: true,
|
|
1588
|
+
passphrase_enabled: true,
|
|
1589
|
+
attach_to_pin_enabled: true,
|
|
1590
|
+
unlocked_by_attach_to_pin: true,
|
|
1591
|
+
},
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
if (request === 'DeviceSessionGet') {
|
|
1595
|
+
return {
|
|
1596
|
+
message: {
|
|
1597
|
+
btc_test_address: 'wrong-attach-state',
|
|
1598
|
+
session_id: 'wrong-attach-session',
|
|
1599
|
+
},
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
if (request === 'LockDevice') {
|
|
1603
|
+
if (lockFails) throw new Error('Lock unsupported');
|
|
1604
|
+
return { message: { message: 'Device locked' } };
|
|
1605
|
+
}
|
|
1606
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
1607
|
+
});
|
|
1608
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
|
|
1609
|
+
(device as any).commands = { typedCall, promptPassphrase };
|
|
1610
|
+
|
|
1611
|
+
await expect(
|
|
1612
|
+
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
1613
|
+
).rejects.toMatchObject({
|
|
1614
|
+
errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1618
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
1619
|
+
expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
|
|
1620
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
|
|
1621
|
+
expect(device.getInternalState()).toBeUndefined();
|
|
1622
|
+
}
|
|
1623
|
+
);
|
|
1624
|
+
|
|
1555
1625
|
test('recovers an expired Attach PIN wallet through the unified wallet selector', async () => {
|
|
1556
1626
|
const device = Device.fromDescriptor({
|
|
1557
1627
|
id: 'cache-device-attach-recovery',
|
|
@@ -1608,7 +1678,17 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1608
1678
|
}),
|
|
1609
1679
|
{ cancelDeviceOnReject: false }
|
|
1610
1680
|
);
|
|
1611
|
-
expect(unlockDevice).toHaveBeenCalledWith(
|
|
1681
|
+
expect(unlockDevice).toHaveBeenCalledWith(
|
|
1682
|
+
DeviceSessionPinType.AttachToPin,
|
|
1683
|
+
expect.objectContaining({
|
|
1684
|
+
emitUiEvent: false,
|
|
1685
|
+
interaction: expect.objectContaining({
|
|
1686
|
+
phase: 'pin',
|
|
1687
|
+
transition: 'start',
|
|
1688
|
+
protocol: 'V2',
|
|
1689
|
+
}),
|
|
1690
|
+
})
|
|
1691
|
+
);
|
|
1612
1692
|
expect(device.getInternalState()).toBe('new-attach-session');
|
|
1613
1693
|
});
|
|
1614
1694
|
|
|
@@ -1892,12 +1972,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1892
1972
|
expect(getFeatures).not.toHaveBeenCalled();
|
|
1893
1973
|
});
|
|
1894
1974
|
|
|
1895
|
-
test('
|
|
1975
|
+
test('uses the cached wallet address when the local session is missing', async () => {
|
|
1896
1976
|
const typedCall = jest
|
|
1897
1977
|
.fn()
|
|
1898
1978
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1899
|
-
.mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
|
|
1900
|
-
.mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
|
|
1901
1979
|
.mockResolvedValueOnce({
|
|
1902
1980
|
message: {
|
|
1903
1981
|
btc_test_address: 'expected-state',
|
|
@@ -1925,16 +2003,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1925
2003
|
await expect(
|
|
1926
2004
|
getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
|
|
1927
2005
|
).resolves.toMatchObject({ passphraseState: 'expected-state' });
|
|
1928
|
-
expect(
|
|
1929
|
-
|
|
1930
|
-
existsAttachPinUser: false,
|
|
1931
|
-
deviceOnly: false,
|
|
1932
|
-
source: 'wallet-session-coordinator',
|
|
1933
|
-
reason: 'session-recovery',
|
|
1934
|
-
expectedPassphraseState: 'expected-state',
|
|
1935
|
-
},
|
|
1936
|
-
{ cancelDeviceOnReject: false }
|
|
1937
|
-
);
|
|
2006
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2007
|
+
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1938
2008
|
});
|
|
1939
2009
|
|
|
1940
2010
|
test('reuses the Pro2 session opened by legacy getPassphraseState on the next App call', async () => {
|
|
@@ -2000,7 +2070,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2000
2070
|
true
|
|
2001
2071
|
);
|
|
2002
2072
|
expect(typedCall.mock.calls).toEqual([
|
|
2003
|
-
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
2004
2073
|
['DeviceSessionGet', 'DeviceSession', { session_id: 'session-pro2-app' }],
|
|
2005
2074
|
]);
|
|
2006
2075
|
});
|
|
@@ -2268,12 +2337,13 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2268
2337
|
);
|
|
2269
2338
|
|
|
2270
2339
|
expect(device.getInternalState()).toBeUndefined();
|
|
2271
|
-
expect(typedCall).
|
|
2340
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2341
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
2272
2342
|
passphrase: 'host hidden wallet',
|
|
2273
2343
|
on_device: false,
|
|
2274
2344
|
});
|
|
2275
|
-
expect(typedCall).
|
|
2276
|
-
expect(typedCall).
|
|
2345
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2346
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2277
2347
|
});
|
|
2278
2348
|
|
|
2279
2349
|
test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
|
|
@@ -2310,6 +2380,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2310
2380
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2311
2381
|
eventless_wallet_session: true,
|
|
2312
2382
|
});
|
|
2383
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
2384
|
+
type: DeviceSessionPinType.Main,
|
|
2385
|
+
});
|
|
2313
2386
|
});
|
|
2314
2387
|
|
|
2315
2388
|
test('marks fallback features as unavailable when DeviceInfo is missing', () => {
|
|
@@ -2680,7 +2753,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2680
2753
|
timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
|
|
2681
2754
|
}
|
|
2682
2755
|
);
|
|
2683
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2756
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2757
|
+
eventless_wallet_session: true,
|
|
2758
|
+
});
|
|
2684
2759
|
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
2685
2760
|
});
|
|
2686
2761
|
|
|
@@ -2718,7 +2793,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2718
2793
|
}),
|
|
2719
2794
|
expect.anything()
|
|
2720
2795
|
);
|
|
2721
|
-
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2796
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2797
|
+
eventless_wallet_session: true,
|
|
2798
|
+
});
|
|
2722
2799
|
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2723
2800
|
expect(device.features).toMatchObject({
|
|
2724
2801
|
deviceId: null,
|
|
@@ -2839,6 +2916,59 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2839
2916
|
});
|
|
2840
2917
|
});
|
|
2841
2918
|
|
|
2919
|
+
test('maps an explicit unsupported DeviceStatusGet response to bootloader for an unknown fingerprint', async () => {
|
|
2920
|
+
const device = Device.fromDescriptor({
|
|
2921
|
+
path: 'usb-path',
|
|
2922
|
+
protocolType: 'V2',
|
|
2923
|
+
} as any);
|
|
2924
|
+
const protocolInfo = {
|
|
2925
|
+
...protocolV2ApplicationInfo,
|
|
2926
|
+
build_fingerprint: 'legacy-fingerprint',
|
|
2927
|
+
};
|
|
2928
|
+
const typedCall = jest
|
|
2929
|
+
.fn()
|
|
2930
|
+
.mockResolvedValueOnce({ type: 'ProtocolInfo', message: protocolInfo })
|
|
2931
|
+
.mockRejectedValueOnce(new Error('Failure_UnexpectedMessage,Unknown message'));
|
|
2932
|
+
(device as any).commands = { typedCall };
|
|
2933
|
+
|
|
2934
|
+
await device.probeProtocolV2RuntimeState({
|
|
2935
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2936
|
+
fw: { bootloader: { version: '0.2.0' } },
|
|
2937
|
+
});
|
|
2938
|
+
|
|
2939
|
+
expect(device.features).toMatchObject({
|
|
2940
|
+
mode: 'bootloader',
|
|
2941
|
+
bootloaderMode: true,
|
|
2942
|
+
initialized: null,
|
|
2943
|
+
});
|
|
2944
|
+
});
|
|
2945
|
+
|
|
2946
|
+
test('propagates DeviceStatusGet link failures instead of mapping them to bootloader', async () => {
|
|
2947
|
+
const device = Device.fromDescriptor({
|
|
2948
|
+
path: 'usb-path',
|
|
2949
|
+
protocolType: 'V2',
|
|
2950
|
+
} as any);
|
|
2951
|
+
const protocolInfo = {
|
|
2952
|
+
...protocolV2ApplicationInfo,
|
|
2953
|
+
build_fingerprint: 'legacy-fingerprint',
|
|
2954
|
+
};
|
|
2955
|
+
const linkError = new Error('Protocol V2 CRC mismatch');
|
|
2956
|
+
const typedCall = jest
|
|
2957
|
+
.fn()
|
|
2958
|
+
.mockResolvedValueOnce({ type: 'ProtocolInfo', message: protocolInfo })
|
|
2959
|
+
.mockRejectedValueOnce(linkError);
|
|
2960
|
+
(device as any).commands = { typedCall };
|
|
2961
|
+
|
|
2962
|
+
await expect(
|
|
2963
|
+
device.probeProtocolV2RuntimeState({
|
|
2964
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2965
|
+
fw: { application: { version: '1.2.3' } },
|
|
2966
|
+
})
|
|
2967
|
+
).rejects.toBe(linkError);
|
|
2968
|
+
|
|
2969
|
+
expect(device.features).toBeUndefined();
|
|
2970
|
+
});
|
|
2971
|
+
|
|
2842
2972
|
test('fails safely when runtime fingerprint and capabilities are both unknown', async () => {
|
|
2843
2973
|
const device = Device.fromDescriptor({
|
|
2844
2974
|
path: 'usb-path',
|
|
@@ -2931,10 +3061,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2931
3061
|
fw: { application: { version: '1.2.4' } },
|
|
2932
3062
|
},
|
|
2933
3063
|
})
|
|
2934
|
-
.mockResolvedValueOnce({
|
|
2935
|
-
type: 'ProtocolInfo',
|
|
2936
|
-
message: protocolV2ApplicationInfo,
|
|
2937
|
-
})
|
|
2938
3064
|
.mockResolvedValueOnce({
|
|
2939
3065
|
type: 'DeviceStatus',
|
|
2940
3066
|
message: { init_states: true, unlocked: true },
|
|
@@ -2956,7 +3082,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2956
3082
|
firmwareVersion: '1.2.4',
|
|
2957
3083
|
passphraseProtection: null,
|
|
2958
3084
|
});
|
|
2959
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
3085
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
2960
3086
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
2961
3087
|
4,
|
|
2962
3088
|
'DeviceInfoGet',
|
|
@@ -3070,9 +3196,23 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3070
3196
|
status: { init_states: true },
|
|
3071
3197
|
}
|
|
3072
3198
|
);
|
|
3199
|
+
const pinOnDevice = jest.fn();
|
|
3200
|
+
device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
|
|
3073
3201
|
|
|
3074
3202
|
const features = await device.unlockDevice();
|
|
3075
3203
|
|
|
3204
|
+
expect(pinOnDevice).toHaveBeenCalledWith(
|
|
3205
|
+
device,
|
|
3206
|
+
DeviceSessionPinType.Main,
|
|
3207
|
+
expect.objectContaining({
|
|
3208
|
+
source: 'unlock-coordinator',
|
|
3209
|
+
reason: 'device-unlock',
|
|
3210
|
+
deviceOnly: true,
|
|
3211
|
+
})
|
|
3212
|
+
);
|
|
3213
|
+
expect(pinOnDevice.mock.invocationCallOrder[0]).toBeLessThan(
|
|
3214
|
+
typedCall.mock.invocationCallOrder[0]
|
|
3215
|
+
);
|
|
3076
3216
|
expect(typedCall.mock.calls).toEqual([
|
|
3077
3217
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
|
|
3078
3218
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
@@ -3118,9 +3258,12 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3118
3258
|
throw new Error(`Unexpected request: ${requestType}`);
|
|
3119
3259
|
});
|
|
3120
3260
|
(device as any).commands = { typedCall };
|
|
3261
|
+
const pinOnDevice = jest.fn();
|
|
3262
|
+
device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
|
|
3121
3263
|
|
|
3122
|
-
await device.unlockDevice();
|
|
3264
|
+
await device.unlockDevice(DeviceSessionPinType.Main, { emitUiEvent: false });
|
|
3123
3265
|
|
|
3266
|
+
expect(pinOnDevice).not.toHaveBeenCalled();
|
|
3124
3267
|
expect(typedCall.mock.calls).toEqual([
|
|
3125
3268
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
|
|
3126
3269
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
@@ -4339,6 +4482,19 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4339
4482
|
)
|
|
4340
4483
|
).toBe(true);
|
|
4341
4484
|
|
|
4485
|
+
(method.postProgressMessage as jest.Mock).mockClear();
|
|
4486
|
+
expect(
|
|
4487
|
+
(method as any).assertProtocolV2TargetStatus(
|
|
4488
|
+
[
|
|
4489
|
+
{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
|
|
4490
|
+
{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
|
|
4491
|
+
],
|
|
4492
|
+
new Set([4, 10])
|
|
4493
|
+
)
|
|
4494
|
+
).toBe(false);
|
|
4495
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(50, 'installingFirmware');
|
|
4496
|
+
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4497
|
+
|
|
4342
4498
|
expect(
|
|
4343
4499
|
(method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 1 }], expectedTargetIds)
|
|
4344
4500
|
).toBe(false);
|
|
@@ -4944,7 +5100,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4944
5100
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4945
5101
|
|
|
4946
5102
|
const order: string[] = [];
|
|
4947
|
-
const resourceBinary =
|
|
5103
|
+
const resourceBinary = buildOkppHeader().buffer as ArrayBuffer;
|
|
4948
5104
|
const getSysResourceBinarySpy = jest
|
|
4949
5105
|
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
4950
5106
|
.mockImplementation(() => {
|
|
@@ -5012,6 +5168,31 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5012
5168
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5013
5169
|
});
|
|
5014
5170
|
|
|
5171
|
+
test('rejects a remote RESC bundle without an OKPP header when metadata is absent', async () => {
|
|
5172
|
+
const method = new FirmwareUpdateV4({
|
|
5173
|
+
id: 1,
|
|
5174
|
+
payload: {
|
|
5175
|
+
method: 'firmwareUpdateV4',
|
|
5176
|
+
},
|
|
5177
|
+
});
|
|
5178
|
+
const getSysResourceBinarySpy = jest
|
|
5179
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
5180
|
+
.mockResolvedValue({ binary: new Uint8Array([1, 2, 3]).buffer });
|
|
5181
|
+
|
|
5182
|
+
await expect(
|
|
5183
|
+
(method as any).prefetchProtocolV2ResourceBundles([
|
|
5184
|
+
{
|
|
5185
|
+
name: 'images',
|
|
5186
|
+
binary: new ArrayBuffer(0),
|
|
5187
|
+
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
5188
|
+
url: 'https://example.com/images.okpkg',
|
|
5189
|
+
},
|
|
5190
|
+
])
|
|
5191
|
+
).rejects.toThrow('Invalid Protocol V2 RESC bundle header: images');
|
|
5192
|
+
|
|
5193
|
+
getSysResourceBinarySpy.mockRestore();
|
|
5194
|
+
});
|
|
5195
|
+
|
|
5015
5196
|
test('rejects an empty remote RESC bundle before entering bootloader', async () => {
|
|
5016
5197
|
const method = new FirmwareUpdateV4({
|
|
5017
5198
|
id: 1,
|
|
@@ -6012,311 +6193,230 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6012
6193
|
expect(method.unlockPolicy).toBe('none');
|
|
6013
6194
|
});
|
|
6014
6195
|
|
|
6015
|
-
test('
|
|
6196
|
+
test('checks fresh status for a wallet business method without a name allowlist', async () => {
|
|
6016
6197
|
const calls: string[] = [];
|
|
6198
|
+
const features = { unlocked: true };
|
|
6017
6199
|
const method = {
|
|
6018
|
-
name: '
|
|
6019
|
-
unlockPolicy: '
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
.
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
})
|
|
6027
|
-
.mockImplementationOnce(() => {
|
|
6028
|
-
calls.push('run-2');
|
|
6029
|
-
return Promise.resolve({ message: 'ok' });
|
|
6030
|
-
}),
|
|
6200
|
+
name: 'confluxSignMessageCIP23',
|
|
6201
|
+
unlockPolicy: 'none',
|
|
6202
|
+
useDevicePassphraseState: true,
|
|
6203
|
+
protocolV2UiInteraction: { reason: 'address-confirmation' },
|
|
6204
|
+
run: jest.fn(() => {
|
|
6205
|
+
calls.push('run');
|
|
6206
|
+
return Promise.resolve({ address: '0x1' });
|
|
6207
|
+
}),
|
|
6031
6208
|
};
|
|
6032
6209
|
const device = {
|
|
6210
|
+
features,
|
|
6211
|
+
commands: {
|
|
6212
|
+
typedCall: jest.fn(() => {
|
|
6213
|
+
calls.push('status');
|
|
6214
|
+
return Promise.resolve({ message: { unlocked: true } });
|
|
6215
|
+
}),
|
|
6216
|
+
},
|
|
6033
6217
|
isProtocolV2: () => true,
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6218
|
+
isBootloader: () => false,
|
|
6219
|
+
isRomloader: () => false,
|
|
6220
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
6221
|
+
features.unlocked = status.unlocked === true;
|
|
6222
|
+
return features;
|
|
6037
6223
|
}),
|
|
6224
|
+
unlockDevice: jest.fn(),
|
|
6038
6225
|
};
|
|
6039
6226
|
const uiCoordinator = {
|
|
6040
6227
|
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6041
|
-
enterUnlockInteraction: jest.fn(
|
|
6042
|
-
resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6228
|
+
enterUnlockInteraction: jest.fn(),
|
|
6043
6229
|
};
|
|
6044
6230
|
|
|
6045
6231
|
await expect(
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
]);
|
|
6232
|
+
runMethodWithUnlockPolicy(method as any, device as any, {
|
|
6233
|
+
uiCoordinator: uiCoordinator as any,
|
|
6234
|
+
})
|
|
6235
|
+
).resolves.toEqual({ address: '0x1' });
|
|
6236
|
+
|
|
6237
|
+
expect(calls).toEqual(['status', 'method-prompt', 'run']);
|
|
6238
|
+
expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
6239
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6240
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6056
6241
|
});
|
|
6057
6242
|
|
|
6058
|
-
test('
|
|
6243
|
+
test('validates the fresh device identity before starting unlock', async () => {
|
|
6059
6244
|
const calls: string[] = [];
|
|
6245
|
+
const identityError = new Error('Unexpected device');
|
|
6060
6246
|
const method = {
|
|
6061
6247
|
name: 'evmSignMessage',
|
|
6062
|
-
|
|
6248
|
+
unlockPolicy: 'none',
|
|
6063
6249
|
useDevicePassphraseState: true,
|
|
6064
|
-
|
|
6065
|
-
run: jest
|
|
6066
|
-
.fn()
|
|
6067
|
-
.mockImplementationOnce(() => {
|
|
6068
|
-
calls.push('run-1');
|
|
6069
|
-
return Promise.reject(deviceLockedError());
|
|
6070
|
-
})
|
|
6071
|
-
.mockImplementationOnce(() => {
|
|
6072
|
-
calls.push('run-2');
|
|
6073
|
-
return Promise.resolve({ message: 'ok' });
|
|
6074
|
-
}),
|
|
6250
|
+
run: jest.fn(),
|
|
6075
6251
|
};
|
|
6076
|
-
const
|
|
6077
|
-
if (requestType === 'ProtocolInfoRequest') {
|
|
6078
|
-
calls.push('negotiate-session');
|
|
6079
|
-
return Promise.resolve({ message: { version: 2 } });
|
|
6080
|
-
}
|
|
6081
|
-
if (requestType === 'DeviceSessionGet') {
|
|
6082
|
-
calls.push('resume-hidden-session');
|
|
6083
|
-
expect(request).toEqual({ session_id: 'hidden-session' });
|
|
6084
|
-
return Promise.resolve({
|
|
6085
|
-
message: {
|
|
6086
|
-
session_id: 'hidden-session',
|
|
6087
|
-
btc_test_address: 'hidden-state',
|
|
6088
|
-
},
|
|
6089
|
-
});
|
|
6090
|
-
}
|
|
6091
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
6092
|
-
});
|
|
6252
|
+
const features = { unlocked: false };
|
|
6093
6253
|
const device = {
|
|
6094
|
-
features
|
|
6095
|
-
|
|
6096
|
-
|
|
6254
|
+
features,
|
|
6255
|
+
commands: {
|
|
6256
|
+
typedCall: jest.fn(() => {
|
|
6257
|
+
calls.push('status');
|
|
6258
|
+
return Promise.resolve({ message: { unlocked: false } });
|
|
6259
|
+
}),
|
|
6260
|
+
},
|
|
6097
6261
|
isProtocolV2: () => true,
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
getCurrentPassphraseProtection: () => true,
|
|
6103
|
-
getInternalState: () => 'hidden-session',
|
|
6104
|
-
clearInternalState: jest.fn(),
|
|
6105
|
-
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6106
|
-
updateInternalState: jest.fn(() => calls.push('validate-hidden-session')),
|
|
6262
|
+
isBootloader: () => false,
|
|
6263
|
+
isRomloader: () => false,
|
|
6264
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
6265
|
+
unlockDevice: jest.fn(),
|
|
6107
6266
|
};
|
|
6108
6267
|
|
|
6109
|
-
await expect(
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
'validate-hidden-session',
|
|
6118
|
-
'run-2',
|
|
6119
|
-
]);
|
|
6120
|
-
});
|
|
6121
|
-
|
|
6122
|
-
test('does not replay a business method when the hidden-wallet session cannot be restored after unlock', async () => {
|
|
6123
|
-
const initialError = deviceLockedError();
|
|
6124
|
-
const restoreError = new Error('Invalid wallet session');
|
|
6125
|
-
const method = {
|
|
6126
|
-
name: 'evmSignMessage',
|
|
6127
|
-
payload: { passphraseState: 'hidden-state' },
|
|
6128
|
-
useDevicePassphraseState: true,
|
|
6129
|
-
unlockPolicy: 'retry-on-locked',
|
|
6130
|
-
run: jest.fn().mockRejectedValueOnce(initialError),
|
|
6131
|
-
};
|
|
6132
|
-
const typedCall = jest.fn((requestType: string) => {
|
|
6133
|
-
if (requestType === 'ProtocolInfoRequest') {
|
|
6134
|
-
return Promise.resolve({ message: { version: 2 } });
|
|
6135
|
-
}
|
|
6136
|
-
if (requestType === 'DeviceSessionGet') {
|
|
6137
|
-
return Promise.reject(restoreError);
|
|
6138
|
-
}
|
|
6139
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
6140
|
-
});
|
|
6141
|
-
const device = {
|
|
6142
|
-
features: { unlocked: true, passphraseProtection: true },
|
|
6143
|
-
passphraseState: 'hidden-state',
|
|
6144
|
-
commands: { typedCall },
|
|
6145
|
-
isProtocolV2: () => true,
|
|
6146
|
-
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6147
|
-
getCurrentPassphraseProtection: () => true,
|
|
6148
|
-
getInternalState: () => 'hidden-session',
|
|
6149
|
-
clearInternalState: jest.fn(),
|
|
6150
|
-
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6151
|
-
updateInternalState: jest.fn(),
|
|
6152
|
-
};
|
|
6268
|
+
await expect(
|
|
6269
|
+
runMethodWithUnlockPolicy(method as any, device as any, {
|
|
6270
|
+
afterStatusBeforeUnlock: () => {
|
|
6271
|
+
calls.push('identity');
|
|
6272
|
+
throw identityError;
|
|
6273
|
+
},
|
|
6274
|
+
})
|
|
6275
|
+
).rejects.toBe(identityError);
|
|
6153
6276
|
|
|
6154
|
-
|
|
6155
|
-
expect(
|
|
6156
|
-
expect(
|
|
6157
|
-
expect(device.clearInternalState).toHaveBeenCalledTimes(1);
|
|
6158
|
-
expect(device.updateInternalState).not.toHaveBeenCalled();
|
|
6277
|
+
expect(calls).toEqual(['status', 'identity']);
|
|
6278
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6279
|
+
expect(method.run).not.toHaveBeenCalled();
|
|
6159
6280
|
});
|
|
6160
6281
|
|
|
6161
|
-
test('
|
|
6282
|
+
test('unlocks before business and never replays the callback', async () => {
|
|
6162
6283
|
const calls: string[] = [];
|
|
6284
|
+
const features = { unlocked: true };
|
|
6163
6285
|
const method = {
|
|
6164
6286
|
name: 'evmSignMessage',
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
unlockPolicy: 'retry-on-locked',
|
|
6287
|
+
unlockPolicy: 'unlock-before-run',
|
|
6288
|
+
protocolV2UiInteraction: { reason: 'signing-confirmation' },
|
|
6168
6289
|
run: jest.fn(() => {
|
|
6169
6290
|
calls.push('run');
|
|
6170
|
-
return Promise.resolve({
|
|
6291
|
+
return Promise.resolve({ signature: 'signed' });
|
|
6171
6292
|
}),
|
|
6172
6293
|
};
|
|
6173
|
-
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
6174
|
-
if (requestType === 'ProtocolInfoRequest') {
|
|
6175
|
-
calls.push('negotiate-session');
|
|
6176
|
-
return Promise.resolve({ message: { version: 2 } });
|
|
6177
|
-
}
|
|
6178
|
-
if (requestType === 'DeviceSessionGet') {
|
|
6179
|
-
calls.push('resume-hidden-session');
|
|
6180
|
-
expect(request).toEqual({ session_id: 'hidden-session' });
|
|
6181
|
-
return Promise.resolve({
|
|
6182
|
-
message: {
|
|
6183
|
-
session_id: 'hidden-session',
|
|
6184
|
-
btc_test_address: 'hidden-state',
|
|
6185
|
-
},
|
|
6186
|
-
});
|
|
6187
|
-
}
|
|
6188
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
6189
|
-
});
|
|
6190
|
-
const features = {
|
|
6191
|
-
unlocked: false,
|
|
6192
|
-
unlockedAttachPin: true,
|
|
6193
|
-
passphraseProtection: true,
|
|
6194
|
-
};
|
|
6195
6294
|
const device = {
|
|
6196
6295
|
features,
|
|
6197
|
-
|
|
6198
|
-
|
|
6296
|
+
commands: {
|
|
6297
|
+
typedCall: jest.fn(() => {
|
|
6298
|
+
calls.push('status');
|
|
6299
|
+
return Promise.resolve({ message: { unlocked: false } });
|
|
6300
|
+
}),
|
|
6301
|
+
},
|
|
6199
6302
|
isProtocolV2: () => true,
|
|
6303
|
+
isBootloader: () => false,
|
|
6304
|
+
isRomloader: () => false,
|
|
6305
|
+
updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
|
|
6306
|
+
features.unlocked = status.unlocked === true;
|
|
6307
|
+
return features;
|
|
6308
|
+
}),
|
|
6200
6309
|
unlockDevice: jest.fn(() => {
|
|
6201
|
-
calls.push('unlock
|
|
6310
|
+
calls.push('unlock');
|
|
6202
6311
|
features.unlocked = true;
|
|
6203
|
-
features
|
|
6204
|
-
|
|
6312
|
+
return Promise.resolve(features);
|
|
6313
|
+
}),
|
|
6314
|
+
};
|
|
6315
|
+
const uiCoordinator = {
|
|
6316
|
+
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6317
|
+
enterUnlockInteraction: jest.fn(() => {
|
|
6318
|
+
calls.push('unlock-prompt');
|
|
6319
|
+
return undefined;
|
|
6205
6320
|
}),
|
|
6206
|
-
getCurrentPassphraseProtection: () => true,
|
|
6207
|
-
getInternalState: () => 'hidden-session',
|
|
6208
|
-
clearInternalState: jest.fn(),
|
|
6209
|
-
getCurrentDeviceId: () => 'wallet-device-id',
|
|
6210
|
-
updateInternalState: jest.fn(() => calls.push('validate-hidden-session')),
|
|
6211
6321
|
};
|
|
6212
6322
|
|
|
6213
|
-
await expect(
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6323
|
+
await expect(
|
|
6324
|
+
runMethodWithUnlockPolicy(method as any, device as any, {
|
|
6325
|
+
uiCoordinator: uiCoordinator as any,
|
|
6326
|
+
prepare: () => {
|
|
6327
|
+
calls.push('wallet-session');
|
|
6328
|
+
return Promise.resolve();
|
|
6329
|
+
},
|
|
6330
|
+
})
|
|
6331
|
+
).resolves.toEqual({ signature: 'signed' });
|
|
6332
|
+
|
|
6217
6333
|
expect(calls).toEqual([
|
|
6218
|
-
'
|
|
6219
|
-
'
|
|
6220
|
-
'
|
|
6221
|
-
'
|
|
6334
|
+
'status',
|
|
6335
|
+
'unlock-prompt',
|
|
6336
|
+
'unlock',
|
|
6337
|
+
'wallet-session',
|
|
6338
|
+
'method-prompt',
|
|
6222
6339
|
'run',
|
|
6223
6340
|
]);
|
|
6341
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
6342
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6224
6343
|
});
|
|
6225
6344
|
|
|
6226
|
-
test('
|
|
6345
|
+
test('fails closed when fresh status does not explicitly report unlocked state', async () => {
|
|
6227
6346
|
const method = {
|
|
6228
6347
|
name: 'evmGetAddress',
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
unlockPolicy: 'retry-on-locked',
|
|
6232
|
-
run: jest.fn().mockResolvedValue({ address: 'standard-wallet-address' }),
|
|
6348
|
+
unlockPolicy: 'unlock-before-run',
|
|
6349
|
+
run: jest.fn(),
|
|
6233
6350
|
};
|
|
6234
6351
|
const device = {
|
|
6235
|
-
features: { unlocked:
|
|
6236
|
-
|
|
6352
|
+
features: { unlocked: undefined },
|
|
6353
|
+
commands: { typedCall: jest.fn().mockResolvedValue({ message: {} }) },
|
|
6237
6354
|
isProtocolV2: () => true,
|
|
6238
|
-
|
|
6355
|
+
isBootloader: () => false,
|
|
6356
|
+
isRomloader: () => false,
|
|
6357
|
+
updateProtocolV2Status: jest.fn(() => ({ unlocked: undefined })),
|
|
6358
|
+
unlockDevice: jest.fn(),
|
|
6239
6359
|
};
|
|
6240
6360
|
|
|
6241
|
-
await expect(
|
|
6242
|
-
|
|
6361
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).rejects.toMatchObject({
|
|
6362
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
6243
6363
|
});
|
|
6244
|
-
expect(device.unlockDevice).
|
|
6245
|
-
expect(method.run).
|
|
6364
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6365
|
+
expect(method.run).not.toHaveBeenCalled();
|
|
6246
6366
|
});
|
|
6247
6367
|
|
|
6248
|
-
test('
|
|
6249
|
-
const
|
|
6368
|
+
test('returns a business DeviceLocked error without unlocking or replaying', async () => {
|
|
6369
|
+
const error = deviceLockedError();
|
|
6250
6370
|
const method = {
|
|
6251
|
-
name: '
|
|
6371
|
+
name: 'evmSignMessage',
|
|
6252
6372
|
unlockPolicy: 'unlock-before-run',
|
|
6253
|
-
|
|
6254
|
-
run: jest.fn(() => {
|
|
6255
|
-
calls.push('run');
|
|
6256
|
-
return Promise.resolve({ message: 'ok' });
|
|
6257
|
-
}),
|
|
6373
|
+
run: jest.fn().mockRejectedValue(error),
|
|
6258
6374
|
};
|
|
6259
6375
|
const features = { unlocked: true };
|
|
6260
6376
|
const device = {
|
|
6261
6377
|
features,
|
|
6262
|
-
commands: {
|
|
6263
|
-
typedCall: jest.fn(() => {
|
|
6264
|
-
calls.push('refresh-status');
|
|
6265
|
-
return Promise.resolve({ message: { unlocked: false } });
|
|
6266
|
-
}),
|
|
6267
|
-
},
|
|
6378
|
+
commands: { typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }) },
|
|
6268
6379
|
isProtocolV2: () => true,
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
unlockDevice: jest.fn(() => {
|
|
6274
|
-
calls.push('unlock');
|
|
6275
|
-
features.unlocked = true;
|
|
6276
|
-
return Promise.resolve();
|
|
6277
|
-
}),
|
|
6278
|
-
};
|
|
6279
|
-
const uiCoordinator = {
|
|
6280
|
-
enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6281
|
-
enterUnlockInteraction: jest.fn(() => calls.push('unlock-prompt')),
|
|
6282
|
-
resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
|
|
6380
|
+
isBootloader: () => false,
|
|
6381
|
+
isRomloader: () => false,
|
|
6382
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
6383
|
+
unlockDevice: jest.fn(),
|
|
6283
6384
|
};
|
|
6284
6385
|
|
|
6285
|
-
await expect(
|
|
6286
|
-
|
|
6287
|
-
).resolves.toEqual({ message: 'ok' });
|
|
6288
|
-
expect(calls).toEqual(['refresh-status', 'unlock-prompt', 'unlock', 'method-prompt', 'run']);
|
|
6289
|
-
expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
6386
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).rejects.toBe(error);
|
|
6387
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6290
6388
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6291
|
-
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledTimes(1);
|
|
6292
|
-
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6293
6389
|
});
|
|
6294
6390
|
|
|
6295
|
-
test('
|
|
6296
|
-
const features = { unlocked:
|
|
6297
|
-
const
|
|
6298
|
-
|
|
6391
|
+
test('reuses one lightweight preflight context across nested protected methods', async () => {
|
|
6392
|
+
const features = { unlocked: true };
|
|
6393
|
+
const context = createProtocolV2UnlockContext();
|
|
6394
|
+
const firstMethod = {
|
|
6395
|
+
name: 'allNetworkGetAddress',
|
|
6299
6396
|
unlockPolicy: 'unlock-before-run',
|
|
6300
|
-
run: jest.fn().mockResolvedValue({
|
|
6397
|
+
run: jest.fn().mockResolvedValue({ root: true }),
|
|
6398
|
+
};
|
|
6399
|
+
const nestedMethod = {
|
|
6400
|
+
name: 'evmGetAddress',
|
|
6401
|
+
unlockPolicy: 'unlock-before-run',
|
|
6402
|
+
run: jest.fn().mockResolvedValue({ address: '0x1' }),
|
|
6301
6403
|
};
|
|
6302
6404
|
const device = {
|
|
6303
6405
|
features,
|
|
6304
|
-
commands: {
|
|
6305
|
-
typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
|
|
6306
|
-
},
|
|
6406
|
+
commands: { typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }) },
|
|
6307
6407
|
isProtocolV2: () => true,
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
}),
|
|
6408
|
+
isBootloader: () => false,
|
|
6409
|
+
isRomloader: () => false,
|
|
6410
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
6312
6411
|
unlockDevice: jest.fn(),
|
|
6313
6412
|
};
|
|
6314
6413
|
|
|
6315
|
-
await
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
expect(device.
|
|
6319
|
-
expect(
|
|
6414
|
+
await runMethodWithUnlockPolicy(firstMethod as any, device as any, { context });
|
|
6415
|
+
await runMethodWithUnlockPolicy(nestedMethod as any, device as any, { context });
|
|
6416
|
+
|
|
6417
|
+
expect(device.commands.typedCall).toHaveBeenCalledTimes(1);
|
|
6418
|
+
expect(firstMethod.run).toHaveBeenCalledTimes(1);
|
|
6419
|
+
expect(nestedMethod.run).toHaveBeenCalledTimes(1);
|
|
6320
6420
|
});
|
|
6321
6421
|
|
|
6322
6422
|
test.each(['bootloader', 'romloader'] as const)(
|
|
@@ -6336,7 +6436,7 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6336
6436
|
unlockDevice: jest.fn(),
|
|
6337
6437
|
};
|
|
6338
6438
|
|
|
6339
|
-
await expect(
|
|
6439
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
|
|
6340
6440
|
message: 'updating',
|
|
6341
6441
|
});
|
|
6342
6442
|
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
@@ -6345,157 +6445,30 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
6345
6445
|
}
|
|
6346
6446
|
);
|
|
6347
6447
|
|
|
6348
|
-
test('
|
|
6349
|
-
const method = {
|
|
6350
|
-
name: 'evmSignMessage',
|
|
6351
|
-
params: { message: 'not-exposed-in-event' },
|
|
6352
|
-
payload: {},
|
|
6353
|
-
unlockPolicy: 'retry-on-locked',
|
|
6354
|
-
run: jest.fn().mockResolvedValue({ signature: 'test' }),
|
|
6355
|
-
};
|
|
6356
|
-
const device = {
|
|
6357
|
-
isProtocolV2: () => true,
|
|
6358
|
-
unlockDevice: jest.fn(),
|
|
6359
|
-
};
|
|
6360
|
-
const uiCoordinator = {
|
|
6361
|
-
enterMethodInteraction: jest.fn(),
|
|
6362
|
-
enterUnlockInteraction: jest.fn(),
|
|
6363
|
-
resumeMethodInteraction: jest.fn(),
|
|
6364
|
-
};
|
|
6365
|
-
|
|
6366
|
-
await runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any);
|
|
6367
|
-
|
|
6368
|
-
expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledWith({
|
|
6369
|
-
request: 'button',
|
|
6370
|
-
source: 'method-lifecycle',
|
|
6371
|
-
reason: 'signing-confirmation',
|
|
6372
|
-
completion: 'operation-completed',
|
|
6373
|
-
deviceOnly: true,
|
|
6374
|
-
operation: 'evmSignMessage',
|
|
6375
|
-
});
|
|
6376
|
-
});
|
|
6377
|
-
|
|
6378
|
-
test('does not unlock a Protocol V1 device or a method without the policy', async () => {
|
|
6448
|
+
test('keeps Protocol V1 and lock-free methods outside the preflight path', async () => {
|
|
6379
6449
|
for (const [isProtocolV2, unlockPolicy] of [
|
|
6380
|
-
[false, '
|
|
6450
|
+
[false, 'unlock-before-run'],
|
|
6381
6451
|
[true, 'none'],
|
|
6382
6452
|
] as const) {
|
|
6383
|
-
const error = deviceLockedError();
|
|
6384
6453
|
const method = {
|
|
6385
6454
|
unlockPolicy,
|
|
6386
|
-
|
|
6455
|
+
useDevicePassphraseState: false,
|
|
6456
|
+
run: jest.fn().mockResolvedValue({ message: 'ok' }),
|
|
6387
6457
|
};
|
|
6388
6458
|
const device = {
|
|
6459
|
+
commands: { typedCall: jest.fn() },
|
|
6389
6460
|
isProtocolV2: () => isProtocolV2,
|
|
6390
6461
|
unlockDevice: jest.fn(),
|
|
6391
6462
|
};
|
|
6392
6463
|
|
|
6393
|
-
await expect(
|
|
6464
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
|
|
6465
|
+
message: 'ok',
|
|
6466
|
+
});
|
|
6467
|
+
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
6394
6468
|
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6395
6469
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6396
6470
|
}
|
|
6397
6471
|
});
|
|
6398
|
-
|
|
6399
|
-
test('runs lock-free eventless methods without unlocking or synthesized UI', async () => {
|
|
6400
|
-
const method = {
|
|
6401
|
-
name: 'uploadPortfolio',
|
|
6402
|
-
unlockPolicy: 'none',
|
|
6403
|
-
protocolV2UiMode: 'none',
|
|
6404
|
-
run: jest.fn().mockResolvedValue({ message: 'ok' }),
|
|
6405
|
-
};
|
|
6406
|
-
const device = {
|
|
6407
|
-
features: { unlocked: false },
|
|
6408
|
-
isProtocolV2: () => true,
|
|
6409
|
-
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6410
|
-
};
|
|
6411
|
-
const uiCoordinator = {
|
|
6412
|
-
enterMethodInteraction: jest.fn(),
|
|
6413
|
-
enterUnlockInteraction: jest.fn(),
|
|
6414
|
-
resumeMethodInteraction: jest.fn(),
|
|
6415
|
-
};
|
|
6416
|
-
|
|
6417
|
-
await expect(
|
|
6418
|
-
runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
|
|
6419
|
-
).resolves.toEqual({ message: 'ok' });
|
|
6420
|
-
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6421
|
-
expect(uiCoordinator.enterMethodInteraction).not.toHaveBeenCalled();
|
|
6422
|
-
expect(uiCoordinator.enterUnlockInteraction).not.toHaveBeenCalled();
|
|
6423
|
-
expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6424
|
-
});
|
|
6425
|
-
|
|
6426
|
-
test('does not unlock or replay a lock-free state-changing method after a locked response', async () => {
|
|
6427
|
-
const error = deviceLockedError();
|
|
6428
|
-
const method = {
|
|
6429
|
-
name: 'uploadPortfolio',
|
|
6430
|
-
unlockPolicy: 'none',
|
|
6431
|
-
run: jest.fn().mockRejectedValue(error),
|
|
6432
|
-
};
|
|
6433
|
-
const device = {
|
|
6434
|
-
features: { unlocked: true },
|
|
6435
|
-
isProtocolV2: () => true,
|
|
6436
|
-
unlockDevice: jest.fn(),
|
|
6437
|
-
};
|
|
6438
|
-
|
|
6439
|
-
await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error);
|
|
6440
|
-
expect(method.run).toHaveBeenCalledTimes(1);
|
|
6441
|
-
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
6442
|
-
});
|
|
6443
|
-
|
|
6444
|
-
test('does not retry when unlock fails or when the retry is still locked', async () => {
|
|
6445
|
-
const initialError = deviceLockedError();
|
|
6446
|
-
const unlockError = new Error('PIN cancelled');
|
|
6447
|
-
const unlockFailMethod = {
|
|
6448
|
-
unlockPolicy: 'retry-on-locked',
|
|
6449
|
-
run: jest.fn().mockRejectedValue(initialError),
|
|
6450
|
-
};
|
|
6451
|
-
const unlockFailDevice = {
|
|
6452
|
-
isProtocolV2: () => true,
|
|
6453
|
-
unlockDevice: jest.fn().mockRejectedValue(unlockError),
|
|
6454
|
-
};
|
|
6455
|
-
const unlockFailCoordinator = {
|
|
6456
|
-
enterMethodInteraction: jest.fn(),
|
|
6457
|
-
enterUnlockInteraction: jest.fn(),
|
|
6458
|
-
resumeMethodInteraction: jest.fn(),
|
|
6459
|
-
};
|
|
6460
|
-
|
|
6461
|
-
await expect(
|
|
6462
|
-
runMethodWithUnlockRetry(
|
|
6463
|
-
unlockFailMethod as any,
|
|
6464
|
-
unlockFailDevice as any,
|
|
6465
|
-
unlockFailCoordinator as any
|
|
6466
|
-
)
|
|
6467
|
-
).rejects.toBe(unlockError);
|
|
6468
|
-
expect(unlockFailMethod.run).toHaveBeenCalledTimes(1);
|
|
6469
|
-
expect(unlockFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
|
|
6470
|
-
expect(unlockFailCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
|
|
6471
|
-
|
|
6472
|
-
const retryError = deviceLockedError();
|
|
6473
|
-
const retryFailMethod = {
|
|
6474
|
-
unlockPolicy: 'retry-on-locked',
|
|
6475
|
-
run: jest.fn().mockRejectedValueOnce(initialError).mockRejectedValueOnce(retryError),
|
|
6476
|
-
};
|
|
6477
|
-
const retryFailDevice = {
|
|
6478
|
-
isProtocolV2: () => true,
|
|
6479
|
-
unlockDevice: jest.fn().mockResolvedValue(undefined),
|
|
6480
|
-
};
|
|
6481
|
-
const retryFailCoordinator = {
|
|
6482
|
-
enterMethodInteraction: jest.fn(),
|
|
6483
|
-
enterUnlockInteraction: jest.fn(),
|
|
6484
|
-
resumeMethodInteraction: jest.fn(),
|
|
6485
|
-
};
|
|
6486
|
-
|
|
6487
|
-
await expect(
|
|
6488
|
-
runMethodWithUnlockRetry(
|
|
6489
|
-
retryFailMethod as any,
|
|
6490
|
-
retryFailDevice as any,
|
|
6491
|
-
retryFailCoordinator as any
|
|
6492
|
-
)
|
|
6493
|
-
).rejects.toBe(retryError);
|
|
6494
|
-
expect(retryFailMethod.run).toHaveBeenCalledTimes(2);
|
|
6495
|
-
expect(retryFailDevice.unlockDevice).toHaveBeenCalledTimes(1);
|
|
6496
|
-
expect(retryFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
|
|
6497
|
-
expect(retryFailCoordinator.resumeMethodInteraction).toHaveBeenCalledTimes(1);
|
|
6498
|
-
});
|
|
6499
6472
|
});
|
|
6500
6473
|
|
|
6501
6474
|
describe('Protocol V2 current low-level methods', () => {
|
|
@@ -6627,7 +6600,9 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
6627
6600
|
|
|
6628
6601
|
await method.run();
|
|
6629
6602
|
|
|
6630
|
-
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
6603
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
6604
|
+
eventless_wallet_session: true,
|
|
6605
|
+
});
|
|
6631
6606
|
});
|
|
6632
6607
|
|
|
6633
6608
|
test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => {
|