@onekeyfe/hd-transport 1.2.0-alpha.20 → 1.2.0-alpha.22

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 (35) hide show
  1. package/__tests__/messages.test.js +32 -7
  2. package/__tests__/protocol-v2-link-manager.test.js +27 -34
  3. package/__tests__/protocol-v2.test.js +242 -56
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/index.d.ts +101 -19
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +166 -106
  9. package/dist/protocols/index.d.ts +5 -2
  10. package/dist/protocols/index.d.ts.map +1 -1
  11. package/dist/protocols/v2/decode.d.ts +11 -1
  12. package/dist/protocols/v2/decode.d.ts.map +1 -1
  13. package/dist/protocols/v2/errors.d.ts +8 -0
  14. package/dist/protocols/v2/errors.d.ts.map +1 -0
  15. package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
  16. package/dist/protocols/v2/index.d.ts +1 -0
  17. package/dist/protocols/v2/index.d.ts.map +1 -1
  18. package/dist/protocols/v2/link-manager.d.ts +0 -1
  19. package/dist/protocols/v2/link-manager.d.ts.map +1 -1
  20. package/dist/protocols/v2/session.d.ts +4 -5
  21. package/dist/protocols/v2/session.d.ts.map +1 -1
  22. package/dist/types/messages.d.ts +34 -5
  23. package/dist/types/messages.d.ts.map +1 -1
  24. package/messages-protocol-v2.json +77 -4
  25. package/package.json +2 -2
  26. package/scripts/protobuf-types.js +7 -1
  27. package/src/constants.ts +8 -0
  28. package/src/protocols/index.ts +18 -4
  29. package/src/protocols/v2/decode.ts +49 -19
  30. package/src/protocols/v2/errors.ts +23 -0
  31. package/src/protocols/v2/frame-assembler.ts +6 -4
  32. package/src/protocols/v2/index.ts +1 -0
  33. package/src/protocols/v2/link-manager.ts +0 -2
  34. package/src/protocols/v2/session.ts +124 -122
  35. package/src/types/messages.ts +46 -6
@@ -1,6 +1,10 @@
1
- import { PROTOCOL_V2_PACKET_SRC_COMMAND } from '../../constants';
1
+ import {
2
+ PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
3
+ PROTOCOL_V2_PACKET_SRC_COMMAND,
4
+ } from '../../constants';
2
5
  import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
3
6
  import { ProtocolV2SequenceCursor } from './sequence-cursor';
7
+ import { ProtocolV2LinkError } from './errors';
4
8
  import { ProtocolV2 } from '..';
5
9
  import * as check from '../../utils/highlevel-checks';
6
10
  import { LogBlockCommand } from '../../utils/logBlockCommand';
@@ -8,6 +12,8 @@ import { LogBlockCommand } from '../../utils/logBlockCommand';
8
12
  import type { Root } from 'protobufjs/light';
9
13
  import type { MessageFromOneKey } from '../../types';
10
14
 
