@onekeyfe/hd-core 1.2.8-alpha.13 → 1.2.8-alpha.130
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__/allNetworkGetAddress-privacy-networks.test.ts +78 -0
- package/__tests__/core-dispose.test.ts +114 -1
- package/__tests__/core-error-output.test.ts +21 -0
- package/__tests__/device-connector-protocol.test.ts +5 -2
- package/__tests__/device-lifecycle-events.test.ts +118 -3
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +11 -0
- package/__tests__/logBlockEvent.test.ts +292 -0
- 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-resources.test.ts +14 -0
- package/__tests__/protocol-v2.test.ts +845 -3
- package/__tests__/protocolV2FileWrite.test.ts +434 -7
- package/__tests__/search-devices.test.ts +36 -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/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/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 +6 -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/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 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +1 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- 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 +183 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5625 -4192
- 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/export.d.ts +6 -0
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +12 -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/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 +358 -74
- package/src/api/SearchDevices.ts +172 -7
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +38 -1
- package/src/api/firmware/FirmwareArtifactSource.ts +12 -3
- package/src/api/helpers/pathUtils.ts +23 -0
- package/src/api/helpers/protocolV2FileWrite.ts +375 -111
- package/src/api/index.ts +7 -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/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 +49 -17
- package/src/data/messages/messages-protocol-v2.json +138 -1
- package/src/device/Device.ts +21 -3
- package/src/device/DeviceConnector.ts +4 -4
- package/src/events/logBlockEvent.ts +55 -3
- package/src/events/ui-request.ts +2 -0
- package/src/index.ts +1 -0
- package/src/inject.ts +13 -0
- package/src/protocols/protocol-v2/resources.ts +6 -1
- package/src/types/api/allNetworkGetAddress.ts +9 -1
- package/src/types/api/export.ts +15 -0
- package/src/types/api/index.ts +20 -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/utils/logger.ts +4 -0
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
|
|
3
|
+
PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW,
|
|
3
4
|
PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
|
|
4
5
|
} from '@onekeyfe/hd-transport';
|
|
5
|
-
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
6
|
+
import { EDeviceType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
7
|
+
import semver from 'semver';
|
|
6
8
|
|
|
7
9
|
import { DataManager } from '../../data-manager';
|
|
8
10
|
import { LoggerNames, getLogger } from '../../utils/logger';
|
|
11
|
+
import { runMethodWithUnlockPolicy } from '../../protocols/protocol-v2/unlockPolicyRunner';
|
|
12
|
+
import { ProtocolV2UiInteractionCoordinator } from '../../protocols/protocol-v2/uiInteraction';
|
|
13
|
+
import { DevicePool } from '../../device/DevicePool';
|
|
14
|
+
import { DEVICE } from '../../events';
|
|
9
15
|
|
|
10
16
|
import type { DeviceCommands } from '../../device/DeviceCommands';
|
|
17
|
+
import type { BaseMethod } from '../BaseMethod';
|
|
11
18
|
|
|
12
19
|
const Log = getLogger(LoggerNames.Core);
|
|
13
20
|
|
|
@@ -19,6 +26,7 @@ export type ProtocolV2FileWriteProgress = {
|
|
|
19
26
|
totalBytes: number;
|
|
20
27
|
rateBytesPerSecond?: number;
|
|
21
28
|
elapsedMs: number;
|
|
29
|
+
retryCount?: number;
|
|
22
30
|
};
|
|
23
31
|
|
|
24
32
|
export type ProtocolV2FileWriteChunkContext = {
|
|
@@ -28,7 +36,7 @@ export type ProtocolV2FileWriteChunkContext = {
|
|
|
28
36
|
};
|
|
29
37
|
|
|
30
38
|
export type ProtocolV2FileWriteOptions = {
|
|
31
|
-
commands: Pick<DeviceCommands, 'typedCall'
|
|
39
|
+
commands: Pick<DeviceCommands, 'typedCall'> & Partial<Pick<DeviceCommands, 'device'>>;
|
|
32
40
|
path: string;
|
|
33
41
|
data: ProtocolV2FileWriteData;
|
|
34
42
|
offset?: number;
|
|
@@ -44,6 +52,8 @@ export type ProtocolV2FileWriteOptions = {
|
|
|
44
52
|
timeoutMs?: number;
|
|
45
53
|
writeWithResponse?: boolean;
|
|
46
54
|
maxChunkRetries?: number;
|
|
55
|
+
/** Reacquire and verify the device before restarting an overwriting asset upload. */
|
|
56
|
+
recoverConnection?: () => Promise<ProtocolV2FileWriteOptions['commands']>;
|
|
47
57
|
paceMs?: number;
|
|
48
58
|
throwIfAborted?: () => void;
|
|
49
59
|
getUiPercentage?: (context: ProtocolV2FileWriteChunkContext) => number | undefined;
|
|
@@ -54,6 +64,102 @@ const MIN_FILE_CHUNK_SIZE = 64;
|
|
|
54
64
|
const FILE_TRANSFER_RATE_WINDOW_MS = 1000;
|
|
55
65
|
const FILE_TRANSFER_LOG_INTERVAL_MS = 10_000;
|
|
56
66
|
const SESSION_ERROR = 'session not found';
|
|
67
|
+
// Application firmware from firmware-pro2 #239 has the 4096-byte UART FIFO required for
|
|
68
|
+
// two 1800-byte chunks. Gate on the running application, not the installed bootloader.
|
|
69
|
+
const ASSET_FILE_WRITE_MIN_APPLICATION_VERSION = '1.0.3';
|
|
70
|
+
const ASSET_FILE_WRITE_RESPONSE_TIMEOUT = 10_000;
|
|
71
|
+
|
|
72
|
+
export function createProtocolV2AssetFileWriteRecovery(
|
|
73
|
+
method: Pick<
|
|
74
|
+
BaseMethod,
|
|
75
|
+
| 'device'
|
|
76
|
+
| 'abortSignal'
|
|
77
|
+
| 'postMessage'
|
|
78
|
+
| 'name'
|
|
79
|
+
| 'unlockPolicy'
|
|
80
|
+
| 'useDevicePassphraseState'
|
|
81
|
+
| 'protocolV2PreUnlockPinType'
|
|
82
|
+
| 'protocolV2UiMode'
|
|
83
|
+
| 'payload'
|
|
84
|
+
>
|
|
85
|
+
): () => Promise<ProtocolV2FileWriteOptions['commands']> {
|
|
86
|
+
const { device } = method;
|
|
87
|
+
const expectedSerialNo = device.state?.identity?.serialNo;
|
|
88
|
+
const expectedDeviceType = device.state?.identity?.deviceType;
|
|
89
|
+
return async () => {
|
|
90
|
+
if (method.abortSignal?.aborted) {
|
|
91
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
92
|
+
}
|
|
93
|
+
if (!expectedSerialNo) {
|
|
94
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
|
|
95
|
+
}
|
|
96
|
+
await device.acquire('V2', { throwOnRunPromiseError: true });
|
|
97
|
+
if (method.abortSignal?.aborted) {
|
|
98
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
99
|
+
}
|
|
100
|
+
await device.initialize();
|
|
101
|
+
if (
|
|
102
|
+
!device.isProtocolV2() ||
|
|
103
|
+
device.state?.identity.serialNo !== expectedSerialNo ||
|
|
104
|
+
device.state?.identity.deviceType !== expectedDeviceType ||
|
|
105
|
+
!['normal', 'notInitialized'].includes(device.state?.status.mode ?? '')
|
|
106
|
+
) {
|
|
107
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
|
|
108
|
+
}
|
|
109
|
+
if (method.abortSignal?.aborted) {
|
|
110
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
111
|
+
}
|
|
112
|
+
// Re-run the existing unlock policy with a fresh preflight after reconnecting.
|
|
113
|
+
await runMethodWithUnlockPolicy(
|
|
114
|
+
{
|
|
115
|
+
name: method.name,
|
|
116
|
+
unlockPolicy: method.unlockPolicy,
|
|
117
|
+
useDevicePassphraseState: method.useDevicePassphraseState,
|
|
118
|
+
protocolV2PreUnlockPinType: method.protocolV2PreUnlockPinType,
|
|
119
|
+
protocolV2UiMode: method.protocolV2UiMode,
|
|
120
|
+
payload: method.payload,
|
|
121
|
+
params: undefined,
|
|
122
|
+
run: () => Promise.resolve(undefined),
|
|
123
|
+
},
|
|
124
|
+
device,
|
|
125
|
+
{ uiCoordinator: new ProtocolV2UiInteractionCoordinator(device, method.postMessage) }
|
|
126
|
+
);
|
|
127
|
+
if (method.abortSignal?.aborted) {
|
|
128
|
+
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
129
|
+
}
|
|
130
|
+
DevicePool.emitter.emit(DEVICE.CONNECT, device);
|
|
131
|
+
return device.commands;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function shouldPipelineAssetFileWrite(options: ProtocolV2FileWriteOptions) {
|
|
136
|
+
const env = DataManager.getSettings('env');
|
|
137
|
+
if (env !== 'react-native' && env !== 'desktop-web-ble') return false;
|
|
138
|
+
if (
|
|
139
|
+
options.overwrite !== true ||
|
|
140
|
+
options.append ||
|
|
141
|
+
(options.offset ?? 0) !== 0 ||
|
|
142
|
+
(options.paceMs ?? 0) > 0 ||
|
|
143
|
+
options.writeWithResponse === true ||
|
|
144
|
+
!/^vol1:\/(?:wallpapers\/wallpaper\.okpkg|nft\/nft-[0-9a-f]{8}-[1-9][0-9]*\.okpkg|portfolio\/portfolio\.okpkg\.pending)$/.test(
|
|
145
|
+
options.path
|
|
146
|
+
)
|
|
147
|
+
) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const { device } = options.commands;
|
|
151
|
+
const deviceType = device?.getCurrentDeviceType();
|
|
152
|
+
const firmwareVersion = device?.state?.versions.firmware;
|
|
153
|
+
const mode = device?.state?.status.mode;
|
|
154
|
+
return Boolean(
|
|
155
|
+
device?.isProtocolV2() &&
|
|
156
|
+
(deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) &&
|
|
157
|
+
(mode === 'normal' || mode === 'notInitialized') &&
|
|
158
|
+
firmwareVersion &&
|
|
159
|
+
semver.valid(firmwareVersion) &&
|
|
160
|
+
semver.gte(firmwareVersion, ASSET_FILE_WRITE_MIN_APPLICATION_VERSION)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
57
163
|
|
|
58
164
|
function formatFileTransferRate(bytesPerSecond: number) {
|
|
59
165
|
return (Math.max(bytesPerSecond, 0) / 1024).toFixed(2);
|
|
@@ -80,6 +186,9 @@ function logFileTransferMetrics({
|
|
|
80
186
|
hostWriteElapsedMs,
|
|
81
187
|
responseWaitElapsedMs,
|
|
82
188
|
measuredAttempts,
|
|
189
|
+
fileWriteWindow,
|
|
190
|
+
chunkSize,
|
|
191
|
+
retryCount,
|
|
83
192
|
}: {
|
|
84
193
|
transport: string;
|
|
85
194
|
status: 'progress' | 'completed' | 'failed';
|
|
@@ -91,6 +200,9 @@ function logFileTransferMetrics({
|
|
|
91
200
|
hostWriteElapsedMs: number;
|
|
92
201
|
responseWaitElapsedMs: number;
|
|
93
202
|
measuredAttempts: number;
|
|
203
|
+
fileWriteWindow: number;
|
|
204
|
+
chunkSize: number;
|
|
205
|
+
retryCount: number;
|
|
94
206
|
}) {
|
|
95
207
|
const segmentedMetrics =
|
|
96
208
|
measuredAttempts > 0
|
|
@@ -101,7 +213,11 @@ function logFileTransferMetrics({
|
|
|
101
213
|
Log.log(
|
|
102
214
|
`[FileWrite] metrics transport=${transport} status=${status} path=${path} bytes=${transferredBytes}/${totalBytes} elapsed=${(
|
|
103
215
|
elapsedMs / 1000
|
|
104
|
-
).toFixed(2)}s speed=${formatFileTransferRate(
|
|
216
|
+
).toFixed(2)}s speed=${formatFileTransferRate(
|
|
217
|
+
rateBytesPerSecond
|
|
218
|
+
)} KiB/s${segmentedMetrics} window=${fileWriteWindow} chunk=${chunkSize} retry=${retryCount} averageSpeed=${formatFileTransferRate(
|
|
219
|
+
getAverageFileTransferRate(transferredBytes, elapsedMs)
|
|
220
|
+
)} KiB/s`
|
|
105
221
|
);
|
|
106
222
|
}
|
|
107
223
|
|
|
@@ -185,16 +301,26 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
|
|
|
185
301
|
}
|
|
186
302
|
|
|
187
303
|
const defaultChunkSizeLimit = getProtocolV2FileChunkLimit(options.bleChunkSizeLimit);
|
|
304
|
+
const pipelineAsset = totalSize === dataLength && shouldPipelineAssetFileWrite(options);
|
|
188
305
|
const configuredChunkSizeLimit = Number(options.chunkSizeLimit);
|
|
189
306
|
const chunkSizeLimit =
|
|
190
307
|
Number.isFinite(configuredChunkSizeLimit) && configuredChunkSizeLimit > 0
|
|
191
308
|
? Math.floor(configuredChunkSizeLimit)
|
|
192
309
|
: defaultChunkSizeLimit;
|
|
193
|
-
const chunkSize = normalizeChunkSize(
|
|
310
|
+
const chunkSize = normalizeChunkSize(
|
|
311
|
+
options.chunkSize ?? options.chunkLen,
|
|
312
|
+
pipelineAsset ? Math.min(chunkSizeLimit, PROTOCOL_V2_BLE_FILE_CHUNK_SIZE) : chunkSizeLimit
|
|
313
|
+
);
|
|
314
|
+
const fileWriteWindow =
|
|
315
|
+
pipelineAsset && dataLength > chunkSize ? PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW : 1;
|
|
194
316
|
let written = 0;
|
|
195
317
|
let chunks = 0;
|
|
196
318
|
let lastMessage: Record<string, unknown> | undefined;
|
|
197
|
-
|
|
319
|
+
let { commands } = options;
|
|
320
|
+
let retryCount = 0;
|
|
321
|
+
const maxChunkRetries = Math.max(Math.floor(options.maxChunkRetries ?? 0), 0);
|
|
322
|
+
const canRecoverPipeline = pipelineAsset && dataLength > chunkSize && options.recoverConnection;
|
|
323
|
+
let startTime = Date.now();
|
|
198
324
|
let rateWindowStartedAt = startTime;
|
|
199
325
|
let rateWindowStartedBytes = 0;
|
|
200
326
|
let rateBytesPerSecond: number | undefined;
|
|
@@ -204,32 +330,182 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
|
|
|
204
330
|
let hostWriteElapsedMs = 0;
|
|
205
331
|
let responseWaitElapsedMs = 0;
|
|
206
332
|
let measuredAttempts = 0;
|
|
333
|
+
const createRequest = (position: number) => {
|
|
334
|
+
const chunk = data.slice(position, Math.min(position + chunkSize, dataLength));
|
|
335
|
+
const offset = startOffset + position;
|
|
336
|
+
const progress =
|
|
337
|
+
options.uiPercentage ??
|
|
338
|
+
options.getUiPercentage?.({
|
|
339
|
+
offset,
|
|
340
|
+
chunkLength: chunk.byteLength,
|
|
341
|
+
totalSize,
|
|
342
|
+
}) ??
|
|
343
|
+
getDeviceTransferProgress(offset, offset + chunk.byteLength, totalSize);
|
|
344
|
+
return {
|
|
345
|
+
file: { path: options.path, offset, total_size: totalSize, data: chunk },
|
|
346
|
+
overwrite: position === 0 ? options.overwrite ?? false : false,
|
|
347
|
+
append: options.append ?? false,
|
|
348
|
+
ui_percentage: progress,
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const confirmChunk = (
|
|
353
|
+
response: { type?: string; message?: Record<string, unknown> },
|
|
354
|
+
offset: number,
|
|
355
|
+
chunkLength: number
|
|
356
|
+
) => {
|
|
357
|
+
const responseType = (response as { type?: string }).type;
|
|
358
|
+
if (responseType && responseType !== 'FilesystemFile') {
|
|
359
|
+
const responseError = (response as { message?: { error?: unknown } }).message?.error;
|
|
360
|
+
if (typeof responseError === 'string' && responseError.includes(SESSION_ERROR)) {
|
|
361
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
|
|
362
|
+
}
|
|
363
|
+
throw ERRORS.TypedError(
|
|
364
|
+
HardwareErrorCode.RuntimeError,
|
|
365
|
+
`FilesystemFileWrite received unexpected response ${responseType}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
options.throwIfAborted?.();
|
|
369
|
+
lastMessage = response.message;
|
|
370
|
+
const rawProcessedByte = response.message?.processed_byte;
|
|
371
|
+
const processedByte = Number(rawProcessedByte);
|
|
372
|
+
if (
|
|
373
|
+
rawProcessedByte !== undefined &&
|
|
374
|
+
(!Number.isFinite(processedByte) ||
|
|
375
|
+
processedByte <= offset ||
|
|
376
|
+
processedByte > offset + chunkLength)
|
|
377
|
+
) {
|
|
378
|
+
throw ERRORS.TypedError(
|
|
379
|
+
HardwareErrorCode.RuntimeError,
|
|
380
|
+
`FilesystemFileWrite invalid processed_byte ${processedByte}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
written = rawProcessedByte === undefined ? written + chunkLength : processedByte - startOffset;
|
|
384
|
+
chunks += 1;
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
lastConfirmedAt = now;
|
|
387
|
+
const elapsedMs = now - startTime;
|
|
388
|
+
const transferredBytes = Math.min(written, dataLength);
|
|
389
|
+
const rateWindowElapsedMs = now - rateWindowStartedAt;
|
|
390
|
+
if (rateWindowElapsedMs >= FILE_TRANSFER_RATE_WINDOW_MS) {
|
|
391
|
+
const rateWindowBytes = Math.max(transferredBytes - rateWindowStartedBytes, 0);
|
|
392
|
+
rateBytesPerSecond = Math.round((rateWindowBytes / rateWindowElapsedMs) * 1000);
|
|
393
|
+
rateWindowStartedAt = now;
|
|
394
|
+
rateWindowStartedBytes = transferredBytes;
|
|
395
|
+
} else if (rateBytesPerSecond === undefined && elapsedMs > 0) {
|
|
396
|
+
rateBytesPerSecond = Math.round((transferredBytes / elapsedMs) * 1000);
|
|
397
|
+
}
|
|
398
|
+
options.onProgress?.({
|
|
399
|
+
progress: getConfirmedProgress(startOffset + written, totalSize, written, dataLength),
|
|
400
|
+
transferredBytes,
|
|
401
|
+
totalBytes: dataLength,
|
|
402
|
+
rateBytesPerSecond,
|
|
403
|
+
elapsedMs,
|
|
404
|
+
...(retryCount > 0 ? { retryCount } : {}),
|
|
405
|
+
});
|
|
406
|
+
const logWindowElapsedMs = now - logWindowStartedAt;
|
|
407
|
+
if (logWindowElapsedMs >= FILE_TRANSFER_LOG_INTERVAL_MS && transferredBytes < dataLength) {
|
|
408
|
+
const logWindowBytes = Math.max(transferredBytes - logWindowStartedBytes, 0);
|
|
409
|
+
logFileTransferMetrics({
|
|
410
|
+
transport,
|
|
411
|
+
status: 'progress',
|
|
412
|
+
path: options.path,
|
|
413
|
+
transferredBytes,
|
|
414
|
+
totalBytes: dataLength,
|
|
415
|
+
elapsedMs,
|
|
416
|
+
rateBytesPerSecond: getAverageFileTransferRate(logWindowBytes, logWindowElapsedMs),
|
|
417
|
+
hostWriteElapsedMs,
|
|
418
|
+
responseWaitElapsedMs,
|
|
419
|
+
measuredAttempts,
|
|
420
|
+
fileWriteWindow,
|
|
421
|
+
chunkSize,
|
|
422
|
+
retryCount,
|
|
423
|
+
});
|
|
424
|
+
logWindowStartedAt = now;
|
|
425
|
+
logWindowStartedBytes = transferredBytes;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
207
429
|
const transport = getFileTransferTransport();
|
|
208
430
|
Log.log(
|
|
209
431
|
`[FileWrite] started transport=${transport} path=${options.path} bytes=${dataLength} offset=${startOffset} chunk=${chunkSize}`
|
|
210
432
|
);
|
|
211
433
|
|
|
212
|
-
|
|
434
|
+
const writeAttempt = async () => {
|
|
435
|
+
options.throwIfAborted?.();
|
|
436
|
+
const attemptRetryCount = retryCount;
|
|
437
|
+
if (fileWriteWindow === PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW) {
|
|
438
|
+
Log.debug('[FileWrite] BLE asset pipeline', {
|
|
439
|
+
path: options.path,
|
|
440
|
+
window: fileWriteWindow,
|
|
441
|
+
chunkSize,
|
|
442
|
+
retryCount,
|
|
443
|
+
applicationVersion: commands.device?.state?.versions.firmware,
|
|
444
|
+
});
|
|
445
|
+
let sendOffset = chunkSize;
|
|
446
|
+
let interrupted = false;
|
|
447
|
+
let interruption: unknown;
|
|
448
|
+
const response = await commands.typedCall(
|
|
449
|
+
'FilesystemFileWrite',
|
|
450
|
+
'FilesystemFile',
|
|
451
|
+
createRequest(0),
|
|
452
|
+
{
|
|
453
|
+
timeoutMs: options.timeoutMs ?? ASSET_FILE_WRITE_RESPONSE_TIMEOUT,
|
|
454
|
+
writeWithResponse: false,
|
|
455
|
+
fileWritePipeline: {
|
|
456
|
+
next: () => {
|
|
457
|
+
if (interrupted || attemptRetryCount !== retryCount || sendOffset >= dataLength)
|
|
458
|
+
return Promise.resolve(undefined);
|
|
459
|
+
try {
|
|
460
|
+
options.throwIfAborted?.();
|
|
461
|
+
const request = createRequest(sendOffset);
|
|
462
|
+
sendOffset += request.file.data.byteLength;
|
|
463
|
+
return Promise.resolve(request);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
interrupted = true;
|
|
466
|
+
interruption = error;
|
|
467
|
+
return Promise.resolve(undefined);
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
onResponse: confirmed => {
|
|
471
|
+
if (interrupted || attemptRetryCount !== retryCount) return;
|
|
472
|
+
try {
|
|
473
|
+
const offset = startOffset + written;
|
|
474
|
+
const length = Math.min(chunkSize, dataLength - written);
|
|
475
|
+
if (Number(confirmed.message.processed_byte) !== offset + length) {
|
|
476
|
+
throw ERRORS.TypedError(
|
|
477
|
+
HardwareErrorCode.RuntimeError,
|
|
478
|
+
'FilesystemFileWrite pipeline did not confirm the complete chunk'
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
confirmChunk(confirmed, offset, length);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
// Drain already-sent responses before raising callback errors or cancellation.
|
|
484
|
+
interrupted = true;
|
|
485
|
+
interruption = error;
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
options.throwIfAborted?.();
|
|
492
|
+
if (interrupted) throw interruption;
|
|
493
|
+
if (
|
|
494
|
+
response.type !== 'FilesystemFile' ||
|
|
495
|
+
Number(response.message.processed_byte) !== startOffset + dataLength ||
|
|
496
|
+
written !== dataLength
|
|
497
|
+
) {
|
|
498
|
+
throw ERRORS.TypedError(
|
|
499
|
+
HardwareErrorCode.RuntimeError,
|
|
500
|
+
'FilesystemFileWrite pipeline ended before the complete file was confirmed'
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
213
504
|
while (written < dataLength) {
|
|
214
505
|
options.throwIfAborted?.();
|
|
215
|
-
const
|
|
216
|
-
const offset
|
|
217
|
-
|
|
218
|
-
options.uiPercentage ??
|
|
219
|
-
options.getUiPercentage?.({
|
|
220
|
-
offset,
|
|
221
|
-
chunkLength: chunk.byteLength,
|
|
222
|
-
totalSize,
|
|
223
|
-
}) ??
|
|
224
|
-
getDeviceTransferProgress(offset, offset + chunk.byteLength, totalSize);
|
|
225
|
-
const request = {
|
|
226
|
-
file: { path: options.path, offset, total_size: totalSize, data: chunk },
|
|
227
|
-
overwrite: chunks === 0 ? options.overwrite ?? false : false,
|
|
228
|
-
append: options.append ?? false,
|
|
229
|
-
ui_percentage: progress,
|
|
230
|
-
};
|
|
231
|
-
const maxChunkRetries = Math.max(Math.floor(options.maxChunkRetries ?? 0), 0);
|
|
232
|
-
let retryCount = 0;
|
|
506
|
+
const request = createRequest(written);
|
|
507
|
+
const { offset, data: chunk } = request.file;
|
|
508
|
+
let chunkRetryCount = 0;
|
|
233
509
|
let response;
|
|
234
510
|
let isWritePending = true;
|
|
235
511
|
while (isWritePending) {
|
|
@@ -239,7 +515,9 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
|
|
|
239
515
|
const callOptions =
|
|
240
516
|
options.writeWithResponse === undefined
|
|
241
517
|
? {
|
|
242
|
-
timeoutMs:
|
|
518
|
+
timeoutMs:
|
|
519
|
+
options.timeoutMs ??
|
|
520
|
+
(canRecoverPipeline ? ASSET_FILE_WRITE_RESPONSE_TIMEOUT : undefined),
|
|
243
521
|
onWriteCompleted: ({ elapsedMs }: { elapsedMs: number }) => {
|
|
244
522
|
currentHostWriteElapsedMs = elapsedMs;
|
|
245
523
|
responseWaitStartedAt = Date.now();
|
|
@@ -253,7 +531,7 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
|
|
|
253
531
|
responseWaitStartedAt = Date.now();
|
|
254
532
|
},
|
|
255
533
|
};
|
|
256
|
-
response = await
|
|
534
|
+
response = await commands.typedCall(
|
|
257
535
|
'FilesystemFileWrite',
|
|
258
536
|
'FilesystemFile',
|
|
259
537
|
request,
|
|
@@ -261,8 +539,13 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
|
|
|
261
539
|
);
|
|
262
540
|
isWritePending = false;
|
|
263
541
|
} catch (error) {
|
|
264
|
-
if (
|
|
265
|
-
|
|
542
|
+
if (
|
|
543
|
+
canRecoverPipeline ||
|
|
544
|
+
chunkRetryCount >= maxChunkRetries ||
|
|
545
|
+
!isProtocolV2ResponseTimeout(error)
|
|
546
|
+
)
|
|
547
|
+
throw error;
|
|
548
|
+
chunkRetryCount += 1;
|
|
266
549
|
options.throwIfAborted?.();
|
|
267
550
|
} finally {
|
|
268
551
|
if (currentHostWriteElapsedMs !== undefined && responseWaitStartedAt !== undefined) {
|
|
@@ -278,97 +561,75 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
|
|
|
278
561
|
'FilesystemFileWrite completed without a response'
|
|
279
562
|
);
|
|
280
563
|
}
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
throw ERRORS.TypedError(
|
|
288
|
-
HardwareErrorCode.RuntimeError,
|
|
289
|
-
`FilesystemFileWrite received unexpected response ${responseType}`
|
|
290
|
-
);
|
|
564
|
+
confirmChunk(response, offset, chunk.byteLength);
|
|
565
|
+
if (options.paceMs && options.paceMs > 0) {
|
|
566
|
+
await new Promise(resolve => {
|
|
567
|
+
setTimeout(resolve, options.paceMs);
|
|
568
|
+
});
|
|
291
569
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
while (true) {
|
|
573
|
+
try {
|
|
574
|
+
await writeAttempt();
|
|
575
|
+
break;
|
|
576
|
+
} catch (error) {
|
|
577
|
+
const now = Date.now();
|
|
578
|
+
const elapsedMs = Math.max(now - startTime, 0);
|
|
579
|
+
const logWindowElapsedMs = Math.max(now - logWindowStartedAt, 0);
|
|
580
|
+
const logWindowBytes = Math.max(written - logWindowStartedBytes, 0);
|
|
581
|
+
logFileTransferMetrics({
|
|
582
|
+
transport,
|
|
583
|
+
status: 'failed',
|
|
584
|
+
path: options.path,
|
|
585
|
+
transferredBytes: written,
|
|
586
|
+
totalBytes: dataLength,
|
|
587
|
+
elapsedMs,
|
|
588
|
+
rateBytesPerSecond: getAverageFileTransferRate(logWindowBytes, logWindowElapsedMs),
|
|
589
|
+
hostWriteElapsedMs,
|
|
590
|
+
responseWaitElapsedMs,
|
|
591
|
+
measuredAttempts,
|
|
592
|
+
fileWriteWindow,
|
|
593
|
+
chunkSize,
|
|
594
|
+
retryCount,
|
|
595
|
+
});
|
|
296
596
|
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
processedByte > offset + chunk.byteLength)
|
|
597
|
+
!canRecoverPipeline ||
|
|
598
|
+
retryCount >= maxChunkRetries ||
|
|
599
|
+
!isProtocolV2ResponseTimeout(error)
|
|
301
600
|
) {
|
|
302
|
-
throw
|
|
303
|
-
HardwareErrorCode.RuntimeError,
|
|
304
|
-
`FilesystemFileWrite invalid processed_byte ${processedByte}`
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
written =
|
|
308
|
-
rawProcessedByte === undefined ? written + chunk.byteLength : processedByte - startOffset;
|
|
309
|
-
chunks += 1;
|
|
310
|
-
const now = Date.now();
|
|
311
|
-
lastConfirmedAt = now;
|
|
312
|
-
const elapsedMs = now - startTime;
|
|
313
|
-
const transferredBytes = Math.min(written, dataLength);
|
|
314
|
-
const rateWindowElapsedMs = now - rateWindowStartedAt;
|
|
315
|
-
if (rateWindowElapsedMs >= FILE_TRANSFER_RATE_WINDOW_MS) {
|
|
316
|
-
const rateWindowBytes = Math.max(transferredBytes - rateWindowStartedBytes, 0);
|
|
317
|
-
rateBytesPerSecond = Math.round((rateWindowBytes / rateWindowElapsedMs) * 1000);
|
|
318
|
-
rateWindowStartedAt = now;
|
|
319
|
-
rateWindowStartedBytes = transferredBytes;
|
|
320
|
-
} else if (rateBytesPerSecond === undefined && elapsedMs > 0) {
|
|
321
|
-
rateBytesPerSecond = Math.round((transferredBytes / elapsedMs) * 1000);
|
|
601
|
+
throw error;
|
|
322
602
|
}
|
|
603
|
+
options.throwIfAborted?.();
|
|
604
|
+
retryCount += 1;
|
|
605
|
+
Log.log(
|
|
606
|
+
`[FileWrite] retry=${retryCount}/${maxChunkRetries} restarting window=${fileWriteWindow} after reconnect path=${options.path}`
|
|
607
|
+
);
|
|
323
608
|
options.onProgress?.({
|
|
324
|
-
progress:
|
|
325
|
-
transferredBytes,
|
|
609
|
+
progress: 0,
|
|
610
|
+
transferredBytes: 0,
|
|
326
611
|
totalBytes: dataLength,
|
|
327
|
-
|
|
328
|
-
|
|
612
|
+
elapsedMs: 0,
|
|
613
|
+
retryCount,
|
|
329
614
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
if (options.paceMs && options.paceMs > 0) {
|
|
349
|
-
await new Promise(resolve => {
|
|
350
|
-
setTimeout(resolve, options.paceMs);
|
|
351
|
-
});
|
|
352
|
-
}
|
|
615
|
+
// The timed-out link is invalid. Only a verified reconnect may restart this
|
|
616
|
+
// fixed-path overwriting upload; never replay an uncertain in-flight chunk.
|
|
617
|
+
commands = await canRecoverPipeline();
|
|
618
|
+
options.throwIfAborted?.();
|
|
619
|
+
written = 0;
|
|
620
|
+
chunks = 0;
|
|
621
|
+
lastMessage = undefined;
|
|
622
|
+
startTime = Date.now();
|
|
623
|
+
lastConfirmedAt = startTime;
|
|
624
|
+
rateWindowStartedAt = startTime;
|
|
625
|
+
rateWindowStartedBytes = 0;
|
|
626
|
+
rateBytesPerSecond = undefined;
|
|
627
|
+
logWindowStartedAt = startTime;
|
|
628
|
+
logWindowStartedBytes = 0;
|
|
629
|
+
hostWriteElapsedMs = 0;
|
|
630
|
+
responseWaitElapsedMs = 0;
|
|
631
|
+
measuredAttempts = 0;
|
|
353
632
|
}
|
|
354
|
-
} catch (error) {
|
|
355
|
-
const now = Date.now();
|
|
356
|
-
const elapsedMs = Math.max(now - startTime, 0);
|
|
357
|
-
const logWindowElapsedMs = Math.max(now - logWindowStartedAt, 0);
|
|
358
|
-
const logWindowBytes = Math.max(written - logWindowStartedBytes, 0);
|
|
359
|
-
logFileTransferMetrics({
|
|
360
|
-
transport,
|
|
361
|
-
status: 'failed',
|
|
362
|
-
path: options.path,
|
|
363
|
-
transferredBytes: written,
|
|
364
|
-
totalBytes: dataLength,
|
|
365
|
-
elapsedMs,
|
|
366
|
-
rateBytesPerSecond: getAverageFileTransferRate(logWindowBytes, logWindowElapsedMs),
|
|
367
|
-
hostWriteElapsedMs,
|
|
368
|
-
responseWaitElapsedMs,
|
|
369
|
-
measuredAttempts,
|
|
370
|
-
});
|
|
371
|
-
throw error;
|
|
372
633
|
}
|
|
373
634
|
|
|
374
635
|
const elapsedMs = Math.max(lastConfirmedAt - startTime, 0);
|
|
@@ -385,6 +646,9 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
|
|
|
385
646
|
hostWriteElapsedMs,
|
|
386
647
|
responseWaitElapsedMs,
|
|
387
648
|
measuredAttempts,
|
|
649
|
+
fileWriteWindow,
|
|
650
|
+
chunkSize,
|
|
651
|
+
retryCount,
|
|
388
652
|
});
|
|
389
653
|
|
|
390
654
|
return {
|
package/src/api/index.ts
CHANGED
|
@@ -175,3 +175,10 @@ export { default as benfenSignTransaction } from './benfen/BenfenSignTransaction
|
|
|
175
175
|
|
|
176
176
|
export { default as neoGetAddress } from './neo/NeoGetAddress';
|
|
177
177
|
export { default as neoSignTransaction } from './neo/NeoSignTransaction';
|
|
178
|
+
export { default as zcashGetAddress } from './zcash/ZcashGetAddress';
|
|
179
|
+
export { default as zcashSignPczt } from './zcash/ZcashSignPczt';
|
|
180
|
+
|
|
181
|
+
export { default as moneroGetAddress } from './monero/MoneroGetAddress';
|
|
182
|
+
export { default as moneroGetWatchKey } from './monero/MoneroGetWatchKey';
|
|
183
|
+
export { default as moneroKeyImageSync } from './monero/MoneroKeyImageSync';
|
|
184
|
+
export { default as moneroSignTransaction } from './monero/MoneroSignTransaction';
|