@onekeyfe/hd-core 1.2.2-alpha.0 → 1.2.2-alpha.10
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 +2 -2
- package/__tests__/device-lifecycle-events.test.ts +67 -0
- package/__tests__/deviceUploadNft.test.ts +27 -0
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +322 -21
- package/__tests__/open-wallet-session.test.ts +613 -80
- package/__tests__/pro2HostAssetPackage.test.ts +108 -1
- package/__tests__/protocol-v2.test.ts +231 -49
- package/__tests__/protocolV2FileWrite.test.ts +40 -0
- package/dist/api/FirmwareUpdateV4.d.ts +2 -0
- package/dist/api/FirmwareUpdateV4.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 +1 -0
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/core/RequestQueue.d.ts +2 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +1454 -1144
- package/dist/protocols/protocol-v2/walletSession.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/pro2HostAssetPackage.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +73 -23
- package/src/api/OpenWalletSession.ts +0 -3
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
- package/src/api/helpers/protocolV2FileWrite.ts +11 -5
- package/src/api/protocol-v2/DeviceUploadNft.ts +4 -1
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +17 -3
- package/src/core/RequestQueue.ts +16 -1
- package/src/core/index.ts +48 -21
- package/src/data/messages/messages-protocol-v2.json +1471 -1367
- package/src/protocols/protocol-v2/walletSession.ts +157 -38
- package/src/utils/pro2HostAssetPackage.ts +143 -58
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-bitwise -- LZ4 decoding and deterministic test data generation require bitwise operations. */
|
|
1
2
|
import { sha3_512 } from '@noble/hashes/sha3';
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -5,6 +6,82 @@ import {
|
|
|
5
6
|
supportsPro2HostAssetPackage,
|
|
6
7
|
} from '../src/utils/pro2HostAssetPackage';
|
|
7
8
|
|
|
9
|
+
const decodeRawLz4Block = (compressed: Uint8Array, expectedLength: number) => {
|
|
10
|
+
const output = new Uint8Array(expectedLength);
|
|
11
|
+
let inputOffset = 0;
|
|
12
|
+
let outputOffset = 0;
|
|
13
|
+
|
|
14
|
+
const readLength = (initialLength: number) => {
|
|
15
|
+
let length = initialLength;
|
|
16
|
+
if (length === 15) {
|
|
17
|
+
let extension = 255;
|
|
18
|
+
while (extension === 255) {
|
|
19
|
+
extension = compressed[inputOffset];
|
|
20
|
+
inputOffset += 1;
|
|
21
|
+
length += extension;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return length;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
while (inputOffset < compressed.byteLength) {
|
|
28
|
+
const token = compressed[inputOffset];
|
|
29
|
+
inputOffset += 1;
|
|
30
|
+
const literalLength = readLength(token >>> 4);
|
|
31
|
+
output.set(compressed.subarray(inputOffset, inputOffset + literalLength), outputOffset);
|
|
32
|
+
inputOffset += literalLength;
|
|
33
|
+
outputOffset += literalLength;
|
|
34
|
+
if (inputOffset >= compressed.byteLength) break;
|
|
35
|
+
|
|
36
|
+
const matchOffset = compressed[inputOffset] | (compressed[inputOffset + 1] << 8);
|
|
37
|
+
inputOffset += 2;
|
|
38
|
+
const matchLength = readLength(token & 0x0f) + 4;
|
|
39
|
+
for (let index = 0; index < matchLength; index += 1) {
|
|
40
|
+
output[outputOffset] = output[outputOffset - matchOffset];
|
|
41
|
+
outputOffset += 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
expect(outputOffset).toBe(expectedLength);
|
|
46
|
+
return output;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const decodeFirstPackageEntry = (packageData: Uint8Array, rawLength: number) => {
|
|
50
|
+
const containerHeaderSize = 0x5f90;
|
|
51
|
+
const archive = packageData.subarray(containerHeaderSize);
|
|
52
|
+
const archiveView = new DataView(archive.buffer, archive.byteOffset, archive.byteLength);
|
|
53
|
+
const compressedOffset = archiveView.getUint32(42 + 0x100, true);
|
|
54
|
+
const compressed = archive.subarray(compressedOffset);
|
|
55
|
+
const compressedView = new DataView(
|
|
56
|
+
compressed.buffer,
|
|
57
|
+
compressed.byteOffset,
|
|
58
|
+
compressed.byteLength
|
|
59
|
+
);
|
|
60
|
+
const blockCount = compressedView.getUint16(0, true);
|
|
61
|
+
const blockSize = 1 << compressedView.getUint16(2, true);
|
|
62
|
+
let blockOffset = 8 + blockCount * 4;
|
|
63
|
+
const decodedBlocks: Uint8Array[] = [];
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < blockCount; index += 1) {
|
|
66
|
+
const compressedLength = compressedView.getUint32(8 + index * 4, true);
|
|
67
|
+
const expectedLength = Math.min(blockSize, rawLength - index * blockSize);
|
|
68
|
+
decodedBlocks.push(
|
|
69
|
+
decodeRawLz4Block(
|
|
70
|
+
compressed.subarray(blockOffset, blockOffset + compressedLength),
|
|
71
|
+
expectedLength
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
blockOffset += compressedLength;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const decoded = new Uint8Array(rawLength);
|
|
78
|
+
decodedBlocks.reduce((offset, block) => {
|
|
79
|
+
decoded.set(block, offset);
|
|
80
|
+
return offset + block.byteLength;
|
|
81
|
+
}, 0);
|
|
82
|
+
return decoded;
|
|
83
|
+
};
|
|
84
|
+
|
|
8
85
|
describe('Pro2 host asset package', () => {
|
|
9
86
|
test('builds the unsigned RESOURCE container and LZ4-blocked archive expected by firmware', () => {
|
|
10
87
|
const raw = new TextEncoder().encode('123456789');
|
|
@@ -40,12 +117,42 @@ describe('Pro2 host asset package', () => {
|
|
|
40
117
|
|
|
41
118
|
const compressedOffset = archiveView.getUint32(42 + 0x100, true);
|
|
42
119
|
expect(archiveView.getUint16(compressedOffset, true)).toBe(1);
|
|
43
|
-
expect(archiveView.getUint16(compressedOffset + 2, true)).toBe(
|
|
120
|
+
expect(archiveView.getUint16(compressedOffset + 2, true)).toBe(14);
|
|
44
121
|
expect(archiveView.getUint32(compressedOffset + 4, true)).toBe(0);
|
|
45
122
|
expect(archiveView.getUint32(compressedOffset + 8, true)).toBe(10);
|
|
46
123
|
expect(payload.subarray(compressedOffset + 12)).toEqual(new Uint8Array([0x90, ...raw]));
|
|
47
124
|
});
|
|
48
125
|
|
|
126
|
+
test('round-trips multi-block data byte-for-byte with best-match compression', () => {
|
|
127
|
+
const raw = Uint8Array.from({ length: 16_384 * 3 + 137 }, (_, index) => {
|
|
128
|
+
const column = index % 604;
|
|
129
|
+
const row = Math.floor(index / 604);
|
|
130
|
+
return (column * 31 + row * 17) & 0xff;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const packageData = buildPro2HostAssetPackage([{ name: 'wallpaper.bin', data: raw }]);
|
|
134
|
+
|
|
135
|
+
expect(decodeFirstPackageEntry(packageData, raw.byteLength)).toEqual(raw);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('falls back to 8 KiB blocks when a compressed 16 KiB block exceeds firmware capacity', () => {
|
|
139
|
+
let state = 0x12345678;
|
|
140
|
+
const raw = Uint8Array.from({ length: 16_384 }, () => {
|
|
141
|
+
state ^= state << 13;
|
|
142
|
+
state ^= state >>> 17;
|
|
143
|
+
state ^= state << 5;
|
|
144
|
+
return state & 0xff;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const packageData = buildPro2HostAssetPackage([{ name: 'wallpaper.bin', data: raw }]);
|
|
148
|
+
const archive = packageData.subarray(0x5f90);
|
|
149
|
+
const archiveView = new DataView(archive.buffer, archive.byteOffset, archive.byteLength);
|
|
150
|
+
const compressedOffset = archiveView.getUint32(42 + 0x100, true);
|
|
151
|
+
|
|
152
|
+
expect(archiveView.getUint16(compressedOffset + 2, true)).toBe(13);
|
|
153
|
+
expect(decodeFirstPackageEntry(packageData, raw.byteLength)).toEqual(raw);
|
|
154
|
+
});
|
|
155
|
+
|
|
49
156
|
test.each([
|
|
50
157
|
['1.0.0', false],
|
|
51
158
|
['1.0.1-beta.1', false],
|
|
@@ -7,6 +7,7 @@ import { encode as encodeJpeg } from 'jpeg-js';
|
|
|
7
7
|
import {
|
|
8
8
|
DeviceRebootType,
|
|
9
9
|
DeviceSessionPinType,
|
|
10
|
+
DeviceSessionSeedDomain,
|
|
10
11
|
DeviceSettingsPage,
|
|
11
12
|
DeviceType,
|
|
12
13
|
} from '@onekeyfe/hd-transport';
|
|
@@ -255,6 +256,9 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
255
256
|
});
|
|
256
257
|
|
|
257
258
|
test('uploads and applies the fixed wallpaper package on firmware 1.0.1', async () => {
|
|
259
|
+
const getSettingsSpy = jest
|
|
260
|
+
.spyOn(DataManager, 'getSettings')
|
|
261
|
+
.mockReturnValue('react-native' as any);
|
|
258
262
|
const typedCall = jest.fn().mockImplementation((request, _response, params) => {
|
|
259
263
|
if (request === 'FilesystemDirMake') return { message: {} };
|
|
260
264
|
if (request === 'FilesystemFileWrite') {
|
|
@@ -271,17 +275,24 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
271
275
|
payload: {
|
|
272
276
|
method: 'deviceUploadWallpaper',
|
|
273
277
|
jpegBase64: createJpegBase64(604, 1024),
|
|
278
|
+
fileName: 'ignored-on-package-firmware.bin',
|
|
274
279
|
},
|
|
275
280
|
});
|
|
276
281
|
const device = stubWallpaperDevice({
|
|
277
282
|
commands: { typedCall },
|
|
283
|
+
state: { versions: { firmware: '1.0.1' } },
|
|
278
284
|
getCurrentFirmwareVersionString: jest.fn(() => '1.0.1'),
|
|
279
285
|
});
|
|
280
286
|
(method as any).device = device;
|
|
281
287
|
method.postMessage = jest.fn();
|
|
282
288
|
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
let result;
|
|
290
|
+
try {
|
|
291
|
+
method.init();
|
|
292
|
+
result = await method.run();
|
|
293
|
+
} finally {
|
|
294
|
+
getSettingsSpy.mockRestore();
|
|
295
|
+
}
|
|
285
296
|
|
|
286
297
|
const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
|
|
287
298
|
expect(new Set(fileWrites.map(call => call[2].file.path))).toEqual(
|
|
@@ -290,6 +301,7 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
290
301
|
expect(fileWrites[0][2].file.data.subarray(0, 4)).toEqual(
|
|
291
302
|
new Uint8Array([0x4f, 0x4b, 0x50, 0x50])
|
|
292
303
|
);
|
|
304
|
+
expect(fileWrites[0][2].file.data).toHaveLength(1960);
|
|
293
305
|
expect(typedCall).toHaveBeenLastCalledWith('DeviceSettingsSet', 'Success', {
|
|
294
306
|
settings: { wallpaper_path: 'vol1:/wallpapers/wallpaper.okpkg' },
|
|
295
307
|
});
|
|
@@ -300,6 +312,40 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
300
312
|
});
|
|
301
313
|
});
|
|
302
314
|
|
|
315
|
+
test('keeps the legacy wallpaper path on a 1.0.1 prerelease', async () => {
|
|
316
|
+
const typedCall = jest.fn().mockImplementation((request, _response, params) => {
|
|
317
|
+
if (request === 'FilesystemDirMake') return { message: {} };
|
|
318
|
+
if (request === 'FilesystemFileWrite') {
|
|
319
|
+
const file = params.file as { data: Uint8Array; offset: number };
|
|
320
|
+
return { message: { processed_byte: file.offset + file.data.byteLength } };
|
|
321
|
+
}
|
|
322
|
+
if (request === 'DeviceSettingsSet') return { message: {} };
|
|
323
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
324
|
+
});
|
|
325
|
+
const method = new DeviceUploadWallpaper({
|
|
326
|
+
id: 1,
|
|
327
|
+
payload: {
|
|
328
|
+
method: 'deviceUploadWallpaper',
|
|
329
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
330
|
+
fileName: 'prerelease-wallpaper.bin',
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
const device = stubWallpaperDevice({
|
|
334
|
+
commands: { typedCall },
|
|
335
|
+
state: { versions: { firmware: '1.0.1-beta.1' } },
|
|
336
|
+
});
|
|
337
|
+
(method as any).device = device;
|
|
338
|
+
|
|
339
|
+
method.init();
|
|
340
|
+
const result = await method.run();
|
|
341
|
+
|
|
342
|
+
expect(result.path).toBe('vol1:/wallpapers/prerelease-wallpaper.bin');
|
|
343
|
+
const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
|
|
344
|
+
expect(new Set(fileWrites.map(call => call[2].file.path))).toEqual(
|
|
345
|
+
new Set(['vol1:/wallpapers/prerelease-wallpaper.bin'])
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
303
349
|
test('文件上传失败时不修改 wallpaper_path', async () => {
|
|
304
350
|
const typedCall = jest.fn().mockImplementation(request => {
|
|
305
351
|
if (request === 'FilesystemDirMake') return { message: {} };
|
|
@@ -1079,10 +1125,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1079
1125
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
1080
1126
|
passphrase: '',
|
|
1081
1127
|
on_device: false,
|
|
1128
|
+
seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
|
|
1082
1129
|
});
|
|
1083
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1084
|
-
seed_domains: [],
|
|
1085
|
-
});
|
|
1130
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
1086
1131
|
device.passphraseState = 'state-1';
|
|
1087
1132
|
expect(device.getInternalState()).toBe('session-1');
|
|
1088
1133
|
});
|
|
@@ -1135,13 +1180,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1135
1180
|
expect(
|
|
1136
1181
|
typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
|
|
1137
1182
|
).toHaveLength(1);
|
|
1138
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1139
|
-
seed_domains: [],
|
|
1140
|
-
});
|
|
1183
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
1141
1184
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1142
1185
|
session_id: 'standard-session',
|
|
1143
1186
|
btc_test_address: 'standard-state',
|
|
1144
|
-
seed_domains: [],
|
|
1145
1187
|
});
|
|
1146
1188
|
expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
|
|
1147
1189
|
passphraseState: 'standard-state',
|
|
@@ -1506,10 +1548,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1506
1548
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
1507
1549
|
passphrase: 'host hidden wallet',
|
|
1508
1550
|
on_device: false,
|
|
1551
|
+
seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
|
|
1509
1552
|
});
|
|
1510
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1511
|
-
seed_domains: [],
|
|
1512
|
-
});
|
|
1553
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
1513
1554
|
});
|
|
1514
1555
|
|
|
1515
1556
|
test('deviceStatusGet returns raw DeviceStatus and updates dynamic features', async () => {
|
|
@@ -1618,7 +1659,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1618
1659
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1619
1660
|
session_id: 'session-a',
|
|
1620
1661
|
btc_test_address: 'state-a',
|
|
1621
|
-
seed_domains: [],
|
|
1622
1662
|
});
|
|
1623
1663
|
expect(device.getInternalState()).toBe('session-b');
|
|
1624
1664
|
});
|
|
@@ -1692,7 +1732,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1692
1732
|
{
|
|
1693
1733
|
session_id: 'session-a',
|
|
1694
1734
|
btc_test_address: 'state-a',
|
|
1695
|
-
seed_domains: [],
|
|
1696
1735
|
},
|
|
1697
1736
|
],
|
|
1698
1737
|
]);
|
|
@@ -1739,10 +1778,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1739
1778
|
[
|
|
1740
1779
|
'DeviceSessionAskPassphrase',
|
|
1741
1780
|
'Success',
|
|
1742
|
-
{
|
|
1781
|
+
{
|
|
1782
|
+
passphrase: 'host hidden wallet',
|
|
1783
|
+
on_device: false,
|
|
1784
|
+
seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
|
|
1785
|
+
},
|
|
1743
1786
|
],
|
|
1744
1787
|
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1745
|
-
['DeviceSessionGet', 'DeviceSession', {
|
|
1788
|
+
['DeviceSessionGet', 'DeviceSession', {}],
|
|
1746
1789
|
]);
|
|
1747
1790
|
});
|
|
1748
1791
|
|
|
@@ -1904,7 +1947,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1904
1947
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
1905
1948
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
1906
1949
|
btc_test_address: 'expected-state',
|
|
1907
|
-
seed_domains: [],
|
|
1908
1950
|
});
|
|
1909
1951
|
expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
|
|
1910
1952
|
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
|
|
@@ -2202,7 +2244,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2202
2244
|
firmwareVersion: '4.15.0',
|
|
2203
2245
|
passphraseProtection: true,
|
|
2204
2246
|
sessionId: 'feature-session',
|
|
2205
|
-
unlockedAttachPin:
|
|
2247
|
+
unlockedAttachPin: false,
|
|
2206
2248
|
};
|
|
2207
2249
|
const typedCall = jest
|
|
2208
2250
|
.fn()
|
|
@@ -2295,7 +2337,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2295
2337
|
).resolves.toMatchObject({ passphraseState: 'expected-state' });
|
|
2296
2338
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2297
2339
|
btc_test_address: 'expected-state',
|
|
2298
|
-
seed_domains: [],
|
|
2299
2340
|
});
|
|
2300
2341
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
2301
2342
|
});
|
|
@@ -2369,7 +2410,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2369
2410
|
{
|
|
2370
2411
|
session_id: 'session-pro2-app',
|
|
2371
2412
|
btc_test_address: 'state-pro2-app',
|
|
2372
|
-
seed_domains: [],
|
|
2373
2413
|
},
|
|
2374
2414
|
],
|
|
2375
2415
|
]);
|
|
@@ -2559,10 +2599,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2559
2599
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
2560
2600
|
passphrase: 'host hidden wallet',
|
|
2561
2601
|
on_device: false,
|
|
2602
|
+
seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
|
|
2562
2603
|
});
|
|
2563
|
-
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2564
|
-
seed_domains: [],
|
|
2565
|
-
});
|
|
2604
|
+
expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2566
2605
|
});
|
|
2567
2606
|
|
|
2568
2607
|
test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
|
|
@@ -2642,16 +2681,14 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2642
2681
|
expect(device.getInternalState()).toBeUndefined();
|
|
2643
2682
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2644
2683
|
btc_test_address: 'expected-state',
|
|
2645
|
-
seed_domains: [],
|
|
2646
2684
|
});
|
|
2647
2685
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
2648
2686
|
passphrase: 'host hidden wallet',
|
|
2649
2687
|
on_device: false,
|
|
2688
|
+
seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
|
|
2650
2689
|
});
|
|
2651
2690
|
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2652
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2653
|
-
seed_domains: [],
|
|
2654
|
-
});
|
|
2691
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2655
2692
|
});
|
|
2656
2693
|
|
|
2657
2694
|
test('fails closed instead of switching to Main PIN during a standard-wallet safety check', async () => {
|
|
@@ -2749,20 +2786,19 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2749
2786
|
await expect(
|
|
2750
2787
|
device.checkPassphraseStateSafety('stale-hidden-state', true, false)
|
|
2751
2788
|
).resolves.toBe(true);
|
|
2752
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
2789
|
+
expect(typedCall).toHaveBeenCalledTimes(6);
|
|
2753
2790
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2754
2791
|
eventless_wallet_session: true,
|
|
2755
2792
|
});
|
|
2756
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
2757
|
-
seed_domains: [],
|
|
2758
|
-
});
|
|
2793
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
2759
2794
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
|
|
2760
2795
|
passphrase: '',
|
|
2761
2796
|
on_device: false,
|
|
2797
|
+
seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
|
|
2762
2798
|
});
|
|
2763
2799
|
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
2764
2800
|
expect(typedCall.mock.calls.filter(([request]) => request === 'DeviceStatusGet')).toHaveLength(
|
|
2765
|
-
|
|
2801
|
+
3
|
|
2766
2802
|
);
|
|
2767
2803
|
expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', expect.anything());
|
|
2768
2804
|
});
|
|
@@ -5627,7 +5663,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5627
5663
|
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
5628
5664
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5629
5665
|
await cancelableAction?.();
|
|
5630
|
-
expect(cancelDevice).
|
|
5666
|
+
expect(cancelDevice).not.toHaveBeenCalled();
|
|
5631
5667
|
});
|
|
5632
5668
|
|
|
5633
5669
|
test('does not send the install request when Protocol V2 staging fails', async () => {
|
|
@@ -5732,6 +5768,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5732
5768
|
);
|
|
5733
5769
|
expect(typedCall).not.toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
|
|
5734
5770
|
expect((method as any).protocolV2InstallNeedsReconnect).toBe(true);
|
|
5771
|
+
expect((method as any).protocolV2InstallDisconnectObserved).toBe(true);
|
|
5735
5772
|
});
|
|
5736
5773
|
|
|
5737
5774
|
test.each([
|
|
@@ -7147,7 +7184,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7147
7184
|
true
|
|
7148
7185
|
);
|
|
7149
7186
|
expect((method as any).exitProtocolV2BootloaderToNormal).not.toHaveBeenCalled();
|
|
7150
|
-
expect(method.postProgressMessage).toHaveBeenCalledWith(
|
|
7187
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(
|
|
7188
|
+
100,
|
|
7189
|
+
'transferData',
|
|
7190
|
+
expect.objectContaining({
|
|
7191
|
+
transferredBytes: 5,
|
|
7192
|
+
totalBytes: 5,
|
|
7193
|
+
})
|
|
7194
|
+
);
|
|
7151
7195
|
expect((method as any).completeProtocolV2FinalVerification).toHaveBeenCalledTimes(1);
|
|
7152
7196
|
});
|
|
7153
7197
|
|
|
@@ -7221,7 +7265,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7221
7265
|
expect.objectContaining({ processedSize: 2, totalSize: 3 })
|
|
7222
7266
|
);
|
|
7223
7267
|
expect(method.postProgressMessage).toHaveBeenCalledTimes(1);
|
|
7224
|
-
expect(method.postProgressMessage).toHaveBeenCalledWith(
|
|
7268
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(
|
|
7269
|
+
100,
|
|
7270
|
+
'transferData',
|
|
7271
|
+
expect.objectContaining({
|
|
7272
|
+
transferredBytes: 3,
|
|
7273
|
+
totalBytes: 3,
|
|
7274
|
+
})
|
|
7275
|
+
);
|
|
7225
7276
|
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
|
|
7226
7277
|
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
|
|
7227
7278
|
targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
@@ -8541,26 +8592,43 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
8541
8592
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
8542
8593
|
(method as any).protocolV2StartFirmwareUpdate = jest.fn();
|
|
8543
8594
|
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
|
|
8595
|
+
const dateNowSpy = jest
|
|
8596
|
+
.spyOn(Date, 'now')
|
|
8597
|
+
.mockReturnValueOnce(1_000)
|
|
8598
|
+
.mockReturnValueOnce(5_000);
|
|
8544
8599
|
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8600
|
+
try {
|
|
8601
|
+
await (method as any).executeProtocolV2SourceUpdate({
|
|
8602
|
+
installSources: [],
|
|
8603
|
+
resourceSources: [
|
|
8604
|
+
{
|
|
8605
|
+
name: 'images.okpkg',
|
|
8606
|
+
source: {
|
|
8607
|
+
size: 3,
|
|
8608
|
+
readAt: jest.fn(),
|
|
8609
|
+
close: jest.fn(),
|
|
8610
|
+
},
|
|
8611
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
8554
8612
|
},
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8613
|
+
],
|
|
8614
|
+
});
|
|
8615
|
+
} finally {
|
|
8616
|
+
dateNowSpy.mockRestore();
|
|
8617
|
+
}
|
|
8559
8618
|
|
|
8560
8619
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
|
|
8561
8620
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
8562
|
-
expect.objectContaining({
|
|
8621
|
+
expect.objectContaining({
|
|
8622
|
+
filePath: 'vol0:/bundles/images/images.okpkg',
|
|
8623
|
+
transferStartedAt: 1_000,
|
|
8624
|
+
})
|
|
8563
8625
|
);
|
|
8626
|
+
expect(method.postProgressMessage).toHaveBeenLastCalledWith(100, 'transferData', {
|
|
8627
|
+
transferredBytes: 3,
|
|
8628
|
+
totalBytes: 3,
|
|
8629
|
+
rateBytesPerSecond: 1,
|
|
8630
|
+
elapsedMs: 4_000,
|
|
8631
|
+
});
|
|
8564
8632
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(
|
|
8565
8633
|
'vol0:/bundles/images/images.okpkg',
|
|
8566
8634
|
3
|
|
@@ -8790,6 +8858,84 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
8790
8858
|
expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
|
|
8791
8859
|
});
|
|
8792
8860
|
|
|
8861
|
+
test('does not recover or wrap a cancelled V4 file transfer', async () => {
|
|
8862
|
+
const method = new FirmwareUpdateV4({
|
|
8863
|
+
id: 1,
|
|
8864
|
+
payload: {
|
|
8865
|
+
method: 'firmwareUpdateV4',
|
|
8866
|
+
},
|
|
8867
|
+
});
|
|
8868
|
+
const abortController = new AbortController();
|
|
8869
|
+
method.abortSignal = abortController.signal;
|
|
8870
|
+
(method as any).fileWriteChunk = jest.fn().mockImplementation(() => {
|
|
8871
|
+
abortController.abort();
|
|
8872
|
+
return Promise.reject(new Error('transport disposed'));
|
|
8873
|
+
});
|
|
8874
|
+
const recoverProtocolV2FileTransfer = jest.fn();
|
|
8875
|
+
(method as any).recoverProtocolV2FileTransfer = recoverProtocolV2FileTransfer;
|
|
8876
|
+
const source = await openFirmwareByteSource({
|
|
8877
|
+
binary: new Uint8Array([1]).buffer,
|
|
8878
|
+
});
|
|
8879
|
+
|
|
8880
|
+
try {
|
|
8881
|
+
await expect(
|
|
8882
|
+
(method as any).protocolV2SourceUpdateProcess({
|
|
8883
|
+
source,
|
|
8884
|
+
filePath: 'vol1:firmware.bin',
|
|
8885
|
+
processedSize: 0,
|
|
8886
|
+
totalSize: 1,
|
|
8887
|
+
})
|
|
8888
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.CallQueueActionCancelled });
|
|
8889
|
+
} finally {
|
|
8890
|
+
await source?.close();
|
|
8891
|
+
}
|
|
8892
|
+
expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
|
|
8893
|
+
});
|
|
8894
|
+
|
|
8895
|
+
test('keeps public transfer bytes monotonic when a V4 file retry restarts at zero', async () => {
|
|
8896
|
+
const method = new FirmwareUpdateV4({
|
|
8897
|
+
id: 1,
|
|
8898
|
+
payload: {
|
|
8899
|
+
method: 'firmwareUpdateV4',
|
|
8900
|
+
},
|
|
8901
|
+
});
|
|
8902
|
+
let writeCount = 0;
|
|
8903
|
+
(method as any).fileWriteChunk = jest.fn(
|
|
8904
|
+
(_path: string, _size: number, offset: number, data: Uint8Array) => {
|
|
8905
|
+
writeCount += 1;
|
|
8906
|
+
if (writeCount === 3) {
|
|
8907
|
+
return Promise.reject(new Error('transport timeout'));
|
|
8908
|
+
}
|
|
8909
|
+
return Promise.resolve({
|
|
8910
|
+
message: { processed_byte: offset + data.byteLength },
|
|
8911
|
+
});
|
|
8912
|
+
}
|
|
8913
|
+
);
|
|
8914
|
+
(method as any).getProtocolV2FirmwareChunkSize = jest.fn().mockReturnValue(1000);
|
|
8915
|
+
(method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
|
|
8916
|
+
method.postProgressMessage = jest.fn();
|
|
8917
|
+
const source = await openFirmwareByteSource({
|
|
8918
|
+
binary: new Uint8Array(3000).buffer,
|
|
8919
|
+
});
|
|
8920
|
+
|
|
8921
|
+
try {
|
|
8922
|
+
await (method as any).protocolV2SourceUpdateProcess({
|
|
8923
|
+
source,
|
|
8924
|
+
filePath: 'vol1:firmware.bin',
|
|
8925
|
+
processedSize: 0,
|
|
8926
|
+
totalSize: 3000,
|
|
8927
|
+
});
|
|
8928
|
+
} finally {
|
|
8929
|
+
await source?.close();
|
|
8930
|
+
}
|
|
8931
|
+
|
|
8932
|
+
const transferredBytes = (method.postProgressMessage as jest.Mock).mock.calls.map(
|
|
8933
|
+
([, , metrics]) => metrics.transferredBytes
|
|
8934
|
+
);
|
|
8935
|
+
expect(transferredBytes).toEqual([1000, 2000, 3000]);
|
|
8936
|
+
expect((method as any).recoverProtocolV2FileTransfer).toHaveBeenCalledTimes(1);
|
|
8937
|
+
});
|
|
8938
|
+
|
|
8793
8939
|
test('throttles repeated transfer progress while preserving file completion', async () => {
|
|
8794
8940
|
const method = new FirmwareUpdateV4({
|
|
8795
8941
|
id: 1,
|
|
@@ -9148,6 +9294,42 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
9148
9294
|
expect(typedCall).toHaveBeenCalledTimes(6);
|
|
9149
9295
|
});
|
|
9150
9296
|
|
|
9297
|
+
test('preserves the underlying transport error code after firmware transfer retries', async () => {
|
|
9298
|
+
const method = new FirmwareUpdateV4({
|
|
9299
|
+
id: 1,
|
|
9300
|
+
payload: {
|
|
9301
|
+
method: 'firmwareUpdateV4',
|
|
9302
|
+
},
|
|
9303
|
+
});
|
|
9304
|
+
const typedCall = jest
|
|
9305
|
+
.fn()
|
|
9306
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError, 'response timeout'));
|
|
9307
|
+
|
|
9308
|
+
(method as any).device = stubDevice({
|
|
9309
|
+
getCommands: () => ({ typedCall }),
|
|
9310
|
+
});
|
|
9311
|
+
method.postProgressMessage = jest.fn();
|
|
9312
|
+
method.postTipMessage = jest.fn();
|
|
9313
|
+
(method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
|
|
9314
|
+
|
|
9315
|
+
const source = await openFirmwareByteSource({
|
|
9316
|
+
binary: new Uint8Array([1, 2, 3]).buffer,
|
|
9317
|
+
});
|
|
9318
|
+
await expect(
|
|
9319
|
+
(method as any).protocolV2SourceUpdateProcess({
|
|
9320
|
+
source,
|
|
9321
|
+
filePath: 'vol0:/firmware.bin',
|
|
9322
|
+
processedSize: 0,
|
|
9323
|
+
totalSize: 3,
|
|
9324
|
+
})
|
|
9325
|
+
).rejects.toMatchObject({
|
|
9326
|
+
errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError,
|
|
9327
|
+
params: { causeCode: HardwareErrorCode.BleTimeoutError },
|
|
9328
|
+
});
|
|
9329
|
+
await source?.close();
|
|
9330
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
9331
|
+
});
|
|
9332
|
+
|
|
9151
9333
|
// TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
|
|
9152
9334
|
// writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
|
|
9153
9335
|
// streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
|
|
@@ -10,6 +10,46 @@ jest.mock('../src/data/config', () => ({
|
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
12
|
describe('writeProtocolV2File', () => {
|
|
13
|
+
test('allows a verified caller-specific BLE chunk limit', async () => {
|
|
14
|
+
const getSettingsSpy = jest
|
|
15
|
+
.spyOn(DataManager, 'getSettings')
|
|
16
|
+
.mockReturnValue('react-native' as any);
|
|
17
|
+
const isBleConnectSpy = jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(true);
|
|
18
|
+
const data = new Uint8Array(1961);
|
|
19
|
+
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await writeProtocolV2File({
|
|
23
|
+
commands: { typedCall } as any,
|
|
24
|
+
path: 'vol1:/wallpapers/wallpaper.okpkg',
|
|
25
|
+
data,
|
|
26
|
+
bleChunkSizeLimit: 1960,
|
|
27
|
+
});
|
|
28
|
+
} finally {
|
|
29
|
+
getSettingsSpy.mockRestore();
|
|
30
|
+
isBleConnectSpy.mockRestore();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
34
|
+
expect(typedCall.mock.calls[0][2].file.data).toEqual(data.slice(0, 1960));
|
|
35
|
+
expect(typedCall.mock.calls[1][2].file.data).toEqual(data.slice(1960));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('does not apply the BLE-only limit to WebUSB', async () => {
|
|
39
|
+
const data = new Uint8Array(1961);
|
|
40
|
+
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
41
|
+
|
|
42
|
+
await writeProtocolV2File({
|
|
43
|
+
commands: { typedCall } as any,
|
|
44
|
+
path: 'vol1:/wallpapers/wallpaper.okpkg',
|
|
45
|
+
data,
|
|
46
|
+
bleChunkSizeLimit: 1960,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(typedCall.mock.calls[0][2].file.data).toEqual(data);
|
|
51
|
+
});
|
|
52
|
+
|
|
13
53
|
test('按分片写入并只在首片设置 overwrite', async () => {
|
|
14
54
|
const data = new Uint8Array(4097);
|
|
15
55
|
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
@@ -18,10 +18,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
18
18
|
private protocolV2LatestFinalDeviceInfo?;
|
|
19
19
|
private protocolV2InstallBaselineVersions;
|
|
20
20
|
private protocolV2InstallNeedsReconnect;
|
|
21
|
+
private protocolV2InstallDisconnectObserved;
|
|
21
22
|
private protocolV2InstallTerminalSuccessObserved;
|
|
22
23
|
private protocolV2LastRuntimeProbeFeatures?;
|
|
23
24
|
private protocolV2LastTransferProgress?;
|
|
24
25
|
private protocolV2LastTransferProgressAt;
|
|
26
|
+
private protocolV2LastTransferredBytes;
|
|
25
27
|
init(): void;
|
|
26
28
|
private getProtocolV2FirmwareChunkSize;
|
|
27
29
|
run(): Promise<{
|