15
+ export * from './errors';
16
+
11
17
  export type ProtocolV2Schemas = {
12
18
  protocolV1: Root;
13
19
  protocolV2: Root;
@@ -18,6 +24,7 @@ export type ProtocolV2CallContext = {
18
24
  timeoutMs?: number;
19
25
  highVolume: boolean;
20
26
  generation: number;
27
+ signal: AbortSignal;
21
28
  };
22
29
 
23
30
  type ProtocolLogger = {
@@ -38,8 +45,6 @@ export type ProtocolV2SessionOptions = {
38
45
  createTimeoutError?: (name: string, timeoutMs: number) => Error;
39
46
  sequenceCursor?: ProtocolV2SequenceCursor;
40
47
  generation?: number;
41
- writeTimeoutMs?: number;
42
- deliveryTimeoutMs?: number;
43
48
  };
44
49
 
45
50
  export type ProtocolV2CallOptions = {
@@ -116,11 +121,13 @@ export async function withProtocolTimeout<T>(
116
121
  promise: Promise<T>,
117
122
  timeoutMs: number | undefined,
118
123
  createTimeoutError: () => Error,
119
- onTimeout?: () => void
124
+ onTimeout?: () => void,
125
+ abortSignal?: AbortSignal
120
126
  ): Promise<T> {
121
127
  if (!timeoutMs) return promise;
122
128
 
123
129
  let timer: ReturnType<typeof setTimeout> | undefined;
130
+ let abortHandler: (() => void) | undefined;
124
131
  try {
125
132
  return await Promise.race([
126
133
  promise,
@@ -132,18 +139,29 @@ export async function withProtocolTimeout<T>(
132
139
  reject(createTimeoutError());
133
140
  }, timeoutMs);
134
141
  }),
142
+ ...(abortSignal
143
+ ? [
144
+ new Promise<never>((_, reject) => {
145
+ abortHandler = () => {
146
+ reject(new Error('Protocol V2 operation aborted'));
147
+ };
148
+ if (abortSignal.aborted) {
149
+ abortHandler();
150
+ } else {
151
+ abortSignal.addEventListener('abort', abortHandler, { once: true });
152
+ }
153
+ }),
154
+ ]
155
+ : []),
135
156
  ]);
136
157
  } finally {
137
158
  if (timer) clearTimeout(timer);
159
+ if (abortHandler) {
160
+ abortSignal?.removeEventListener('abort', abortHandler);
161
+ }
138
162
  }
139
163
  }
140
164
 
