@onekeyfe/hd-core 1.2.8 → 1.2.10-alpha.2

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