@onekeyfe/hd-core 1.2.0-alpha.42 → 1.2.0-alpha.44
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 +161 -21
- 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 +71 -13
- 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 +174 -45
- 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 +84 -24
- 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,104 @@ describe('openWalletSession', () => {
|
|
|
95
98
|
deviceWalletSessionStore.clear();
|
|
96
99
|
});
|
|
97
100
|
|
|
101
|
+
test('selects the standard wallet explicitly when passphrase is enabled', async () => {
|
|
102
|
+
const typedCall = jest.fn((request: string) => {
|
|
103
|
+
if (request === 'ProtocolInfoRequest') {
|
|
104
|
+
return { message: { version: 2 } };
|
|
105
|
+
}
|
|
106
|
+
if (request === 'DeviceSessionAskPassphrase') {
|
|
107
|
+
return { message: {} };
|
|
108
|
+
}
|
|
109
|
+
if (request === 'DeviceSessionGet') {
|
|
110
|
+
return {
|
|
111
|
+
message: {
|
|
112
|
+
btc_test_address: 'standard-state',
|
|
113
|
+
session_id: 'standard-session',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
118
|
+
});
|
|
119
|
+
const device = createDevice({ typedCall });
|
|
120
|
+
|
|
121
|
+
await expect(
|
|
122
|
+
getProtocolV2WalletSession(device as any, {
|
|
123
|
+
onlyMainPin: true,
|
|
124
|
+
})
|
|
125
|
+
).resolves.toMatchObject({
|
|
126
|
+
passphraseState: 'standard-state',
|
|
127
|
+
newSession: 'standard-session',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
131
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
132
|
+
passphrase: '',
|
|
133
|
+
on_device: false,
|
|
134
|
+
});
|
|
135
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
136
|
+
seed_domains: [],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('opens the standard wallet directly when passphrase is disabled', async () => {
|
|
141
|
+
const typedCall = jest.fn((request: string) => {
|
|
142
|
+
if (request === 'ProtocolInfoRequest') {
|
|
143
|
+
return { message: { version: 2 } };
|
|
144
|
+
}
|
|
145
|
+
if (request === 'DeviceSessionGet') {
|
|
146
|
+
return {
|
|
147
|
+
message: {
|
|
148
|
+
btc_test_address: 'standard-state',
|
|
149
|
+
session_id: 'standard-session',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
154
|
+
});
|
|
155
|
+
const device = createDevice({ passphraseProtection: false, typedCall });
|
|
156
|
+
|
|
157
|
+
await getProtocolV2WalletSession(device as any, { onlyMainPin: true });
|
|
158
|
+
|
|
159
|
+
expect(typedCall).not.toHaveBeenCalledWith(
|
|
160
|
+
'DeviceSessionAskPassphrase',
|
|
161
|
+
'Success',
|
|
162
|
+
expect.anything()
|
|
163
|
+
);
|
|
164
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
165
|
+
seed_domains: [],
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('does not treat an Attach PIN DeviceStatus as the Main wallet', async () => {
|
|
170
|
+
const typedCall = jest.fn((request: string) => {
|
|
171
|
+
if (request === 'ProtocolInfoRequest') {
|
|
172
|
+
return { message: { version: 2 } };
|
|
173
|
+
}
|
|
174
|
+
if (request === 'DeviceSessionAskPassphrase') {
|
|
175
|
+
return { message: {} };
|
|
176
|
+
}
|
|
177
|
+
if (request === 'DeviceSessionGet') {
|
|
178
|
+
return {
|
|
179
|
+
message: {
|
|
180
|
+
btc_test_address: 'standard-state',
|
|
181
|
+
session_id: 'standard-session',
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
186
|
+
});
|
|
187
|
+
const device = createDevice({ typedCall });
|
|
188
|
+
device.features.unlockedAttachPin = true;
|
|
189
|
+
|
|
190
|
+
await getProtocolV2WalletSession(device as any, { onlyMainPin: true });
|
|
191
|
+
|
|
192
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
|
|
193
|
+
source: 'wallet-session-coordinator',
|
|
194
|
+
reason: 'open-wallet',
|
|
195
|
+
deviceOnly: true,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
98
199
|
test('unlocks a locked Protocol V2 device before restoring a wallet session', async () => {
|
|
99
200
|
const device = {
|
|
100
201
|
commands: {
|
|
@@ -190,7 +291,9 @@ describe('openWalletSession', () => {
|
|
|
190
291
|
passphrase: 'host hidden wallet',
|
|
191
292
|
on_device: false,
|
|
192
293
|
});
|
|
193
|
-
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
294
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
295
|
+
seed_domains: [],
|
|
296
|
+
});
|
|
194
297
|
});
|
|
195
298
|
|
|
196
299
|
test('keeps Legacy Protocol V1 getPassphraseState parameterless', async () => {
|
|
@@ -308,7 +411,9 @@ describe('openWalletSession', () => {
|
|
|
308
411
|
passphrase: 'host hidden wallet',
|
|
309
412
|
on_device: false,
|
|
310
413
|
});
|
|
311
|
-
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
414
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
415
|
+
seed_domains: [],
|
|
416
|
+
});
|
|
312
417
|
});
|
|
313
418
|
|
|
314
419
|
test('refreshes Pro2 status and unlocks before selecting a hidden wallet when locked', async () => {
|
|
@@ -573,7 +678,9 @@ describe('openWalletSession', () => {
|
|
|
573
678
|
passphrase: 'host hidden wallet',
|
|
574
679
|
on_device: false,
|
|
575
680
|
});
|
|
576
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
681
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
682
|
+
seed_domains: [],
|
|
683
|
+
});
|
|
577
684
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
578
685
|
expect(deviceWalletSessionStore.get('device-1', 'new-hidden-state')).toBe('new-hidden-session');
|
|
579
686
|
});
|
|
@@ -644,6 +751,8 @@ describe('openWalletSession', () => {
|
|
|
644
751
|
});
|
|
645
752
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
646
753
|
session_id: 'known-session',
|
|
754
|
+
btc_test_address: 'hidden-state',
|
|
755
|
+
seed_domains: [],
|
|
647
756
|
});
|
|
648
757
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
649
758
|
});
|
|
@@ -795,6 +904,7 @@ describe('openWalletSession', () => {
|
|
|
795
904
|
const typedCall = jest
|
|
796
905
|
.fn()
|
|
797
906
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
907
|
+
.mockResolvedValueOnce({ message: {} })
|
|
798
908
|
.mockResolvedValueOnce({
|
|
799
909
|
message: {
|
|
800
910
|
btc_test_address: 'main-wallet-state',
|
|
@@ -817,16 +927,18 @@ describe('openWalletSession', () => {
|
|
|
817
927
|
passphraseState: null,
|
|
818
928
|
resumed: false,
|
|
819
929
|
});
|
|
820
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
930
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
821
931
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
822
932
|
eventless_wallet_session: true,
|
|
823
933
|
});
|
|
824
|
-
expect(device.unlockDevice).
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
934
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
935
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
936
|
+
passphrase: '',
|
|
937
|
+
on_device: false,
|
|
938
|
+
});
|
|
939
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
940
|
+
seed_domains: [],
|
|
828
941
|
});
|
|
829
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
830
942
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
831
943
|
expect(device.passphraseState).toBeUndefined();
|
|
832
944
|
});
|
|
@@ -835,6 +947,7 @@ describe('openWalletSession', () => {
|
|
|
835
947
|
const typedCall = jest
|
|
836
948
|
.fn()
|
|
837
949
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
950
|
+
.mockResolvedValueOnce({ message: {} })
|
|
838
951
|
.mockResolvedValueOnce({
|
|
839
952
|
message: {
|
|
840
953
|
btc_test_address: 'main-wallet-state',
|
|
@@ -886,6 +999,7 @@ describe('openWalletSession', () => {
|
|
|
886
999
|
const typedCall = jest
|
|
887
1000
|
.fn()
|
|
888
1001
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
1002
|
+
.mockResolvedValueOnce({ message: {} })
|
|
889
1003
|
.mockResolvedValueOnce({
|
|
890
1004
|
message: {
|
|
891
1005
|
btc_test_address: 'main-wallet-state',
|
|
@@ -931,7 +1045,9 @@ describe('openWalletSession', () => {
|
|
|
931
1045
|
reason: 'open-wallet',
|
|
932
1046
|
deviceOnly: true,
|
|
933
1047
|
});
|
|
934
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1048
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1049
|
+
seed_domains: [],
|
|
1050
|
+
});
|
|
935
1051
|
});
|
|
936
1052
|
|
|
937
1053
|
test('selects a hidden wallet without exposing the internal device session', async () => {
|
|
@@ -965,7 +1081,9 @@ describe('openWalletSession', () => {
|
|
|
965
1081
|
passphrase: 'host hidden wallet',
|
|
966
1082
|
on_device: false,
|
|
967
1083
|
});
|
|
968
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1084
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1085
|
+
seed_domains: [],
|
|
1086
|
+
});
|
|
969
1087
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
970
1088
|
expect(device.passphraseState).toBeUndefined();
|
|
971
1089
|
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
|
|
@@ -994,7 +1112,9 @@ describe('openWalletSession', () => {
|
|
|
994
1112
|
passphrase: 'host hidden wallet',
|
|
995
1113
|
on_device: false,
|
|
996
1114
|
});
|
|
997
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1115
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1116
|
+
seed_domains: [],
|
|
1117
|
+
});
|
|
998
1118
|
});
|
|
999
1119
|
|
|
1000
1120
|
test('selects an on-device passphrase wallet with an explicit on_device request', async () => {
|
|
@@ -1019,7 +1139,9 @@ describe('openWalletSession', () => {
|
|
|
1019
1139
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
1020
1140
|
on_device: true,
|
|
1021
1141
|
});
|
|
1022
|
-
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
1142
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
1143
|
+
seed_domains: [],
|
|
1144
|
+
});
|
|
1023
1145
|
});
|
|
1024
1146
|
|
|
1025
1147
|
test('forwards a host passphrase to Pro2 firmware', async () => {
|
|
@@ -1061,7 +1183,9 @@ describe('openWalletSession', () => {
|
|
|
1061
1183
|
passphrase: 'host hidden wallet',
|
|
1062
1184
|
on_device: false,
|
|
1063
1185
|
});
|
|
1064
|
-
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
1186
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
|
|
1187
|
+
seed_domains: [],
|
|
1188
|
+
});
|
|
1065
1189
|
});
|
|
1066
1190
|
|
|
1067
1191
|
test('normalizes a Unicode Host passphrase before sending it to Pro2 firmware', async () => {
|
|
@@ -1141,7 +1265,9 @@ describe('openWalletSession', () => {
|
|
|
1141
1265
|
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin, {
|
|
1142
1266
|
emitUiEvent: false,
|
|
1143
1267
|
});
|
|
1144
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1268
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1269
|
+
seed_domains: [],
|
|
1270
|
+
});
|
|
1145
1271
|
});
|
|
1146
1272
|
|
|
1147
1273
|
test('uses the complete Attach PIN wire flow without a main PIN unlock', async () => {
|
|
@@ -1170,7 +1296,7 @@ describe('openWalletSession', () => {
|
|
|
1170
1296
|
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
1171
1297
|
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.AttachToPin }],
|
|
1172
1298
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1173
|
-
['DeviceSessionGet', 'DeviceSession', {}],
|
|
1299
|
+
['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }],
|
|
1174
1300
|
]);
|
|
1175
1301
|
expect(device.commands.typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
1176
1302
|
type: DeviceSessionPinType.Main,
|
|
@@ -1227,7 +1353,7 @@ describe('openWalletSession', () => {
|
|
|
1227
1353
|
|
|
1228
1354
|
await expect(method.run()).rejects.toThrow(message);
|
|
1229
1355
|
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
1230
|
-
expect(typedCall
|
|
1356
|
+
expect(typedCall.mock.calls.some(([name]) => name === 'DeviceSessionGet')).toBe(false);
|
|
1231
1357
|
}
|
|
1232
1358
|
);
|
|
1233
1359
|
|
|
@@ -1381,6 +1507,8 @@ describe('openWalletSession', () => {
|
|
|
1381
1507
|
});
|
|
1382
1508
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1383
1509
|
session_id: 'known-session',
|
|
1510
|
+
btc_test_address: 'hidden-state',
|
|
1511
|
+
seed_domains: [],
|
|
1384
1512
|
});
|
|
1385
1513
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1386
1514
|
});
|
|
@@ -1439,6 +1567,8 @@ describe('openWalletSession', () => {
|
|
|
1439
1567
|
expect(device.getDeviceState).toHaveBeenCalledTimes(2);
|
|
1440
1568
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1441
1569
|
session_id: 'known-session',
|
|
1570
|
+
btc_test_address: 'hidden-state',
|
|
1571
|
+
seed_domains: [],
|
|
1442
1572
|
});
|
|
1443
1573
|
const sessionGetCall = typedCall.mock.calls.findIndex(
|
|
1444
1574
|
([requestName]) => requestName === 'DeviceSessionGet'
|
|
@@ -1535,6 +1665,8 @@ describe('openWalletSession', () => {
|
|
|
1535
1665
|
});
|
|
1536
1666
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1537
1667
|
session_id: 'expired-session',
|
|
1668
|
+
btc_test_address: 'hidden-state',
|
|
1669
|
+
seed_domains: [],
|
|
1538
1670
|
});
|
|
1539
1671
|
expect(promptPassphrase).toHaveBeenCalledTimes(1);
|
|
1540
1672
|
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
|
|
@@ -1570,7 +1702,10 @@ describe('openWalletSession', () => {
|
|
|
1570
1702
|
passphraseState: 'hidden-state',
|
|
1571
1703
|
resumed: false,
|
|
1572
1704
|
});
|
|
1573
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1705
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1706
|
+
btc_test_address: 'hidden-state',
|
|
1707
|
+
seed_domains: [],
|
|
1708
|
+
});
|
|
1574
1709
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1575
1710
|
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
|
|
1576
1711
|
});
|
|
@@ -1604,6 +1739,11 @@ describe('openWalletSession', () => {
|
|
|
1604
1739
|
walletType: 'hidden',
|
|
1605
1740
|
passphraseState: 'hidden-state',
|
|
1606
1741
|
});
|
|
1607
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1742
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1743
|
+
seed_domains: [
|
|
1744
|
+
DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
1745
|
+
...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
|
|
1746
|
+
],
|
|
1747
|
+
});
|
|
1608
1748
|
});
|
|
1609
1749
|
});
|
|
@@ -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 = {
|