@onekeyfe/hd-core 1.2.0-alpha.42 → 1.2.0-alpha.43
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__/deviceUploadNft.test.ts +87 -2
- package/__tests__/open-wallet-session.test.ts +114 -20
- package/__tests__/protocol-v2-ui-lifecycle.test.ts +35 -0
- package/__tests__/protocol-v2-unlock-policy.test.ts +110 -0
- package/__tests__/protocol-v2.test.ts +54 -14
- package/__tests__/protocolV2UiInteraction.test.ts +63 -0
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +1 -0
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +3 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/events/ui-request.d.ts +1 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +157 -38
- package/dist/protocols/protocol-v2/uiInteraction.d.ts +4 -1
- package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/utils/pro2Nft.d.ts +2 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/protocol-v2/DeviceUploadNft.ts +25 -2
- package/src/core/index.ts +18 -2
- package/src/data/messages/messages-protocol-v2.json +15 -0
- package/src/device/Device.ts +15 -5
- package/src/events/ui-request.ts +1 -0
- package/src/protocols/protocol-v2/uiInteraction.ts +20 -6
- package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
- package/src/protocols/protocol-v2/walletSession.ts +66 -17
- package/src/utils/pro2Nft.ts +51 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
1
3
|
import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
|
|
2
4
|
|
|
3
5
|
jest.mock('../src/data/config', () => ({
|
|
@@ -13,7 +15,7 @@ const createRgba = (width: number, height: number) => {
|
|
|
13
15
|
|
|
14
16
|
const createMethod = ({
|
|
15
17
|
typedCall,
|
|
16
|
-
supportedMessages = [60805, 61500],
|
|
18
|
+
supportedMessages = [60805, 60808, 61500],
|
|
17
19
|
useFullBundle = false,
|
|
18
20
|
}: {
|
|
19
21
|
typedCall: jest.Mock;
|
|
@@ -67,9 +69,23 @@ const fileWriteSuccess = (params: { file: { offset: number; data: Uint8Array } }
|
|
|
67
69
|
},
|
|
68
70
|
});
|
|
69
71
|
|
|
72
|
+
const nftFileList = (count: number, basenameAt?: number) =>
|
|
73
|
+
Array.from({ length: count }, (_, index) => {
|
|
74
|
+
const basename =
|
|
75
|
+
index === basenameAt
|
|
76
|
+
? 'nft-deadbeef-1760000000000'
|
|
77
|
+
: `nft-${index.toString(16).padStart(8, '0')}-${1760000000001 + index}`;
|
|
78
|
+
return [`${basename}.bin`, `${basename}_m.bin`, `${basename}.json`];
|
|
79
|
+
})
|
|
80
|
+
.flat()
|
|
81
|
+
.join('\n');
|
|
82
|
+
|
|
70
83
|
describe('DeviceUploadNft', () => {
|
|
71
84
|
test('uploads the triplet in order without creating the firmware-owned directory', async () => {
|
|
72
85
|
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
86
|
+
if (request === 'FilesystemDirList') {
|
|
87
|
+
return { message: { path: 'vol1:/nft', child_files: '' } };
|
|
88
|
+
}
|
|
73
89
|
if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
|
|
74
90
|
if (request === 'NftUpdate') return { message: { message: 'NFT updated' } };
|
|
75
91
|
throw new Error(`Unexpected request: ${request}`);
|
|
@@ -79,7 +95,14 @@ describe('DeviceUploadNft', () => {
|
|
|
79
95
|
const result = await method.run();
|
|
80
96
|
|
|
81
97
|
const requests = typedCall.mock.calls.map(call => call[0]);
|
|
82
|
-
expect(requests[0]).toBe('
|
|
98
|
+
expect(requests[0]).toBe('FilesystemDirList');
|
|
99
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
100
|
+
1,
|
|
101
|
+
'FilesystemDirList',
|
|
102
|
+
'FilesystemDir',
|
|
103
|
+
{ path: 'vol1:/nft', depth: 1 },
|
|
104
|
+
{ timeoutMs: 15_000 }
|
|
105
|
+
);
|
|
83
106
|
expect(requests).not.toContain('FilesystemDirMake');
|
|
84
107
|
expect(requests.at(-1)).toBe('NftUpdate');
|
|
85
108
|
const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
|
|
@@ -122,6 +145,9 @@ describe('DeviceUploadNft', () => {
|
|
|
122
145
|
});
|
|
123
146
|
let updateCalls = 0;
|
|
124
147
|
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
148
|
+
if (request === 'FilesystemDirList') {
|
|
149
|
+
return { message: { path: 'vol1:/nft', child_files: '' } };
|
|
150
|
+
}
|
|
125
151
|
if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
|
|
126
152
|
if (request === 'NftUpdate') {
|
|
127
153
|
updateCalls += 1;
|
|
@@ -141,6 +167,9 @@ describe('DeviceUploadNft', () => {
|
|
|
141
167
|
|
|
142
168
|
test('does not retry a non-timeout NftUpdate failure', async () => {
|
|
143
169
|
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
170
|
+
if (request === 'FilesystemDirList') {
|
|
171
|
+
return { message: { path: 'vol1:/nft', child_files: '' } };
|
|
172
|
+
}
|
|
144
173
|
if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
|
|
145
174
|
if (request === 'NftUpdate') throw new Error('Invalid NFT metadata');
|
|
146
175
|
throw new Error(`Unexpected request: ${request}`);
|
|
@@ -156,6 +185,9 @@ describe('DeviceUploadNft', () => {
|
|
|
156
185
|
code: 'response-timeout',
|
|
157
186
|
});
|
|
158
187
|
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
188
|
+
if (request === 'FilesystemDirList') {
|
|
189
|
+
return { message: { path: 'vol1:/nft', child_files: '' } };
|
|
190
|
+
}
|
|
159
191
|
if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
|
|
160
192
|
if (request === 'NftUpdate') throw timeout;
|
|
161
193
|
throw new Error(`Unexpected request: ${request}`);
|
|
@@ -168,6 +200,9 @@ describe('DeviceUploadNft', () => {
|
|
|
168
200
|
|
|
169
201
|
test('does not publish when a file write fails', async () => {
|
|
170
202
|
const typedCall = jest.fn((request: string) => {
|
|
203
|
+
if (request === 'FilesystemDirList') {
|
|
204
|
+
return { message: { path: 'vol1:/nft', child_files: '' } };
|
|
205
|
+
}
|
|
171
206
|
if (request === 'FilesystemFileWrite') throw new Error('Filesystem full');
|
|
172
207
|
throw new Error(`Unexpected request: ${request}`);
|
|
173
208
|
});
|
|
@@ -184,4 +219,54 @@ describe('DeviceUploadNft', () => {
|
|
|
184
219
|
await expect(method.run()).rejects.toBeDefined();
|
|
185
220
|
expect(typedCall).not.toHaveBeenCalled();
|
|
186
221
|
});
|
|
222
|
+
|
|
223
|
+
test('rejects a new NFT before writing when ten complete NFT bundles exist', async () => {
|
|
224
|
+
const typedCall = jest.fn((request: string) => {
|
|
225
|
+
if (request === 'FilesystemDirList') {
|
|
226
|
+
return { message: { path: 'vol1:/nft', child_files: nftFileList(10) } };
|
|
227
|
+
}
|
|
228
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
229
|
+
});
|
|
230
|
+
const method = createMethod({ typedCall });
|
|
231
|
+
|
|
232
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
233
|
+
errorCode: HardwareErrorCode.NftStorageLimitReached,
|
|
234
|
+
params: { count: 10, limit: 10 },
|
|
235
|
+
});
|
|
236
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['FilesystemDirList']);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('allows an idempotent retry for a basename already counted at the limit', async () => {
|
|
240
|
+
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
241
|
+
if (request === 'FilesystemDirList') {
|
|
242
|
+
return { message: { path: 'vol1:/nft', child_files: nftFileList(10, 0) } };
|
|
243
|
+
}
|
|
244
|
+
if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
|
|
245
|
+
if (request === 'NftUpdate') return { message: {} };
|
|
246
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
247
|
+
});
|
|
248
|
+
const method = createMethod({ typedCall });
|
|
249
|
+
|
|
250
|
+
await expect(method.run()).resolves.toMatchObject({ nftUpdated: true });
|
|
251
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileWrite')).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('does not count unrelated or incomplete files as stored NFTs', async () => {
|
|
255
|
+
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
256
|
+
if (request === 'FilesystemDirList') {
|
|
257
|
+
return {
|
|
258
|
+
message: {
|
|
259
|
+
path: 'vol1:/nft',
|
|
260
|
+
child_files: `${nftFileList(9)}\nnft-ffffffff-1760000009999.json\nnotes.txt`,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
|
|
265
|
+
if (request === 'NftUpdate') return { message: {} };
|
|
266
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
267
|
+
});
|
|
268
|
+
const method = createMethod({ typedCall });
|
|
269
|
+
|
|
270
|
+
await expect(method.run()).resolves.toMatchObject({ nftUpdated: true });
|
|
271
|
+
});
|
|
187
272
|
});
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
|
|
2
|
+
import { DeviceSessionPinType, DeviceSessionSeedDomain } from '@onekeyfe/hd-transport';
|
|
3
3
|
|
|
4
4
|
import GetPassphraseState from '../src/api/GetPassphraseState';
|
|
5
5
|
import OpenWalletSession from '../src/api/OpenWalletSession';
|
|
6
6
|
import { Device } from '../src/device/Device';
|
|
7
7
|
import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
ensureProtocolV2WalletSessionUnlocked,
|
|
10
|
+
getProtocolV2WalletSession,
|
|
11
|
+
} from '../src/protocols/protocol-v2/walletSession';
|
|
9
12
|
|
|
10
13
|
jest.mock('../src/data/config', () => ({
|
|
11
14
|
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
@@ -95,6 +98,65 @@ describe('openWalletSession', () => {
|
|
|
95
98
|
deviceWalletSessionStore.clear();
|
|
96
99
|
});
|
|
97
100
|
|
|
101
|
+
test('reuses the authoritative Main wallet DeviceStatus', async () => {
|
|
102
|
+
const typedCall = jest.fn((request: string) => {
|
|
103
|
+
if (request === 'ProtocolInfoRequest') {
|
|
104
|
+
return { message: { version: 2 } };
|
|
105
|
+
}
|
|
106
|
+
if (request === 'DeviceSessionGet') {
|
|
107
|
+
return {
|
|
108
|
+
message: {
|
|
109
|
+
btc_test_address: 'standard-state',
|
|
110
|
+
session_id: 'standard-session',
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
115
|
+
});
|
|
116
|
+
const device = createDevice({ typedCall });
|
|
117
|
+
|
|
118
|
+
await expect(
|
|
119
|
+
getProtocolV2WalletSession(device as any, {
|
|
120
|
+
onlyMainPin: true,
|
|
121
|
+
})
|
|
122
|
+
).resolves.toMatchObject({
|
|
123
|
+
passphraseState: 'standard-state',
|
|
124
|
+
newSession: 'standard-session',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
128
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
129
|
+
seed_domains: [],
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('does not treat an Attach PIN DeviceStatus as the Main wallet', async () => {
|
|
134
|
+
const typedCall = jest.fn((request: string) => {
|
|
135
|
+
if (request === 'ProtocolInfoRequest') {
|
|
136
|
+
return { message: { version: 2 } };
|
|
137
|
+
}
|
|
138
|
+
if (request === 'DeviceSessionGet') {
|
|
139
|
+
return {
|
|
140
|
+
message: {
|
|
141
|
+
btc_test_address: 'standard-state',
|
|
142
|
+
session_id: 'standard-session',
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
147
|
+
});
|
|
148
|
+
const device = createDevice({ typedCall });
|
|
149
|
+
device.features.unlockedAttachPin = true;
|
|
150
|
+
|
|
151
|
+
await getProtocolV2WalletSession(device as any, { onlyMainPin: true });
|
|
152
|
+
|
|
153
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
|
|
154
|
+
source: 'wallet-session-coordinator',
|
|
155
|
+
reason: 'open-wallet',
|
|
156
|
+
deviceOnly: true,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
98
160
|
test('unlocks a locked Protocol V2 device before restoring a wallet session', async () => {
|
|
99
161
|
const device = {
|
|
100
162
|
commands: {
|
|
@@ -190,7 +252,9 @@ describe('openWalletSession', () => {
|
|
|
190
252
|
passphrase: 'host hidden wallet',
|
|
191
253
|
on_device: false,
|
|
192
254
|
});
|
|
193
|
-
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
255
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
256
|
+
seed_domains: [],
|
|
257
|
+
});
|
|
194
258
|
});
|
|
195
259
|
|
|
196
260
|
test('keeps Legacy Protocol V1 getPassphraseState parameterless', async () => {
|
|
@@ -308,7 +372,9 @@ describe('openWalletSession', () => {
|
|
|
308
372
|
passphrase: 'host hidden wallet',
|
|
309
373
|
on_device: false,
|
|
310
374
|
});
|
|
311
|
-
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
375
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
376
|
+
seed_domains: [],
|
|
377
|
+
});
|
|
312
378
|
});
|
|
313
379
|
|
|
314
380
|
test('refreshes Pro2 status and unlocks before selecting a hidden wallet when locked', async () => {
|
|
@@ -573,7 +639,9 @@ describe('openWalletSession', () => {
|
|
|
573
639
|
passphrase: 'host hidden wallet',
|
|
574
640
|
on_device: false,
|
|
575
641
|
});
|
|
576
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
642
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
643
|
+
seed_domains: [],
|
|
644
|
+
});
|
|
577
645
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
578
646
|
expect(deviceWalletSessionStore.get('device-1', 'new-hidden-state')).toBe('new-hidden-session');
|
|
579
647
|
});
|
|
@@ -644,6 +712,8 @@ describe('openWalletSession', () => {
|
|
|
644
712
|
});
|
|
645
713
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
646
714
|
session_id: 'known-session',
|
|
715
|
+
btc_test_address: 'hidden-state',
|
|
716
|
+
seed_domains: [],
|
|
647
717
|
});
|
|
648
718
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
649
719
|
});
|
|
@@ -821,12 +891,10 @@ describe('openWalletSession', () => {
|
|
|
821
891
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
822
892
|
eventless_wallet_session: true,
|
|
823
893
|
});
|
|
824
|
-
expect(device.unlockDevice).
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
deviceOnly: true,
|
|
894
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
895
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
896
|
+
seed_domains: [],
|
|
828
897
|
});
|
|
829
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
830
898
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
831
899
|
expect(device.passphraseState).toBeUndefined();
|
|
832
900
|
});
|
|
@@ -931,7 +999,9 @@ describe('openWalletSession', () => {
|
|
|
931
999
|
reason: 'open-wallet',
|
|
932
1000
|
deviceOnly: true,
|
|
933
1001
|
});
|
|
934
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1002
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1003
|
+
seed_domains: [],
|
|
1004
|
+
});
|
|
935
1005
|
});
|
|
936
1006
|
|
|
937
1007
|
test('selects a hidden wallet without exposing the internal device session', async () => {
|
|
@@ -965,7 +1035,9 @@ describe('openWalletSession', () => {
|
|
|
965
1035
|
passphrase: 'host hidden wallet',
|
|
966
1036
|
on_device: false,
|
|
967
1037
|
});
|
|
968
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1038
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1039
|
+
seed_domains: [],
|
|
1040
|
+
});
|
|
969
1041
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
970
1042
|
expect(device.passphraseState).toBeUndefined();
|
|
971
1043
|
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
|
|
@@ -994,7 +1066,9 @@ describe('openWalletSession', () => {
|
|
|
994
1066
|
passphrase: 'host hidden wallet',
|
|
995
1067
|
on_device: false,
|
|
996
1068
|
});
|
|
997
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1069
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1070
|
+
seed_domains: [],
|
|
1071
|
+
});
|
|
998
1072
|
});
|
|
999
1073
|
|
|
1000
1074
|
test('selects an on-device passphrase wallet with an explicit on_device request', async () => {
|
|
@@ -1019,7 +1093,9 @@ describe('openWalletSession', () => {
|
|
|
1019
1093
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
1020
1094
|
on_device: true,
|
|
1021
1095
|
});
|
|
1022
|
-
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
1096
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
1097
|
+
seed_domains: [],
|
|
1098
|
+
});
|
|
1023
1099
|
});
|
|
1024
1100
|
|
|
1025
1101
|
test('forwards a host passphrase to Pro2 firmware', async () => {
|
|
@@ -1061,7 +1137,9 @@ describe('openWalletSession', () => {
|
|
|
1061
1137
|
passphrase: 'host hidden wallet',
|
|
1062
1138
|
on_device: false,
|
|
1063
1139
|
});
|
|
1064
|
-
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
1140
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
1141
|
+
seed_domains: [],
|
|
1142
|
+
});
|
|
1065
1143
|
});
|
|
1066
1144
|
|
|
1067
1145
|
test('normalizes a Unicode Host passphrase before sending it to Pro2 firmware', async () => {
|
|
@@ -1141,7 +1219,9 @@ describe('openWalletSession', () => {
|
|
|
1141
1219
|
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin, {
|
|
1142
1220
|
emitUiEvent: false,
|
|
1143
1221
|
});
|
|
1144
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1222
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1223
|
+
seed_domains: [],
|
|
1224
|
+
});
|
|
1145
1225
|
});
|
|
1146
1226
|
|
|
1147
1227
|
test('uses the complete Attach PIN wire flow without a main PIN unlock', async () => {
|
|
@@ -1170,7 +1250,7 @@ describe('openWalletSession', () => {
|
|
|
1170
1250
|
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
1171
1251
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.AttachToPin }],
|
|
1172
1252
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1173
|
-
['DeviceSessionGet', 'DeviceSession', {}],
|
|
1253
|
+
['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }],
|
|
1174
1254
|
]);
|
|
1175
1255
|
expect(device.commands.typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
1176
1256
|
type: DeviceSessionPinType.Main,
|
|
@@ -1227,7 +1307,7 @@ describe('openWalletSession', () => {
|
|
|
1227
1307
|
|
|
1228
1308
|
await expect(method.run()).rejects.toThrow(message);
|
|
1229
1309
|
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
1230
|
-
expect(typedCall
|
|
1310
|
+
expect(typedCall.mock.calls.some(([name]) => name === 'DeviceSessionGet')).toBe(false);
|
|
1231
1311
|
}
|
|
1232
1312
|
);
|
|
1233
1313
|
|
|
@@ -1381,6 +1461,8 @@ describe('openWalletSession', () => {
|
|
|
1381
1461
|
});
|
|
1382
1462
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1383
1463
|
session_id: 'known-session',
|
|
1464
|
+
btc_test_address: 'hidden-state',
|
|
1465
|
+
seed_domains: [],
|
|
1384
1466
|
});
|
|
1385
1467
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1386
1468
|
});
|
|
@@ -1439,6 +1521,8 @@ describe('openWalletSession', () => {
|
|
|
1439
1521
|
expect(device.getDeviceState).toHaveBeenCalledTimes(2);
|
|
1440
1522
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1441
1523
|
session_id: 'known-session',
|
|
1524
|
+
btc_test_address: 'hidden-state',
|
|
1525
|
+
seed_domains: [],
|
|
1442
1526
|
});
|
|
1443
1527
|
const sessionGetCall = typedCall.mock.calls.findIndex(
|
|
1444
1528
|
([requestName]) => requestName === 'DeviceSessionGet'
|
|
@@ -1535,6 +1619,8 @@ describe('openWalletSession', () => {
|
|
|
1535
1619
|
});
|
|
1536
1620
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1537
1621
|
session_id: 'expired-session',
|
|
1622
|
+
btc_test_address: 'hidden-state',
|
|
1623
|
+
seed_domains: [],
|
|
1538
1624
|
});
|
|
1539
1625
|
expect(promptPassphrase).toHaveBeenCalledTimes(1);
|
|
1540
1626
|
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
|
|
@@ -1570,7 +1656,10 @@ describe('openWalletSession', () => {
|
|
|
1570
1656
|
passphraseState: 'hidden-state',
|
|
1571
1657
|
resumed: false,
|
|
1572
1658
|
});
|
|
1573
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1659
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1660
|
+
btc_test_address: 'hidden-state',
|
|
1661
|
+
seed_domains: [],
|
|
1662
|
+
});
|
|
1574
1663
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1575
1664
|
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
|
|
1576
1665
|
});
|
|
@@ -1604,6 +1693,11 @@ describe('openWalletSession', () => {
|
|
|
1604
1693
|
walletType: 'hidden',
|
|
1605
1694
|
passphraseState: 'hidden-state',
|
|
1606
1695
|
});
|
|
1607
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1696
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1697
|
+
seed_domains: [
|
|
1698
|
+
DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
1699
|
+
...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
|
|
1700
|
+
],
|
|
1701
|
+
});
|
|
1608
1702
|
});
|
|
1609
1703
|
});
|
|
@@ -9,6 +9,41 @@ jest.mock('../src/data/config', () => ({
|
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
11
|
describe('Protocol V2 UI interaction lifecycle', () => {
|
|
12
|
+
test('synthesizes finish metadata for a progress-only operation', () => {
|
|
13
|
+
const device = new Device({ id: 'connect-progress' } as any);
|
|
14
|
+
device.isProtocolV2 = jest.fn(() => true);
|
|
15
|
+
device.beginProtocolV2UiInteraction();
|
|
16
|
+
|
|
17
|
+
const metadata = device.finishProtocolV2UiInteraction('succeeded', {
|
|
18
|
+
ensureMetadata: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(metadata).toMatchObject({
|
|
22
|
+
phase: 'processing',
|
|
23
|
+
transition: 'finish',
|
|
24
|
+
outcome: 'succeeded',
|
|
25
|
+
protocol: 'V2',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('keeps finish metadata after the Protocol V2 transport is released', () => {
|
|
30
|
+
const device = new Device({ id: 'connect-released' } as any);
|
|
31
|
+
const isProtocolV2 = jest.fn().mockReturnValueOnce(true).mockReturnValue(false);
|
|
32
|
+
device.isProtocolV2 = isProtocolV2;
|
|
33
|
+
device.beginProtocolV2UiInteraction();
|
|
34
|
+
|
|
35
|
+
const metadata = device.finishProtocolV2UiInteraction('succeeded', {
|
|
36
|
+
ensureMetadata: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(metadata).toMatchObject({
|
|
40
|
+
phase: 'processing',
|
|
41
|
+
transition: 'finish',
|
|
42
|
+
outcome: 'succeeded',
|
|
43
|
+
protocol: 'V2',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
12
47
|
test('emits a matching PIN phase completion after DeviceSessionAskPin succeeds', async () => {
|
|
13
48
|
const device = new Device({ id: 'connect-1' } as any);
|
|
14
49
|
device.isProtocolV2 = jest.fn(() => true);
|
|
@@ -61,6 +61,116 @@ describe('Protocol V2 unlock semantics', () => {
|
|
|
61
61
|
expect(method.run).toHaveBeenCalledTimes(1);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test('lets the standard wallet session own the Main PIN unlock', async () => {
|
|
65
|
+
const calls: string[] = [];
|
|
66
|
+
const features = {
|
|
67
|
+
unlocked: false,
|
|
68
|
+
passphraseProtection: true,
|
|
69
|
+
};
|
|
70
|
+
const method = {
|
|
71
|
+
name: 'btcGetPublicKey',
|
|
72
|
+
unlockPolicy: 'none',
|
|
73
|
+
useDevicePassphraseState: true,
|
|
74
|
+
payload: { useEmptyPassphrase: true },
|
|
75
|
+
run: jest.fn().mockImplementation(() => {
|
|
76
|
+
calls.push('run');
|
|
77
|
+
return Promise.resolve({ message: 'ok' });
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
const device = {
|
|
81
|
+
features,
|
|
82
|
+
commands: {
|
|
83
|
+
typedCall: jest.fn().mockImplementation(() => {
|
|
84
|
+
calls.push('status');
|
|
85
|
+
return Promise.resolve({
|
|
86
|
+
message: {
|
|
87
|
+
unlocked: false,
|
|
88
|
+
passphraseProtection: true,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
isProtocolV2: () => true,
|
|
94
|
+
isBootloader: () => false,
|
|
95
|
+
isRomloader: () => false,
|
|
96
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
97
|
+
unlockDevice: jest.fn().mockImplementation(() => {
|
|
98
|
+
calls.push('wallet-session-unlock');
|
|
99
|
+
features.unlocked = true;
|
|
100
|
+
return Promise.resolve(features);
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await expect(
|
|
105
|
+
runMethodWithUnlockPolicy(method as any, device as any, {
|
|
106
|
+
prepare: async () => {
|
|
107
|
+
await device.unlockDevice();
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
).resolves.toEqual({ message: 'ok' });
|
|
111
|
+
|
|
112
|
+
expect(calls).toEqual(['status', 'wallet-session-unlock', 'run']);
|
|
113
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('reuses a Main PIN selected by pre-unlock when locked status hides passphrase state', async () => {
|
|
117
|
+
const calls: string[] = [];
|
|
118
|
+
const features: {
|
|
119
|
+
unlocked: boolean;
|
|
120
|
+
passphraseProtection?: boolean;
|
|
121
|
+
unlockedAttachPin?: boolean;
|
|
122
|
+
} = {
|
|
123
|
+
unlocked: false,
|
|
124
|
+
};
|
|
125
|
+
const method: any = {
|
|
126
|
+
name: 'btcGetPublicKey',
|
|
127
|
+
unlockPolicy: 'none',
|
|
128
|
+
useDevicePassphraseState: true,
|
|
129
|
+
payload: { useEmptyPassphrase: true },
|
|
130
|
+
run: jest.fn().mockImplementation(() => {
|
|
131
|
+
calls.push('run');
|
|
132
|
+
return Promise.resolve({ message: 'ok' });
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
const device = {
|
|
136
|
+
features,
|
|
137
|
+
commands: {
|
|
138
|
+
typedCall: jest.fn().mockImplementation(() => {
|
|
139
|
+
calls.push('status');
|
|
140
|
+
return Promise.resolve({
|
|
141
|
+
message: {
|
|
142
|
+
unlocked: false,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
isProtocolV2: () => true,
|
|
148
|
+
isBootloader: () => false,
|
|
149
|
+
isRomloader: () => false,
|
|
150
|
+
updateProtocolV2Status: jest.fn(() => features),
|
|
151
|
+
unlockDevice: jest.fn().mockImplementation(() => {
|
|
152
|
+
calls.push('main-pin');
|
|
153
|
+
features.unlocked = true;
|
|
154
|
+
features.passphraseProtection = true;
|
|
155
|
+
features.unlockedAttachPin = false;
|
|
156
|
+
return Promise.resolve(features);
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
runMethodWithUnlockPolicy(method, device as any, {
|
|
162
|
+
prepare: async () => {
|
|
163
|
+
if (!(features.unlocked && features.unlockedAttachPin === false)) {
|
|
164
|
+
await device.unlockDevice();
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
).resolves.toEqual({ message: 'ok' });
|
|
169
|
+
|
|
170
|
+
expect(calls).toEqual(['status', 'main-pin', 'run']);
|
|
171
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
64
174
|
test('runs fresh-status validation before starting unlock', async () => {
|
|
65
175
|
const identityError = new Error('Unexpected device');
|
|
66
176
|
const method = {
|