@onekeyfe/hd-core 1.2.8 → 1.2.10-alpha.1
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/README.md +27 -0
- package/__tests__/DeviceCommands.test.ts +66 -13
- package/__tests__/allNetworkGetAddress-privacy-networks.test.ts +78 -0
- package/__tests__/btc-derive-context-hash.test.ts +200 -0
- package/__tests__/core-dispose.test.ts +114 -1
- package/__tests__/core-error-output.test.ts +55 -3
- package/__tests__/device-connector-protocol.test.ts +5 -2
- package/__tests__/device-lifecycle-events.test.ts +191 -27
- package/__tests__/device-pool-state.test.ts +50 -0
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +11 -0
- package/__tests__/logBlockEvent.test.ts +331 -9
- package/__tests__/logger.test.ts +18 -0
- package/__tests__/monero-get-address.test.ts +147 -0
- package/__tests__/monero-key-image-sync.test.ts +272 -0
- package/__tests__/monero-sign-transaction.test.ts +436 -0
- package/__tests__/protocol-v2-bootloader-mode.test.ts +43 -2
- package/__tests__/protocol-v2-resources.test.ts +14 -0
- package/__tests__/protocol-v2.test.ts +841 -46
- package/__tests__/protocolV2FileWrite.test.ts +454 -7
- package/__tests__/refresh-device-state.test.ts +19 -17
- package/__tests__/search-devices.test.ts +36 -0
- package/__tests__/sol-sign-offchain-message.test.ts +8 -0
- package/__tests__/usb-identity-probe.test.ts +358 -0
- package/__tests__/zcash-protobuf.test.ts +141 -0
- package/__tests__/zcash-sign-pczt.test.ts +446 -0
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/FileWrite.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +5 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetDeviceState.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts +21 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/btc/BTCDeriveContextHash.d.ts +16 -0
- package/dist/api/btc/BTCDeriveContextHash.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
- package/dist/api/helpers/pathUtils.d.ts +1 -0
- package/dist/api/helpers/pathUtils.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +5 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/monero/MoneroGetAddress.d.ts +11 -0
- package/dist/api/monero/MoneroGetAddress.d.ts.map +1 -0
- package/dist/api/monero/MoneroGetWatchKey.d.ts +11 -0
- package/dist/api/monero/MoneroGetWatchKey.d.ts.map +1 -0
- package/dist/api/monero/MoneroKeyImageSync.d.ts +20 -0
- package/dist/api/monero/MoneroKeyImageSync.d.ts.map +1 -0
- package/dist/api/monero/MoneroSignTransaction.d.ts +17 -0
- package/dist/api/monero/MoneroSignTransaction.d.ts.map +1 -0
- package/dist/api/monero/helpers.d.ts +5 -0
- package/dist/api/monero/helpers.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceReboot.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/api/zcash/ZcashGetAddress.d.ts +12 -0
- package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
- package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
- package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +3 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts +10 -0
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +2 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +196 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4363 -2792
- package/dist/inject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts +1 -0
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/allNetworkGetAddress.d.ts +5 -1
- package/dist/types/api/allNetworkGetAddress.d.ts.map +1 -1
- package/dist/types/api/btcDeriveContextHash.d.ts +10 -0
- package/dist/types/api/btcDeriveContextHash.d.ts.map +1 -0
- package/dist/types/api/export.d.ts +7 -0
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +14 -0
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/moneroGetAddress.d.ts +15 -0
- package/dist/types/api/moneroGetAddress.d.ts.map +1 -0
- package/dist/types/api/moneroGetWatchKey.d.ts +15 -0
- package/dist/types/api/moneroGetWatchKey.d.ts.map +1 -0
- package/dist/types/api/moneroKeyImageSync.d.ts +24 -0
- package/dist/types/api/moneroKeyImageSync.d.ts.map +1 -0
- package/dist/types/api/moneroSignTransaction.d.ts +32 -0
- package/dist/types/api/moneroSignTransaction.d.ts.map +1 -0
- package/dist/types/api/zcashGetAddress.d.ts +22 -0
- package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
- package/dist/types/api/zcashSignPczt.d.ts +9 -0
- package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
- package/dist/types/device.d.ts +10 -0
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/params.d.ts +1 -0
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +2 -0
- package/src/api/FileWrite.ts +7 -1
- package/src/api/FirmwareUpdateV4.ts +366 -99
- package/src/api/GetDeviceState.ts +2 -5
- package/src/api/SearchDevices.ts +172 -7
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +38 -1
- package/src/api/btc/BTCDeriveContextHash.ts +90 -0
- package/src/api/firmware/FirmwareArtifactSource.ts +12 -3
- package/src/api/helpers/pathUtils.ts +23 -0
- package/src/api/helpers/protocolV2FileWrite.ts +382 -111
- package/src/api/index.ts +8 -0
- package/src/api/monero/MoneroGetAddress.ts +71 -0
- package/src/api/monero/MoneroGetWatchKey.ts +69 -0
- package/src/api/monero/MoneroKeyImageSync.ts +164 -0
- package/src/api/monero/MoneroSignTransaction.ts +319 -0
- package/src/api/monero/helpers.ts +28 -0
- package/src/api/protocol-v2/DeviceReboot.ts +2 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +6 -1
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +15 -1
- package/src/api/zcash/ZcashGetAddress.ts +119 -0
- package/src/api/zcash/ZcashSignPczt.ts +152 -0
- package/src/core/index.ts +52 -24
- package/src/data/messages/messages-protocol-v2.json +138 -1
- package/src/data/messages/messages.json +51 -0
- package/src/device/Device.ts +32 -4
- package/src/device/DeviceCommands.ts +15 -1
- package/src/device/DeviceConnector.ts +4 -4
- package/src/device/DevicePool.ts +12 -2
- package/src/events/core.ts +2 -2
- package/src/events/logBlockEvent.ts +57 -3
- package/src/events/ui-request.ts +2 -0
- package/src/index.ts +1 -0
- package/src/inject.ts +15 -0
- package/src/protocols/protocol-v2/resources.ts +6 -1
- package/src/types/api/allNetworkGetAddress.ts +9 -1
- package/src/types/api/btcDeriveContextHash.ts +15 -0
- package/src/types/api/export.ts +16 -0
- package/src/types/api/index.ts +22 -0
- package/src/types/api/moneroGetAddress.ts +27 -0
- package/src/types/api/moneroGetWatchKey.ts +28 -0
- package/src/types/api/moneroKeyImageSync.ts +45 -0
- package/src/types/api/moneroSignTransaction.ts +54 -0
- package/src/types/api/zcashGetAddress.ts +35 -0
- package/src/types/api/zcashSignPczt.ts +18 -0
- package/src/types/device.ts +18 -0
- package/src/types/params.ts +1 -1
- package/src/utils/logger.ts +4 -0
|
@@ -1,15 +1,462 @@
|
|
|
1
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createProtocolV2AssetFileWriteRecovery,
|
|
6
|
+
type ProtocolV2FileWriteOptions,
|
|
7
|
+
writeProtocolV2File,
|
|
8
|
+
} from '../src/api/helpers/protocolV2FileWrite';
|
|
4
9
|
import { DataManager } from '../src/data-manager';
|
|
5
10
|
import { LoggerNames, getLogger } from '../src/utils/logger';
|
|
6
11
|
|
|
12
|
+
import type { DeviceCommands } from '../src/device/DeviceCommands';
|
|
13
|
+
import type { BaseMethod } from '../src/api/BaseMethod';
|
|
14
|
+
import type { MessageFromOneKey, TransportCallOptions } from '@onekeyfe/hd-transport';
|
|
15
|
+
|
|
7
16
|
jest.mock('../src/data/config', () => ({
|
|
8
17
|
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
9
18
|
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
10
19
|
}));
|
|
11
20
|
|
|
12
21
|
describe('writeProtocolV2File', () => {
|
|
22
|
+
describe('Pro2 / Neo asset pipelines', () => {
|
|
23
|
+
type Request = {
|
|
24
|
+
file: { path: string; offset: number; total_size: number; data: Uint8Array };
|
|
25
|
+
overwrite: boolean;
|
|
26
|
+
append: boolean;
|
|
27
|
+
ui_percentage: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const createCommands = (deviceType = EDeviceType.Pro2) => {
|
|
31
|
+
const requests: Request[] = [];
|
|
32
|
+
const device = {
|
|
33
|
+
isProtocolV2: () => true,
|
|
34
|
+
getCurrentDeviceType: () => deviceType,
|
|
35
|
+
state: { status: { mode: 'normal' }, versions: { firmware: '1.0.3', bootloader: '1.0.0' } },
|
|
36
|
+
};
|
|
37
|
+
const typedCall = jest.fn(
|
|
38
|
+
async (
|
|
39
|
+
_name: string,
|
|
40
|
+
_response: string,
|
|
41
|
+
first: Request,
|
|
42
|
+
options?: TransportCallOptions
|
|
43
|
+
) => {
|
|
44
|
+
let request: Request | undefined = first;
|
|
45
|
+
let response: MessageFromOneKey = { type: 'FilesystemFile', message: {} };
|
|
46
|
+
while (request) {
|
|
47
|
+
requests.push(request);
|
|
48
|
+
response = {
|
|
49
|
+
type: 'FilesystemFile',
|
|
50
|
+
message: {
|
|
51
|
+
...request.file,
|
|
52
|
+
processed_byte: request.file.offset + request.file.data.byteLength,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
options?.fileWritePipeline?.onResponse(response);
|
|
56
|
+
request = (await options?.fileWritePipeline?.next()) as Request | undefined;
|
|
57
|
+
}
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
const commands = { device, typedCall } as unknown as DeviceCommands;
|
|
62
|
+
return { commands, device, typedCall, requests };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native');
|
|
67
|
+
jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => jest.restoreAllMocks());
|
|
71
|
+
|
|
72
|
+
test.each(
|
|
73
|
+
[EDeviceType.Pro2, EDeviceType.Neo].flatMap(deviceType =>
|
|
74
|
+
['react-native', 'desktop-web-ble'].flatMap(env =>
|
|
75
|
+
[
|
|
76
|
+
'vol1:/wallpapers/wallpaper.okpkg',
|
|
77
|
+
'vol1:/nft/nft-deadbeef-1760000000000.okpkg',
|
|
78
|
+
'vol1:/portfolio/portfolio.okpkg.pending',
|
|
79
|
+
].map(path => ({ deviceType, env, path }))
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
)(
|
|
83
|
+
'pipelines $deviceType $path on $env using application version',
|
|
84
|
+
async ({ deviceType, env, path }) => {
|
|
85
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env);
|
|
86
|
+
const { commands, typedCall, requests } = createCommands(deviceType);
|
|
87
|
+
const onProgress = jest.fn();
|
|
88
|
+
|
|
89
|
+
await expect(
|
|
90
|
+
writeProtocolV2File({
|
|
91
|
+
commands,
|
|
92
|
+
path,
|
|
93
|
+
data: new Uint8Array(3601),
|
|
94
|
+
overwrite: true,
|
|
95
|
+
bleChunkSizeLimit: 1960,
|
|
96
|
+
chunkSizeLimit: 4000,
|
|
97
|
+
onProgress,
|
|
98
|
+
})
|
|
99
|
+
).resolves.toMatchObject({ processed_byte: 3601, chunks: 3 });
|
|
100
|
+
|
|
101
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(typedCall.mock.calls[0][3]).toMatchObject({
|
|
103
|
+
writeWithResponse: false,
|
|
104
|
+
fileWritePipeline: { next: expect.any(Function), onResponse: expect.any(Function) },
|
|
105
|
+
});
|
|
106
|
+
expect(requests.map(request => request.file.offset)).toEqual([0, 1800, 3600]);
|
|
107
|
+
expect(requests.map(request => request.file.data.byteLength)).toEqual([1800, 1800, 1]);
|
|
108
|
+
expect(requests.map(request => request.overwrite)).toEqual([true, false, false]);
|
|
109
|
+
expect(onProgress.mock.calls.map(([progress]) => progress.transferredBytes)).toEqual([
|
|
110
|
+
1800, 3600, 3601,
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
test('does not replay a failed pipeline on the invalidated link', async () => {
|
|
116
|
+
const { commands, typedCall } = createCommands();
|
|
117
|
+
const timeout = new Error('BLE response timeout');
|
|
118
|
+
typedCall.mockRejectedValueOnce(timeout);
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
writeProtocolV2File({
|
|
122
|
+
commands,
|
|
123
|
+
overwrite: true,
|
|
124
|
+
path: 'vol1:/wallpapers/wallpaper.okpkg',
|
|
125
|
+
data: new Uint8Array(3601),
|
|
126
|
+
maxChunkRetries: 3,
|
|
127
|
+
})
|
|
128
|
+
).rejects.toBe(timeout);
|
|
129
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test.each(
|
|
133
|
+
[EDeviceType.Pro2, EDeviceType.Neo].flatMap(deviceType =>
|
|
134
|
+
['react-native', 'desktop-web-ble'].flatMap(env =>
|
|
135
|
+
[
|
|
136
|
+
'vol1:/wallpapers/wallpaper.okpkg',
|
|
137
|
+
'vol1:/nft/nft-deadbeef-1760000000000.okpkg',
|
|
138
|
+
'vol1:/portfolio/portfolio.okpkg.pending',
|
|
139
|
+
].map(path => ({ deviceType, env, path }))
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
)(
|
|
143
|
+
'reconnects and restarts the dual-frame $deviceType $path upload on $env',
|
|
144
|
+
async ({ deviceType, env, path }) => {
|
|
145
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env);
|
|
146
|
+
const { commands, typedCall } = createCommands(deviceType);
|
|
147
|
+
const fresh = createCommands(deviceType);
|
|
148
|
+
const timeout = Object.assign(new Error('BLE response timeout'), {
|
|
149
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
150
|
+
});
|
|
151
|
+
typedCall.mockImplementationOnce((_name, _response, first, options) => {
|
|
152
|
+
options?.fileWritePipeline?.onResponse({
|
|
153
|
+
type: 'FilesystemFile',
|
|
154
|
+
message: { ...first.file, processed_byte: 1800 },
|
|
155
|
+
});
|
|
156
|
+
return Promise.reject(timeout);
|
|
157
|
+
});
|
|
158
|
+
const recoverConnection = jest.fn().mockResolvedValue(fresh.commands);
|
|
159
|
+
const onProgress = jest.fn();
|
|
160
|
+
const metrics = jest.spyOn(getLogger(LoggerNames.Core), 'log');
|
|
161
|
+
|
|
162
|
+
await expect(
|
|
163
|
+
writeProtocolV2File({
|
|
164
|
+
commands,
|
|
165
|
+
overwrite: true,
|
|
166
|
+
path,
|
|
167
|
+
data: new Uint8Array(3601),
|
|
168
|
+
maxChunkRetries: 3,
|
|
169
|
+
recoverConnection,
|
|
170
|
+
onProgress,
|
|
171
|
+
})
|
|
172
|
+
).resolves.toMatchObject({ processed_byte: 3601, chunks: 3 });
|
|
173
|
+
|
|
174
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(recoverConnection).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(metrics).toHaveBeenCalledWith(
|
|
177
|
+
expect.stringContaining(`status=failed path=${path} bytes=1800/3601`)
|
|
178
|
+
);
|
|
179
|
+
expect(metrics).toHaveBeenLastCalledWith(
|
|
180
|
+
expect.stringContaining('window=2 chunk=1800 retry=1 averageSpeed=')
|
|
181
|
+
);
|
|
182
|
+
expect(fresh.requests.map(request => request.file.offset)).toEqual([0, 1800, 3600]);
|
|
183
|
+
expect(fresh.requests.map(request => request.overwrite)).toEqual([true, false, false]);
|
|
184
|
+
expect(fresh.typedCall).toHaveBeenCalledTimes(1);
|
|
185
|
+
expect(fresh.typedCall.mock.calls[0][3]?.fileWritePipeline).toBeDefined();
|
|
186
|
+
expect(fresh.typedCall.mock.calls[0][3]?.timeoutMs).toBe(10_000);
|
|
187
|
+
expect(onProgress.mock.calls.map(([progress]) => progress.transferredBytes)).toEqual([
|
|
188
|
+
1800, 0, 1800, 3600, 3601,
|
|
189
|
+
]);
|
|
190
|
+
expect(onProgress.mock.calls[onProgress.mock.calls.length - 1][0]).toMatchObject({
|
|
191
|
+
retryCount: 1,
|
|
192
|
+
progress: 100,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
test('bounds dual-frame whole-file recovery to three extra attempts', async () => {
|
|
198
|
+
const { commands, typedCall } = createCommands();
|
|
199
|
+
const timeout = Object.assign(new Error('BLE response timeout'), {
|
|
200
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
201
|
+
});
|
|
202
|
+
typedCall.mockRejectedValue(timeout);
|
|
203
|
+
const recoverConnection = jest.fn().mockResolvedValue(commands);
|
|
204
|
+
await expect(
|
|
205
|
+
writeProtocolV2File({
|
|
206
|
+
commands,
|
|
207
|
+
overwrite: true,
|
|
208
|
+
path: 'vol1:/wallpapers/wallpaper.okpkg',
|
|
209
|
+
data: new Uint8Array(3601),
|
|
210
|
+
maxChunkRetries: 3,
|
|
211
|
+
recoverConnection,
|
|
212
|
+
})
|
|
213
|
+
).rejects.toBe(timeout);
|
|
214
|
+
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
215
|
+
expect(recoverConnection).toHaveBeenCalledTimes(3);
|
|
216
|
+
expect(typedCall.mock.calls.every(call => !!call[3]?.fileWritePipeline)).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('does not start a retry when cancelled during reconnect', async () => {
|
|
220
|
+
const { commands, typedCall } = createCommands();
|
|
221
|
+
const aborted = new Error('cancelled');
|
|
222
|
+
let cancelled = false;
|
|
223
|
+
typedCall.mockRejectedValueOnce(new Error('BLE response timeout'));
|
|
224
|
+
const recoverConnection = jest.fn(() => {
|
|
225
|
+
cancelled = true;
|
|
226
|
+
return Promise.resolve(commands);
|
|
227
|
+
});
|
|
228
|
+
await expect(
|
|
229
|
+
writeProtocolV2File({
|
|
230
|
+
commands,
|
|
231
|
+
overwrite: true,
|
|
232
|
+
path: 'vol1:/wallpapers/wallpaper.okpkg',
|
|
233
|
+
data: new Uint8Array(3601),
|
|
234
|
+
maxChunkRetries: 3,
|
|
235
|
+
recoverConnection,
|
|
236
|
+
throwIfAborted: () => {
|
|
237
|
+
if (cancelled) throw aborted;
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
).rejects.toBe(aborted);
|
|
241
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('does not retry device rejections or callback failures', async () => {
|
|
245
|
+
const { commands, typedCall } = createCommands();
|
|
246
|
+
const failure = new Error('device rejected file');
|
|
247
|
+
typedCall.mockRejectedValueOnce(failure);
|
|
248
|
+
const recoverConnection = jest.fn();
|
|
249
|
+
await expect(
|
|
250
|
+
writeProtocolV2File({
|
|
251
|
+
commands,
|
|
252
|
+
overwrite: true,
|
|
253
|
+
path: 'vol1:/wallpapers/wallpaper.okpkg',
|
|
254
|
+
data: new Uint8Array(3601),
|
|
255
|
+
maxChunkRetries: 3,
|
|
256
|
+
recoverConnection,
|
|
257
|
+
})
|
|
258
|
+
).rejects.toBe(failure);
|
|
259
|
+
expect(recoverConnection).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test.each(['different-device', ''])(
|
|
263
|
+
'rejects reconnect identity %s before unlocking or writing',
|
|
264
|
+
async serialNo => {
|
|
265
|
+
const { commands, device, typedCall } = createCommands();
|
|
266
|
+
const state = {
|
|
267
|
+
...device.state,
|
|
268
|
+
identity: { serialNo: 'test-device', deviceType: EDeviceType.Pro2 },
|
|
269
|
+
};
|
|
270
|
+
const acquire = jest.fn().mockResolvedValue(undefined);
|
|
271
|
+
const initialize = jest.fn(() => {
|
|
272
|
+
state.identity.serialNo = serialNo;
|
|
273
|
+
return Promise.resolve(undefined);
|
|
274
|
+
});
|
|
275
|
+
const method = {
|
|
276
|
+
device: { ...device, state, commands, acquire, initialize },
|
|
277
|
+
} as unknown as BaseMethod;
|
|
278
|
+
const recover = createProtocolV2AssetFileWriteRecovery(method);
|
|
279
|
+
await expect(recover()).rejects.toMatchObject({
|
|
280
|
+
errorCode: HardwareErrorCode.DeviceCheckDeviceIdError,
|
|
281
|
+
});
|
|
282
|
+
expect(acquire).toHaveBeenCalledWith('V2', { throwOnRunPromiseError: true });
|
|
283
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
test.each(['react-native', 'desktop-web-ble'])(
|
|
288
|
+
'disconnects a cached %s link and refreshes unlock status after reconnect',
|
|
289
|
+
async env => {
|
|
290
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env);
|
|
291
|
+
const { commands, device, typedCall } = createCommands();
|
|
292
|
+
typedCall.mockResolvedValueOnce({ type: 'DeviceStatus', message: { unlocked: false } });
|
|
293
|
+
const unlockDevice = jest.fn().mockResolvedValue({ unlocked: true });
|
|
294
|
+
let nativeConnected = true;
|
|
295
|
+
const disconnect = jest.fn(() => {
|
|
296
|
+
nativeConnected = false;
|
|
297
|
+
return Promise.resolve();
|
|
298
|
+
});
|
|
299
|
+
const acquire = jest.fn(() => {
|
|
300
|
+
if (nativeConnected) {
|
|
301
|
+
return Promise.reject(new Error('reused the timed-out native connection'));
|
|
302
|
+
}
|
|
303
|
+
nativeConnected = true;
|
|
304
|
+
return Promise.resolve();
|
|
305
|
+
});
|
|
306
|
+
const method = {
|
|
307
|
+
name: 'deviceUploadWallpaper',
|
|
308
|
+
device: {
|
|
309
|
+
...device,
|
|
310
|
+
state: {
|
|
311
|
+
...device.state,
|
|
312
|
+
identity: { serialNo: 'test-device', deviceType: EDeviceType.Pro2 },
|
|
313
|
+
status: { mode: 'normal', initialized: true },
|
|
314
|
+
},
|
|
315
|
+
commands,
|
|
316
|
+
originalDescriptor: { id: 'ble-device' },
|
|
317
|
+
deviceConnector: { disconnect },
|
|
318
|
+
acquire,
|
|
319
|
+
initialize: jest.fn().mockResolvedValue(undefined),
|
|
320
|
+
updateProtocolV2Status: (status: unknown) => status,
|
|
321
|
+
unlockDevice,
|
|
322
|
+
toMessageObject: () => ({}),
|
|
323
|
+
},
|
|
324
|
+
unlockPolicy: 'unlock-before-run',
|
|
325
|
+
useDevicePassphraseState: false,
|
|
326
|
+
protocolV2PreUnlockPinType: DeviceSessionPinType.Any,
|
|
327
|
+
protocolV2UnlockContext: { preflightCompleted: true },
|
|
328
|
+
postMessage: jest.fn(),
|
|
329
|
+
payload: {},
|
|
330
|
+
} as unknown as BaseMethod;
|
|
331
|
+
await expect(createProtocolV2AssetFileWriteRecovery(method)()).resolves.toBe(commands);
|
|
332
|
+
expect(disconnect).toHaveBeenCalledWith('ble-device');
|
|
333
|
+
expect(acquire).toHaveBeenCalledWith('V2', { throwOnRunPromiseError: true });
|
|
334
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
335
|
+
expect(unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Any, expect.any(Object));
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const serialCases: {
|
|
340
|
+
name: string;
|
|
341
|
+
firmware?: string;
|
|
342
|
+
protocol?: boolean;
|
|
343
|
+
mode?: string;
|
|
344
|
+
deviceType?: EDeviceType;
|
|
345
|
+
env?: string;
|
|
346
|
+
options?: Partial<ProtocolV2FileWriteOptions>;
|
|
347
|
+
}[] = [
|
|
348
|
+
{ name: 'old application with a new bootloader', firmware: '1.0.2' },
|
|
349
|
+
{ name: 'unknown application', firmware: '' },
|
|
350
|
+
{ name: 'invalid application version', firmware: 'unknown' },
|
|
351
|
+
{ name: 'application prerelease', firmware: '1.0.3-rc.1' },
|
|
352
|
+
{ name: 'Protocol V1', protocol: false },
|
|
353
|
+
{ name: 'bootloader runtime', mode: 'bootloader' },
|
|
354
|
+
{ name: 'another model', deviceType: EDeviceType.Pro },
|
|
355
|
+
{ name: 'WebUSB', env: 'web' },
|
|
356
|
+
{ name: 'LowLevel transport', env: 'lowlevel' },
|
|
357
|
+
{ name: 'a generic file', options: { path: 'vol1:/other.bin' } },
|
|
358
|
+
{ name: 'a partial file', options: { totalSize: 7200 } },
|
|
359
|
+
{ name: 'non-overwriting writes', options: { overwrite: false } },
|
|
360
|
+
{ name: 'append', options: { append: true } },
|
|
361
|
+
{ name: 'offset writes', options: { offset: 1 } },
|
|
362
|
+
{ name: 'explicit pacing', options: { paceMs: 1 } },
|
|
363
|
+
{ name: 'acknowledged characteristic writes', options: { writeWithResponse: true } },
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
test.each(serialCases)('keeps $name serial', async testCase => {
|
|
367
|
+
const { commands, device, typedCall } = createCommands(testCase.deviceType);
|
|
368
|
+
device.isProtocolV2 = () => testCase.protocol ?? true;
|
|
369
|
+
device.state.versions.firmware = testCase.firmware ?? '1.0.3';
|
|
370
|
+
device.state.versions.bootloader = '9.0.0';
|
|
371
|
+
device.state.status.mode = testCase.mode ?? 'normal';
|
|
372
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(testCase.env ?? 'react-native');
|
|
373
|
+
|
|
374
|
+
await writeProtocolV2File({
|
|
375
|
+
commands,
|
|
376
|
+
overwrite: true,
|
|
377
|
+
path: 'vol1:/wallpapers/wallpaper.okpkg',
|
|
378
|
+
data: new Uint8Array(3601),
|
|
379
|
+
...testCase.options,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(typedCall.mock.calls.every(call => !call[3]?.fileWritePipeline)).toBe(true);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('drains an in-flight response on cancellation before rejecting', async () => {
|
|
386
|
+
const { commands, typedCall } = createCommands();
|
|
387
|
+
const aborted = new Error('upload cancelled');
|
|
388
|
+
let cancelled = false;
|
|
389
|
+
const drained = jest.fn();
|
|
390
|
+
const onProgress = jest.fn();
|
|
391
|
+
typedCall.mockImplementationOnce(async (_name, _response, first, options) => {
|
|
392
|
+
const pipeline = options?.fileWritePipeline;
|
|
393
|
+
if (!pipeline) throw new Error('Expected a file write pipeline');
|
|
394
|
+
const second = (await pipeline.next()) as Request;
|
|
395
|
+
cancelled = true;
|
|
396
|
+
expect(() =>
|
|
397
|
+
pipeline.onResponse({ type: 'FilesystemFile', message: { processed_byte: 1800 } })
|
|
398
|
+
).not.toThrow();
|
|
399
|
+
await expect(pipeline.next()).resolves.toBeUndefined();
|
|
400
|
+
const response = { type: 'FilesystemFile', message: { processed_byte: 3600 } };
|
|
401
|
+
pipeline.onResponse(response);
|
|
402
|
+
drained(first, second);
|
|
403
|
+
return response;
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await expect(
|
|
407
|
+
writeProtocolV2File({
|
|
408
|
+
commands,
|
|
409
|
+
overwrite: true,
|
|
410
|
+
path: 'vol1:/wallpapers/wallpaper.okpkg',
|
|
411
|
+
data: new Uint8Array(5401),
|
|
412
|
+
throwIfAborted: () => {
|
|
413
|
+
if (cancelled) throw aborted;
|
|
414
|
+
},
|
|
415
|
+
onProgress,
|
|
416
|
+
})
|
|
417
|
+
).rejects.toBe(aborted);
|
|
418
|
+
expect(drained).toHaveBeenCalledTimes(1);
|
|
419
|
+
expect(onProgress).not.toHaveBeenCalled();
|
|
420
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test('raises a progress callback error after stopping further chunks', async () => {
|
|
424
|
+
const { commands, typedCall, requests } = createCommands();
|
|
425
|
+
const callbackError = new Error('progress callback failed');
|
|
426
|
+
|
|
427
|
+
await expect(
|
|
428
|
+
writeProtocolV2File({
|
|
429
|
+
commands,
|
|
430
|
+
overwrite: true,
|
|
431
|
+
path: 'vol1:/portfolio/portfolio.okpkg.pending',
|
|
432
|
+
data: new Uint8Array(3601),
|
|
433
|
+
onProgress: () => {
|
|
434
|
+
throw callbackError;
|
|
435
|
+
},
|
|
436
|
+
})
|
|
437
|
+
).rejects.toBe(callbackError);
|
|
438
|
+
expect(requests).toHaveLength(1);
|
|
439
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('rejects a pipeline that returns without confirming every chunk', async () => {
|
|
443
|
+
const { commands, typedCall } = createCommands();
|
|
444
|
+
typedCall.mockResolvedValueOnce({
|
|
445
|
+
type: 'FilesystemFile',
|
|
446
|
+
message: { processed_byte: 3601 },
|
|
447
|
+
});
|
|
448
|
+
await expect(
|
|
449
|
+
writeProtocolV2File({
|
|
450
|
+
commands,
|
|
451
|
+
overwrite: true,
|
|
452
|
+
path: 'vol1:/wallpapers/wallpaper.okpkg',
|
|
453
|
+
data: new Uint8Array(3601),
|
|
454
|
+
})
|
|
455
|
+
).rejects.toThrow('before the complete file was confirmed');
|
|
456
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
13
460
|
test('allows a verified caller-specific BLE chunk limit', async () => {
|
|
14
461
|
const getSettingsSpy = jest
|
|
15
462
|
.spyOn(DataManager, 'getSettings')
|
|
@@ -200,7 +647,7 @@ describe('writeProtocolV2File', () => {
|
|
|
200
647
|
});
|
|
201
648
|
|
|
202
649
|
expect(logSpy).toHaveBeenLastCalledWith(
|
|
203
|
-
'[FileWrite] metrics transport=unknown status=completed path=vol0:/firmware.bin bytes=3/3 elapsed=0.05s speed=0.06 KiB/s hostWriteTotal=0.03s responseWaitTotal=0.03s measuredAttempts=1'
|
|
650
|
+
'[FileWrite] metrics transport=unknown status=completed path=vol0:/firmware.bin bytes=3/3 elapsed=0.05s speed=0.06 KiB/s hostWriteTotal=0.03s responseWaitTotal=0.03s measuredAttempts=1 window=1 chunk=4000 retry=0 averageSpeed=0.06 KiB/s'
|
|
204
651
|
);
|
|
205
652
|
} finally {
|
|
206
653
|
dateNowSpy.mockRestore();
|
|
@@ -277,11 +724,11 @@ describe('writeProtocolV2File', () => {
|
|
|
277
724
|
);
|
|
278
725
|
expect(logSpy).toHaveBeenNthCalledWith(
|
|
279
726
|
2,
|
|
280
|
-
'[FileWrite] metrics transport=BLE status=progress path=vol1:/wallpapers/test.bin bytes=3600/5400 elapsed=10.00s speed=0.35 KiB/s'
|
|
727
|
+
'[FileWrite] metrics transport=BLE status=progress path=vol1:/wallpapers/test.bin bytes=3600/5400 elapsed=10.00s speed=0.35 KiB/s window=1 chunk=1800 retry=0 averageSpeed=0.35 KiB/s'
|
|
281
728
|
);
|
|
282
729
|
expect(logSpy).toHaveBeenNthCalledWith(
|
|
283
730
|
3,
|
|
284
|
-
'[FileWrite] metrics transport=BLE status=completed path=vol1:/wallpapers/test.bin bytes=5400/5400 elapsed=14.00s speed=0.44 KiB/s'
|
|
731
|
+
'[FileWrite] metrics transport=BLE status=completed path=vol1:/wallpapers/test.bin bytes=5400/5400 elapsed=14.00s speed=0.44 KiB/s window=1 chunk=1800 retry=0 averageSpeed=0.38 KiB/s'
|
|
285
732
|
);
|
|
286
733
|
} finally {
|
|
287
734
|
dateNowSpy.mockRestore();
|
|
@@ -315,7 +762,7 @@ describe('writeProtocolV2File', () => {
|
|
|
315
762
|
);
|
|
316
763
|
expect(logSpy).toHaveBeenNthCalledWith(
|
|
317
764
|
2,
|
|
318
|
-
'[FileWrite] metrics transport=unknown status=failed path=vol1:/wallpapers/test.bin bytes=0/4000 elapsed=1.00s speed=0.00 KiB/s'
|
|
765
|
+
'[FileWrite] metrics transport=unknown status=failed path=vol1:/wallpapers/test.bin bytes=0/4000 elapsed=1.00s speed=0.00 KiB/s window=1 chunk=4000 retry=0 averageSpeed=0.00 KiB/s'
|
|
319
766
|
);
|
|
320
767
|
} finally {
|
|
321
768
|
dateNowSpy.mockRestore();
|
|
@@ -73,24 +73,26 @@ describe('live device state reads', () => {
|
|
|
73
73
|
}
|
|
74
74
|
);
|
|
75
75
|
|
|
76
|
-
test('
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
test.each(['bootloader', 'romloader'] as const)(
|
|
77
|
+
'reports %s mode for settings scope after minimal mode detection',
|
|
78
|
+
async mode => {
|
|
79
|
+
const getDeviceState = jest.fn().mockResolvedValue({ status: { mode } });
|
|
80
|
+
const method = new GetDeviceState({
|
|
81
|
+
id: 1,
|
|
82
|
+
payload: { method: 'getDeviceState', scope: 'settings' },
|
|
83
|
+
});
|
|
84
|
+
method.init();
|
|
85
|
+
(method as any).device = {
|
|
86
|
+
isProtocolV2: () => true,
|
|
87
|
+
getDeviceState,
|
|
88
|
+
};
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
91
|
+
errorCode: HardwareErrorCode.NotAllowInBootloaderMode,
|
|
92
|
+
});
|
|
93
|
+
expect(getDeviceState).toHaveBeenCalledWith({ refreshSections: ['status', 'settings'] });
|
|
94
|
+
}
|
|
95
|
+
);
|
|
94
96
|
|
|
95
97
|
test('rejects an unknown scope', () => {
|
|
96
98
|
const method = new GetDeviceState({
|
|
@@ -338,6 +338,42 @@ describe('SearchDevices', () => {
|
|
|
338
338
|
}
|
|
339
339
|
);
|
|
340
340
|
|
|
341
|
+
test('BLE discovery returns the advertised color letter without a device identity', async () => {
|
|
342
|
+
mockIsBleConnect.mockReturnValue(true);
|
|
343
|
+
|
|
344
|
+
const descriptor = {
|
|
345
|
+
id: 'ble-peripheral-id',
|
|
346
|
+
path: 'ble-peripheral-id',
|
|
347
|
+
name: 'Neo A1B2',
|
|
348
|
+
commType: 'ble',
|
|
349
|
+
bleColorCode: 'E',
|
|
350
|
+
};
|
|
351
|
+
const method = new SearchDevices({
|
|
352
|
+
id: 1,
|
|
353
|
+
payload: {
|
|
354
|
+
method: 'searchDevices',
|
|
355
|
+
},
|
|
356
|
+
} as any);
|
|
357
|
+
method.init();
|
|
358
|
+
method.connector = {
|
|
359
|
+
enumerate: jest.fn().mockResolvedValue({
|
|
360
|
+
descriptors: [descriptor],
|
|
361
|
+
}),
|
|
362
|
+
} as any;
|
|
363
|
+
|
|
364
|
+
await expect(method.run()).resolves.toEqual([
|
|
365
|
+
{
|
|
366
|
+
...descriptor,
|
|
367
|
+
connectId: 'ble-peripheral-id',
|
|
368
|
+
serialNo: null,
|
|
369
|
+
uuid: '',
|
|
370
|
+
deviceId: null,
|
|
371
|
+
deviceType: 'neo',
|
|
372
|
+
bleColorCode: 'E',
|
|
373
|
+
},
|
|
374
|
+
]);
|
|
375
|
+
});
|
|
376
|
+
|
|
341
377
|
test.each([
|
|
342
378
|
['Pro2 A1B2 - Find My', undefined, 'Pro 2 A1B2'],
|
|
343
379
|
['Pro2 5E9D - Finde My', undefined, 'Pro 2 5E9D'],
|
|
@@ -54,6 +54,14 @@ describe('SolSignOffchainMessage', () => {
|
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
test('maps omitted required signers to an empty repeated field', () => {
|
|
58
|
+
const method = createMethod({ requiredSigners: undefined });
|
|
59
|
+
|
|
60
|
+
method.init();
|
|
61
|
+
|
|
62
|
+
expect(method.params.required_signers).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
57
65
|
test.each([
|
|
58
66
|
{ requiredSigners: ['11'], error: '32-byte hex public key' },
|
|
59
67
|
{ requiredSigners: [signerB, signerA], error: 'strictly sorted and unique' },
|