141
- // A transport write must never keep the serialized device queue pending forever.
142
- // Callers must classify a timeout as link-fatal and reset the physical adapter so
143
- // the losing Promise cannot continue on the next connection generation.
144
- export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 30_000;
145
- export const PROTOCOL_V2_DELIVERY_WATCHDOG_TIMEOUT_MS = 5_000;
146
-
147
165
  export class ProtocolV2Session {
148
166
  private readonly options: ProtocolV2SessionOptions;
149
167
 
@@ -153,6 +171,8 @@ export class ProtocolV2Session {
153
171
  // in-flight calls on the same session would steal each other's responses.
154
172
  private pendingCall: Promise<unknown> = Promise.resolve();
155
173
 
174
+ private lastResponseSequence?: number;
175
+
156
176
  constructor(options: ProtocolV2SessionOptions) {
157
177
  this.options = options;
158
178
  this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
@@ -171,7 +191,7 @@ export class ProtocolV2Session {
171
191
  return result;
172
192
  }
173
193
 
174
- private async executeCall(
194
+ private executeCall(
175
195
  name: string,
176
196
  data: Record<string, unknown>,
177
197
  callOptions: ProtocolV2CallOptions
@@ -188,101 +208,106 @@ export class ProtocolV2Session {
188
208
  logPrefix = 'ProtocolV2',
189
209
  createTimeoutError,
190
210
  generation = 0,
191
- writeTimeoutMs = PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
192
- deliveryTimeoutMs = PROTOCOL_V2_DELIVERY_WATCHDOG_TIMEOUT_MS,
193
211
  } = this.options;
212
+ const timeoutMs =
213
+ callOptions.timeoutMs && callOptions.timeoutMs > 0
214
+ ? callOptions.timeoutMs
215
+ : PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
216
+ const abortController = new AbortController();
194
217
 
195
218
  const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
196
- const callContext: ProtocolV2CallContext = {
219
+ const baseCallContext: ProtocolV2CallContext = {
197
220
  messageName: name,
198
- timeoutMs: callOptions.timeoutMs,
221
+ timeoutMs,
199
222
  highVolume: shouldReduceDebug,
200
223
  generation,
224
+ signal: abortController.signal,
201
225
  };
202
- await prepareCall?.(callContext);
203
- const protoSeq = this.sequenceCursor.next();
204
- const frame = ProtocolV2.encodeFrame(schemas, name, data, {
205
- packetSrc,
206
- router,
207
- seq: protoSeq,
208
- });
209
- // Suppress Protocol V2 TX frame logs to avoid excessive transport diagnostics.
210
- /*
211
- const txMetadata = ProtocolV2.inspectFrame(schemas, frame);
212
-
213
- if (!shouldReduceDebug) {
214
- logger?.debug?.(`[${logPrefix}] TX`, {
215
- method: name,
216
- type: name,
217
- typeId: txMetadata.messageTypeId,
218
- seq: txMetadata.seq,
219
- bytes: frame.length,
226
+
227
+ const runCall = async (): Promise<MessageFromOneKey> => {
228
+ await prepareCall?.(baseCallContext);
229
+ const protoSeq = this.sequenceCursor.next();
230
+ const frame = ProtocolV2.encodeFrame(schemas, name, data, {
231
+ packetSrc,
232
+ router,
233
+ seq: protoSeq,
220
234
  });
221
- }
222
- */
223
235
 
224
- if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
225
- throw new Error(
226
- `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
227
- );
228
- }
236
+ if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
237
+ throw new Error(
238
+ `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
239
+ );
240
+ }
229
241
 
230
- await withProtocolTimeout(
231
- writeFrame(frame, callContext),
232
- writeTimeoutMs,
233
- () => new Error(`Protocol V2 write timeout after ${writeTimeoutMs}ms for ${name}`)
234
- );
242
+ await writeFrame(frame, baseCallContext);
235
243
 
236
- // Cancellation flag for the read loop: when the response timeout fires,
237
- // Promise.race alone would leave this loop running as a zombie that keeps
238
- // consuming frames meant for the next call. The timeout callback flips the
239
- // flag so the loop exits and discards any late frame.
240
- const cancellation = { cancelled: false };
241
- let resolveDelivery: () => void = () => undefined;
242
- let deliveryConfirmed = false;
243
- const deliveryPromise = new Promise<void>(resolve => {
244
- resolveDelivery = () => {
245
- if (deliveryConfirmed) return;
246
- deliveryConfirmed = true;
247
- resolve();
248
- };
249
- });
250
-
251
- const readResponse = async (): Promise<MessageFromOneKey> => {
252
244
  // Some Protocol V2 operations emit progress notifications before the
253
245
  // terminal response. Consume those frames here so callers still see a
254
246
  // request/terminal-response shaped API.
255
- while (!cancellation.cancelled) {
256
- const rxFrame = await readFrame(callContext);
257
- if (cancellation.cancelled) {
258
- // Timed out while waiting: drop the late frame and stop reading.
259
- break;
247
+ while (!abortController.signal.aborted) {
248
+ const rxFrame = await readFrame(baseCallContext);
249
+ if (abortController.signal.aborted) break;
250
+
251
+ let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
252
+ try {
253
+ header = ProtocolV2.inspectFrameHeader(rxFrame);
254
+ } catch (cause) {
255
+ throw new ProtocolV2LinkError(
256
+ 'frame',
257
+ `Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
258
+ cause
259
+ );
260
+ }
261
+ if (header.router !== router) {
262
+ throw new ProtocolV2LinkError(
263
+ 'router',
264
+ `Protocol V2 router mismatch: expected ${router}, got ${header.router}`
265
+ );
266
+ }
267
+ if (header.packetSrc !== packetSrc) {
268
+ throw new ProtocolV2LinkError(
269
+ 'packet-source',
270
+ `Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
271
+ );
272
+ }
273
+
274
+ let isAck: boolean;
275
+ try {
276
+ isAck = ProtocolV2.isAckFrame(rxFrame);
277
+ } catch (cause) {
278
+ throw new ProtocolV2LinkError(
279
+ 'frame',
280
+ `Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
281
+ cause
282
+ );
260
283
  }
261
- const ackSequence = ProtocolV2.getAckSequence(rxFrame);
262
- if (ackSequence !== undefined) {
263
- if (ackSequence === protoSeq) {
264
- resolveDelivery();
284
+ if (isAck) {
285
+ if (header.seq !== protoSeq) {
286
+ throw new ProtocolV2LinkError(
287
+ 'ack-sequence',
288
+ `Protocol V2 ACK sequence mismatch: expected ${protoSeq}, got ${header.seq}`
289
+ );
265
290
  }
266
291
  } else {
267
- // Some firmware versions return the protobuf response without a separate
268
- // ACK. Receiving any valid response frame proves that the request arrived.
269
- resolveDelivery();
270
- // Suppress Protocol V2 RX frame logs to avoid excessive transport diagnostics.
271
- /*
272
- const rxMetadata = ProtocolV2.inspectFrame(schemas, rxFrame);
273
-
274
- if (!shouldReduceDebug) {
275
- logger?.debug?.(`[${logPrefix}] RX`, {
276
- method: name,
277
- type: rxMetadata.type,
278
- typeId: rxMetadata.messageTypeId,
279
- seq: rxMetadata.seq,
280
- bytes: rxFrame.length,
281
- });
292
+ let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
293
+ try {
294
+ decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
295
+ } catch (cause) {
296
+ throw new ProtocolV2LinkError(
297
+ 'frame',
298
+ `Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
299
+ cause
300
+ );
282
301
  }
283
- */
284
-
285
- const decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
302
+ // Firmware owns one TX sequence across all channels and packet sources,
303
+ // so frames routed elsewhere create valid gaps in this session.
304
+ if (this.lastResponseSequence === decoded.seq) {
305
+ throw new ProtocolV2LinkError(
306
+ 'response-sequence',
307
+ `Protocol V2 duplicate response sequence: ${decoded.seq}`
308
+ );
309
+ }
310
+ this.lastResponseSequence = decoded.seq;
286
311
 
287
312
  const response = check.call(decoded);
288
313
  if (callOptions.intermediateTypes?.includes(response.type)) {
@@ -298,47 +323,21 @@ export class ProtocolV2Session {
298
323
  }
299
324
  }
300
325
  }
301
- // Only reachable after cancellation; the outer promise has already been
302
- // rejected by the timeout, so this rejection is consumed by the race.
326
+
303
327
  throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
304
328
  };
305
329
 
306
- const responsePromise = withProtocolTimeout(
307
- readResponse(),
308
- callOptions.timeoutMs,
330
+ return withProtocolTimeout(
331
+ runCall(),
332
+ timeoutMs,
309
333
  () =>
310
334
  createTimeoutError
311
- ? createTimeoutError(name, callOptions.timeoutMs ?? 0)
312
- : new Error(`Protocol V2 response timeout after ${callOptions.timeoutMs}ms for ${name}`),
335
+ ? createTimeoutError(name, timeoutMs)
336
+ : new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
313
337
  () => {
314
- cancellation.cancelled = true;
338
+ abortController.abort();
315
339
  }
316
340
  );
317
- const deliverySignal = Promise.race([deliveryPromise, responsePromise.then(() => undefined)]);
318
- const deliveryWatchdog = withProtocolTimeout(
319
- deliverySignal,
320
- deliveryTimeoutMs,
321
- () => new Error(`Protocol V2 delivery timeout after ${deliveryTimeoutMs}ms for ${name}`),
322
- () => {
323
- cancellation.cancelled = true;
324
- }
325
- );
326
-
327
- try {
328
- const firstResult = await Promise.race([
329
- deliveryWatchdog.then(() => ({ kind: 'delivered' as const })),
330
- responsePromise.then(response => ({ kind: 'response' as const, response })),
331
- ]);
332
- if (firstResult.kind === 'response') {
333
- return firstResult.response;
334
- }
335
- return await responsePromise;
336
- } catch (error) {
337
- cancellation.cancelled = true;
338
- deliveryWatchdog.catch(() => undefined);
339
- responsePromise.catch(() => undefined);
340
- throw error;
341
- }
342
341
  }
343
342
  }
344
343
 
@@ -367,7 +366,10 @@ export async function probeProtocolV2({
367
366
  const response = await call(
368
367
  'Ping',
369
368
  { message: 'protocol-v2-probe' },
370
- { timeoutMs, expectedTypes: ['Success'] }
369
+ {
370
+ timeoutMs,
371
+ expectedTypes: ['Success'],
372
+ }
371
373
  );
372
374
  if (response.type === 'Success') {
373
375
  return true;
@@ -2359,7 +2359,7 @@ export enum Enum_SafetyCheckLevel {
2359
2359
  PromptAlways = 1,
2360
2360
  PromptTemporarily = 2,
2361
2361
  }
2362
- export type SafetyCheckLevel = keyof typeof Enum_SafetyCheckLevel;
2362
+ export type SafetyCheckLevel = keyof typeof Enum_SafetyCheckLevel | Enum_SafetyCheckLevel;
2363
2363
 
2364
2364
  // Initialize
2365
2365
  export type Initialize = {
@@ -4744,7 +4744,9 @@ export type ViewVerifyPage = {
4744
4744
  };
4745
4745
 
4746
4746
  // ProtocolInfoRequest
4747
- export type ProtocolInfoRequest = {};
4747
+ export type ProtocolInfoRequest = {
4748
+ eventless_wallet_session?: boolean;
4749
+ };
4748
4750
 
4749
4751
  // ProtocolInfo
4750
4752
  export type ProtocolInfo = {
@@ -5049,9 +5051,42 @@ export type ProtocolV2DeviceInfo = {
5049
5051
  status?: DeviceStatus;
5050
5052
  };
5051
5053
 
5052
- // DeviceSessionGet
5053
- export type DeviceSessionGet = {
5054
- session_id?: string;
5054
+ export enum DeviceSessionErrorCode {
5055
+ DeviceSessionError_None = 0,
5056
+ DeviceSessionError_UserCancelled = 1,
5057
+ DeviceSessionError_InvalidSession = 2,
5058
+ DeviceSessionError_AttachPinUnavailable = 3,
5059
+ DeviceSessionError_PassphraseDisabled = 4,
5060
+ DeviceSessionError_Busy = 5,
5061
+ }
5062
+
5063
+ // DeviceSessionResume
5064
+ export type DeviceSessionResume = {
5065
+ session_id: string;
5066
+ };
5067
+
5068
+ // DeviceSessionHostPassphrase
5069
+ export type DeviceSessionHostPassphrase = {
5070
+ passphrase: string;
5071
+ };
5072
+
5073
+ // DeviceSessionPassphraseOnDevice
5074
+ export type DeviceSessionPassphraseOnDevice = {};
5075
+
5076
+ // DeviceSessionAttachPinOnDevice
5077
+ export type DeviceSessionAttachPinOnDevice = {};
5078
+
5079
+ // DeviceSessionSelect
5080
+ export type DeviceSessionSelect = {
5081
+ host_passphrase?: DeviceSessionHostPassphrase;
5082
+ passphrase_on_device?: DeviceSessionPassphraseOnDevice;
5083
+ attach_pin_on_device?: DeviceSessionAttachPinOnDevice;
5084
+ };
5085
+
5086
+ // DeviceSessionOpen
5087
+ export type DeviceSessionOpen = {
5088
+ resume?: DeviceSessionResume;
5089
+ select?: DeviceSessionSelect;
5055
5090
  };
5056
5091
 
5057
5092
  // DeviceSession
@@ -5881,7 +5916,12 @@ export type MessageType = {
5881
5916
  DeviceInfoTargets: DeviceInfoTargets;
5882
5917
  DeviceInfoTypes: DeviceInfoTypes;
5883
5918
  DeviceInfoGet: DeviceInfoGet;
5884
- DeviceSessionGet: DeviceSessionGet;
5919
+ DeviceSessionResume: DeviceSessionResume;
5920
+ DeviceSessionHostPassphrase: DeviceSessionHostPassphrase;
5921
+ DeviceSessionPassphraseOnDevice: DeviceSessionPassphraseOnDevice;
5922
+ DeviceSessionAttachPinOnDevice: DeviceSessionAttachPinOnDevice;
5923
+ DeviceSessionSelect: DeviceSessionSelect;
5924
+ DeviceSessionOpen: DeviceSessionOpen;
5885
5925
  DeviceSession: DeviceSession;
5886
5926
  DeviceSessionAskPin: DeviceSessionAskPin;
5887
5927
  DeviceStatus: DeviceStatus;