@onekeyfe/hd-transport 1.2.8-alpha.9 → 1.2.9
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__/babylon-context-hash.test.js +44 -0
- package/__tests__/protocol-v2-file-write-pipeline.test.js +478 -0
- package/__tests__/transport-log.test.js +176 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +157 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +253 -72
- package/dist/protocols/v2/session.d.ts +7 -1
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/types/messages.d.ts +72 -0
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +6 -0
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/transportLog.d.ts.map +1 -1
- package/messages-protocol-v2.json +152 -0
- package/package.json +2 -2
- package/scripts/protobuf-build.sh +30 -0
- package/scripts/protobuf-types.js +1 -0
- package/src/constants.ts +10 -1
- package/src/index.ts +1 -0
- package/src/protocols/v2/session.ts +326 -96
- package/src/types/messages.ts +97 -0
- package/src/types/transport.ts +25 -0
- package/src/utils/transportLog.ts +17 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
3
|
+
PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW,
|
|
3
4
|
PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
4
5
|
} from '../../constants';
|
|
5
6
|
import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
|
|
@@ -9,13 +10,18 @@ import {
|
|
|
9
10
|
createProtocolV2LinkDisabledError,
|
|
10
11
|
isProtocolV2LinkDisabledError,
|
|
11
12
|
isProtocolV2LinkDisabledFailure,
|
|
13
|
+
isProtocolV2LinkError,
|
|
12
14
|
} from './errors';
|
|
13
15
|
import { ProtocolV2 } from '..';
|
|
14
16
|
import * as check from '../../utils/highlevel-checks';
|
|
15
17
|
import { LogBlockCommand } from '../../utils/logBlockCommand';
|
|
16
18
|
|
|
17
19
|
import type { Root } from 'protobufjs/light';
|
|
18
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
MessageFromOneKey,
|
|
22
|
+
ProtocolV2FileWritePipeline,
|
|
23
|
+
TransportWriteMetrics,
|
|
24
|
+
} from '../../types';
|
|
19
25
|
|
|
20
26
|
export * from './errors';
|
|
21
27
|
|
|
@@ -62,6 +68,7 @@ export type ProtocolV2CallOptions = {
|
|
|
62
68
|
returnAfterWrite?: boolean;
|
|
63
69
|
onResponseAfterWrite?: (response: MessageFromOneKey) => void;
|
|
64
70
|
writeWithResponse?: boolean;
|
|
71
|
+
fileWritePipeline?: ProtocolV2FileWritePipeline;
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
export { concatUint8Arrays, ProtocolV2FrameAssembler };
|
|
@@ -214,6 +221,30 @@ export async function withProtocolTimeout<T>(
|
|
|
214
221
|
}
|
|
215
222
|
}
|
|
216
223
|
|
|
224
|
+
const resolveProtocolV2CallTimeoutMs = (timeoutMs?: number) =>
|
|
225
|
+
timeoutMs && timeoutMs > 0 ? timeoutMs : PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
|
|
226
|
+
|
|
227
|
+
function parseFileWriteChunk(request: Record<string, unknown>) {
|
|
228
|
+
const file = request.file as
|
|
229
|
+
| { path?: unknown; offset?: unknown; total_size?: unknown; data?: { byteLength?: unknown } }
|
|
230
|
+
| undefined;
|
|
231
|
+
const offset = Number(file?.offset);
|
|
232
|
+
const totalSize = Number(file?.total_size);
|
|
233
|
+
const length = Number(file?.data?.byteLength);
|
|
234
|
+
if (
|
|
235
|
+
typeof file?.path !== 'string' ||
|
|
236
|
+
!Number.isSafeInteger(offset) ||
|
|
237
|
+
offset < 0 ||
|
|
238
|
+
!Number.isSafeInteger(totalSize) ||
|
|
239
|
+
!Number.isSafeInteger(length) ||
|
|
240
|
+
length <= 0 ||
|
|
241
|
+
offset + length > totalSize
|
|
242
|
+
) {
|
|
243
|
+
throw new Error('Protocol V2 file write pipeline chunk is invalid');
|
|
244
|
+
}
|
|
245
|
+
return { path: file.path, offset, end: offset + length, totalSize };
|
|
246
|
+
}
|
|
247
|
+
|
|
217
248
|
export class ProtocolV2Session {
|
|
218
249
|
private readonly options: ProtocolV2SessionOptions;
|
|
219
250
|
|
|
@@ -293,28 +324,46 @@ export class ProtocolV2Session {
|
|
|
293
324
|
return result;
|
|
294
325
|
}
|
|
295
326
|
|
|
327
|
+
private reportWriteCompleted(
|
|
328
|
+
callOptions: ProtocolV2CallOptions,
|
|
329
|
+
writeStartedAt: number,
|
|
330
|
+
frameBytes: number
|
|
331
|
+
) {
|
|
332
|
+
try {
|
|
333
|
+
callOptions.onWriteCompleted?.({
|
|
334
|
+
elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
|
|
335
|
+
frameBytes,
|
|
336
|
+
});
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const { logger, logPrefix = 'ProtocolV2' } = this.options;
|
|
339
|
+
logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private createCallTimeoutError(name: string, timeoutMs: number): Error {
|
|
344
|
+
const { createTimeoutError } = this.options;
|
|
345
|
+
return createTimeoutError
|
|
346
|
+
? createTimeoutError(name, timeoutMs)
|
|
347
|
+
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
296
350
|
private executeCall(
|
|
297
351
|
name: string,
|
|
298
352
|
data: Record<string, unknown>,
|
|
299
353
|
callOptions: ProtocolV2CallOptions
|
|
300
354
|
): Promise<MessageFromOneKey> {
|
|
355
|
+
if (callOptions.fileWritePipeline) {
|
|
356
|
+
return this.executeFileWritePipeline(name, data, callOptions, callOptions.fileWritePipeline);
|
|
357
|
+
}
|
|
301
358
|
const {
|
|
302
|
-
schemas,
|
|
303
|
-
router,
|
|
304
|
-
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
305
|
-
maxFrameBytes,
|
|
306
359
|
prepareCall,
|
|
307
360
|
writeFrame,
|
|
308
361
|
readFrame,
|
|
309
362
|
logger,
|
|
310
363
|
logPrefix = 'ProtocolV2',
|
|
311
|
-
createTimeoutError,
|
|
312
364
|
generation = 0,
|
|
313
365
|
} = this.options;
|
|
314
|
-
const timeoutMs =
|
|
315
|
-
callOptions.timeoutMs && callOptions.timeoutMs > 0
|
|
316
|
-
? callOptions.timeoutMs
|
|
317
|
-
: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
|
|
366
|
+
const timeoutMs = resolveProtocolV2CallTimeoutMs(callOptions.timeoutMs);
|
|
318
367
|
const abortController = new AbortController();
|
|
319
368
|
|
|
320
369
|
const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
|
|
@@ -335,29 +384,11 @@ export class ProtocolV2Session {
|
|
|
335
384
|
if (!this.pendingResponseAfterWrite) {
|
|
336
385
|
await prepareCall?.(baseCallContext);
|
|
337
386
|
}
|
|
338
|
-
const protoSeq = this.
|
|
339
|
-
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
340
|
-
packetSrc,
|
|
341
|
-
router,
|
|
342
|
-
seq: protoSeq,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
346
|
-
throw new Error(
|
|
347
|
-
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
348
|
-
);
|
|
349
|
-
}
|
|
387
|
+
const { seq: protoSeq, frame } = this.encodeRequestFrame(name, data);
|
|
350
388
|
|
|
351
389
|
const writeStartedAt = Date.now();
|
|
352
390
|
await this.serializeWrite(() => writeFrame(frame, baseCallContext));
|
|
353
|
-
|
|
354
|
-
callOptions.onWriteCompleted?.({
|
|
355
|
-
elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
|
|
356
|
-
frameBytes: frame.byteLength,
|
|
357
|
-
});
|
|
358
|
-
} catch (error) {
|
|
359
|
-
logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
|
|
360
|
-
}
|
|
391
|
+
this.reportWriteCompleted(callOptions, writeStartedAt, frame.byteLength);
|
|
361
392
|
|
|
362
393
|
if (callOptions.returnAfterWrite) {
|
|
363
394
|
if (callOptions.expectedTypes?.length) {
|
|
@@ -376,68 +407,8 @@ export class ProtocolV2Session {
|
|
|
376
407
|
const rxFrame = await readFrame(baseCallContext);
|
|
377
408
|
if (abortController.signal.aborted) break;
|
|
378
409
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
header = ProtocolV2.inspectFrameHeader(rxFrame);
|
|
382
|
-
} catch (cause) {
|
|
383
|
-
throw new ProtocolV2LinkError(
|
|
384
|
-
'frame',
|
|
385
|
-
`Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
|
|
386
|
-
cause
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
if (header.router !== router) {
|
|
390
|
-
throw new ProtocolV2LinkError(
|
|
391
|
-
'router',
|
|
392
|
-
`Protocol V2 router mismatch: expected ${router}, got ${header.router}`
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
if (header.packetSrc !== packetSrc) {
|
|
396
|
-
throw new ProtocolV2LinkError(
|
|
397
|
-
'packet-source',
|
|
398
|
-
`Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
let isAck: boolean;
|
|
403
|
-
try {
|
|
404
|
-
isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
405
|
-
} catch (cause) {
|
|
406
|
-
throw new ProtocolV2LinkError(
|
|
407
|
-
'frame',
|
|
408
|
-
`Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
|
|
409
|
-
cause
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
if (isAck) {
|
|
413
|
-
if (header.seq !== protoSeq) {
|
|
414
|
-
throw new ProtocolV2LinkError(
|
|
415
|
-
'ack-sequence',
|
|
416
|
-
`Protocol V2 ACK sequence mismatch: expected ${protoSeq}, got ${header.seq}`
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
} else {
|
|
420
|
-
let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
|
|
421
|
-
try {
|
|
422
|
-
decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
|
|
423
|
-
} catch (cause) {
|
|
424
|
-
throw new ProtocolV2LinkError(
|
|
425
|
-
'frame',
|
|
426
|
-
`Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
|
|
427
|
-
cause
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
// Firmware owns one TX sequence across all channels and packet sources,
|
|
431
|
-
// so frames routed elsewhere create valid gaps in this session.
|
|
432
|
-
if (this.lastResponseSequence === decoded.seq) {
|
|
433
|
-
throw new ProtocolV2LinkError(
|
|
434
|
-
'response-sequence',
|
|
435
|
-
`Protocol V2 duplicate response sequence: ${decoded.seq}`
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
this.lastResponseSequence = decoded.seq;
|
|
439
|
-
|
|
440
|
-
const response = check.call(decoded);
|
|
410
|
+
const response = this.parseResponseFrame(rxFrame, [protoSeq]);
|
|
411
|
+
if (response) {
|
|
441
412
|
const { pendingResponseAfterWrite } = this;
|
|
442
413
|
let belongsToWriteOnlyCall = false;
|
|
443
414
|
if (pendingResponseAfterWrite) {
|
|
@@ -477,15 +448,274 @@ export class ProtocolV2Session {
|
|
|
477
448
|
return withProtocolTimeout(
|
|
478
449
|
runCall(),
|
|
479
450
|
timeoutMs,
|
|
480
|
-
() =>
|
|
481
|
-
createTimeoutError
|
|
482
|
-
? createTimeoutError(name, timeoutMs)
|
|
483
|
-
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
|
|
451
|
+
() => this.createCallTimeoutError(name, timeoutMs),
|
|
484
452
|
() => {
|
|
485
453
|
abortController.abort();
|
|
486
454
|
}
|
|
487
455
|
);
|
|
488
456
|
}
|
|
457
|
+
|
|
458
|
+
private encodeRequestFrame(name: string, data: Record<string, unknown>) {
|
|
459
|
+
const {
|
|
460
|
+
schemas,
|
|
461
|
+
router,
|
|
462
|
+
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
463
|
+
maxFrameBytes,
|
|
464
|
+
} = this.options;
|
|
465
|
+
const seq = this.sequenceCursor.next();
|
|
466
|
+
const frame = ProtocolV2.encodeFrame(schemas, name, data, { packetSrc, router, seq });
|
|
467
|
+
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
468
|
+
throw new Error(
|
|
469
|
+
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return { seq, frame };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Validates one received frame for this session. Returns undefined for an ACK of one of
|
|
477
|
+
* `ackSequences`, otherwise the decoded response.
|
|
478
|
+
*/
|
|
479
|
+
private parseResponseFrame(
|
|
480
|
+
rxFrame: Uint8Array,
|
|
481
|
+
ackSequences: readonly number[]
|
|
482
|
+
): MessageFromOneKey | undefined {
|
|
483
|
+
const { schemas, router, packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND } = this.options;
|
|
484
|
+
let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
|
|
485
|
+
try {
|
|
486
|
+
header = ProtocolV2.inspectFrameHeader(rxFrame);
|
|
487
|
+
} catch (cause) {
|
|
488
|
+
throw new ProtocolV2LinkError(
|
|
489
|
+
'frame',
|
|
490
|
+
`Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
|
|
491
|
+
cause
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if (header.router !== router) {
|
|
495
|
+
throw new ProtocolV2LinkError(
|
|
496
|
+
'router',
|
|
497
|
+
`Protocol V2 router mismatch: expected ${router}, got ${header.router}`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (header.packetSrc !== packetSrc) {
|
|
501
|
+
throw new ProtocolV2LinkError(
|
|
502
|
+
'packet-source',
|
|
503
|
+
`Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let isAck: boolean;
|
|
508
|
+
try {
|
|
509
|
+
isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
510
|
+
} catch (cause) {
|
|
511
|
+
throw new ProtocolV2LinkError(
|
|
512
|
+
'frame',
|
|
513
|
+
`Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
|
|
514
|
+
cause
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (isAck) {
|
|
518
|
+
if (!ackSequences.includes(header.seq)) {
|
|
519
|
+
throw new ProtocolV2LinkError(
|
|
520
|
+
'ack-sequence',
|
|
521
|
+
`Protocol V2 ACK sequence mismatch: expected ${ackSequences.join('|')}, got ${header.seq}`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
|
|
528
|
+
try {
|
|
529
|
+
decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
|
|
530
|
+
} catch (cause) {
|
|
531
|
+
throw new ProtocolV2LinkError(
|
|
532
|
+
'frame',
|
|
533
|
+
`Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
|
|
534
|
+
cause
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
// Firmware owns one TX sequence across all channels and packet sources,
|
|
538
|
+
// so frames routed elsewhere create valid gaps in this session.
|
|
539
|
+
if (this.lastResponseSequence === decoded.seq) {
|
|
540
|
+
throw new ProtocolV2LinkError(
|
|
541
|
+
'response-sequence',
|
|
542
|
+
`Protocol V2 duplicate response sequence: ${decoded.seq}`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
this.lastResponseSequence = decoded.seq;
|
|
546
|
+
return check.call(decoded);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Writes one file as ordered FilesystemFileWrite chunks while keeping up to
|
|
551
|
+
* PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW chunks in flight. Firmware still processes and
|
|
552
|
+
* answers every chunk in order, so responses are matched to chunks by position. Upstream
|
|
553
|
+
* bootloaders append at their own cursor without checking the request offset, so the echoed
|
|
554
|
+
* offset is what exposes a lost chunk. A device Failure, or `next` returning undefined, stops
|
|
555
|
+
* sending; the remaining in-flight responses are drained so the link stays usable.
|
|
556
|
+
*/
|
|
557
|
+
private async executeFileWritePipeline(
|
|
558
|
+
name: string,
|
|
559
|
+
data: Record<string, unknown>,
|
|
560
|
+
callOptions: ProtocolV2CallOptions,
|
|
561
|
+
pipeline: ProtocolV2FileWritePipeline
|
|
562
|
+
): Promise<MessageFromOneKey> {
|
|
563
|
+
if (name !== 'FilesystemFileWrite') {
|
|
564
|
+
throw new Error(`Protocol V2 file write pipeline does not support ${name}`);
|
|
565
|
+
}
|
|
566
|
+
if (this.pendingResponseAfterWrite) {
|
|
567
|
+
throw new Error('Protocol V2 file write pipeline cannot start while a response is pending');
|
|
568
|
+
}
|
|
569
|
+
const { prepareCall, writeFrame, readFrame, generation = 0 } = this.options;
|
|
570
|
+
const timeoutMs = resolveProtocolV2CallTimeoutMs(callOptions.timeoutMs);
|
|
571
|
+
const abortController = new AbortController();
|
|
572
|
+
const context: ProtocolV2CallContext = {
|
|
573
|
+
messageName: name,
|
|
574
|
+
timeoutMs,
|
|
575
|
+
highThroughput: true,
|
|
576
|
+
...(callOptions.writeWithResponse === undefined
|
|
577
|
+
? {}
|
|
578
|
+
: { writeWithResponse: callOptions.writeWithResponse }),
|
|
579
|
+
generation,
|
|
580
|
+
signal: abortController.signal,
|
|
581
|
+
};
|
|
582
|
+
// Transport errors and timeouts propagate unchanged: each transport classifies its own.
|
|
583
|
+
let transportFailure: unknown;
|
|
584
|
+
// One file can take far longer than one chunk, so the watchdog bounds each step instead.
|
|
585
|
+
const withStepTimeout = <T>(promise: Promise<T>, stepTimeoutMs = timeoutMs) =>
|
|
586
|
+
withProtocolTimeout(
|
|
587
|
+
promise,
|
|
588
|
+
stepTimeoutMs,
|
|
589
|
+
() => this.createCallTimeoutError(name, timeoutMs),
|
|
590
|
+
() => {
|
|
591
|
+
abortController.abort();
|
|
592
|
+
}
|
|
593
|
+
).catch((error: unknown) => {
|
|
594
|
+
transportFailure = error;
|
|
595
|
+
throw error;
|
|
596
|
+
});
|
|
597
|
+
// ACKs and skipped frames must not extend the wait for an in-flight chunk's response.
|
|
598
|
+
let progressDeadline = 0;
|
|
599
|
+
const markProgress = () => {
|
|
600
|
+
progressDeadline = Date.now() + timeoutMs;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const inFlight: Array<{ seq: number; offset: number; end: number }> = [];
|
|
604
|
+
let path: string | undefined;
|
|
605
|
+
let totalSize: number | undefined;
|
|
606
|
+
let nextOffset: number | undefined;
|
|
607
|
+
let sourceDone = false;
|
|
608
|
+
let failure: MessageFromOneKey | undefined;
|
|
609
|
+
let lastConfirmed: MessageFromOneKey | undefined;
|
|
610
|
+
|
|
611
|
+
const sendChunk = async (request: Record<string, unknown>) => {
|
|
612
|
+
const chunk = parseFileWriteChunk(request);
|
|
613
|
+
if (path !== undefined && (chunk.path !== path || chunk.totalSize !== totalSize)) {
|
|
614
|
+
throw new Error('Protocol V2 file write pipeline chunk belongs to a different file');
|
|
615
|
+
}
|
|
616
|
+
if (nextOffset !== undefined && chunk.offset !== nextOffset) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
`Protocol V2 file write pipeline chunk out of order: expected offset ${nextOffset}, got ${chunk.offset}`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
path = chunk.path;
|
|
622
|
+
totalSize = chunk.totalSize;
|
|
623
|
+
nextOffset = chunk.end;
|
|
624
|
+
|
|
625
|
+
const { seq, frame } = this.encodeRequestFrame(name, request);
|
|
626
|
+
const writeStartedAt = Date.now();
|
|
627
|
+
await withStepTimeout(this.serializeWrite(() => writeFrame(frame, context)));
|
|
628
|
+
inFlight.push({ seq, offset: chunk.offset, end: chunk.end });
|
|
629
|
+
markProgress();
|
|
630
|
+
this.reportWriteCompleted(callOptions, writeStartedAt, frame.byteLength);
|
|
631
|
+
};
|
|
632
|
+
const sendNextChunk = async () => {
|
|
633
|
+
const request = await pipeline.next();
|
|
634
|
+
if (!request) {
|
|
635
|
+
sourceDone = true;
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
await sendChunk(request);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
await withStepTimeout(Promise.resolve().then(() => prepareCall?.(context)));
|
|
642
|
+
try {
|
|
643
|
+
await sendChunk(data);
|
|
644
|
+
while (!sourceDone && inFlight.length < PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW) {
|
|
645
|
+
await sendNextChunk();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
while (inFlight.length > 0) {
|
|
649
|
+
// Floor at 1 ms: withProtocolTimeout treats 0 as "no timeout", so an expired deadline
|
|
650
|
+
// must still time out.
|
|
651
|
+
const rxFrame = await withStepTimeout(
|
|
652
|
+
readFrame(context),
|
|
653
|
+
Math.max(progressDeadline - Date.now(), 1)
|
|
654
|
+
);
|
|
655
|
+
const response = this.parseResponseFrame(
|
|
656
|
+
rxFrame,
|
|
657
|
+
inFlight.map(item => item.seq)
|
|
658
|
+
);
|
|
659
|
+
if (!response) {
|
|
660
|
+
// ACK of an in-flight chunk; its FilesystemFile response follows.
|
|
661
|
+
} else if (response.type === 'FilesystemFile') {
|
|
662
|
+
const [expected] = inFlight;
|
|
663
|
+
const { message } = response;
|
|
664
|
+
if (
|
|
665
|
+
Number(message.processed_byte) !== expected.end ||
|
|
666
|
+
message.path !== path ||
|
|
667
|
+
Number(message.offset) !== expected.offset ||
|
|
668
|
+
Number(message.total_size) !== totalSize
|
|
669
|
+
) {
|
|
670
|
+
throw new ProtocolV2LinkError(
|
|
671
|
+
'frame',
|
|
672
|
+
`Protocol V2 file write response does not match chunk ${expected.offset}-${
|
|
673
|
+
expected.end
|
|
674
|
+
}: got offset ${String(message.offset)} processed_byte ${String(
|
|
675
|
+
message.processed_byte
|
|
676
|
+
)}`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
inFlight.shift();
|
|
680
|
+
markProgress();
|
|
681
|
+
if (!failure) {
|
|
682
|
+
lastConfirmed = response;
|
|
683
|
+
pipeline.onResponse(response);
|
|
684
|
+
if (!sourceDone) {
|
|
685
|
+
await sendNextChunk();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} else if (response.type === 'Failure') {
|
|
689
|
+
inFlight.shift();
|
|
690
|
+
markProgress();
|
|
691
|
+
failure = failure ?? response;
|
|
692
|
+
sourceDone = true;
|
|
693
|
+
} else if (COMMON_TERMINAL_RESPONSE_TYPES.has(response.type)) {
|
|
694
|
+
throw new ProtocolV2LinkError(
|
|
695
|
+
'frame',
|
|
696
|
+
`Protocol V2 file write pipeline received unexpected ${response.type}`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
} catch (error) {
|
|
701
|
+
// Unread responses of in-flight chunks would answer the next call on this link, so a
|
|
702
|
+
// session or caller error raised with chunks in flight must invalidate it.
|
|
703
|
+
if (inFlight.length === 0 || error === transportFailure || isProtocolV2LinkError(error)) {
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
throw new ProtocolV2LinkError(
|
|
707
|
+
'frame',
|
|
708
|
+
`Protocol V2 file write pipeline aborted with chunks in flight: ${getErrorMessage(error)}`,
|
|
709
|
+
error
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const terminal = failure ?? lastConfirmed;
|
|
714
|
+
if (!terminal) {
|
|
715
|
+
throw new ProtocolV2LinkError('frame', 'Protocol V2 file write pipeline ended unconfirmed');
|
|
716
|
+
}
|
|
717
|
+
return terminal;
|
|
718
|
+
}
|
|
489
719
|
}
|
|
490
720
|
|
|
491
721
|
export async function probeProtocolV2({
|
package/src/types/messages.ts
CHANGED
|
@@ -4554,6 +4554,28 @@ export enum CommandFlags {
|
|
|
4554
4554
|
Factory_Only = 1,
|
|
4555
4555
|
}
|
|
4556
4556
|
|
|
4557
|
+
export enum Enum_CanonicalBitcoinNetwork {
|
|
4558
|
+
BITCOIN_MAINNET = 0,
|
|
4559
|
+
BITCOIN_TESTNET = 1,
|
|
4560
|
+
BITCOIN_SIGNET = 2,
|
|
4561
|
+
BITCOIN_REGTEST = 3,
|
|
4562
|
+
}
|
|
4563
|
+
export type CanonicalBitcoinNetwork = keyof typeof Enum_CanonicalBitcoinNetwork;
|
|
4564
|
+
|
|
4565
|
+
// BabylonDeriveContextHash
|
|
4566
|
+
export type BabylonDeriveContextHash = {
|
|
4567
|
+
address_n: number[];
|
|
4568
|
+
script_type: InputScriptType;
|
|
4569
|
+
app_name: string;
|
|
4570
|
+
context: string;
|
|
4571
|
+
network: CanonicalBitcoinNetwork;
|
|
4572
|
+
};
|
|
4573
|
+
|
|
4574
|
+
// BabylonDerivedContextHash
|
|
4575
|
+
export type BabylonDerivedContextHash = {
|
|
4576
|
+
secret: string;
|
|
4577
|
+
};
|
|
4578
|
+
|
|
4557
4579
|
export type TextMemo = {
|
|
4558
4580
|
text: string;
|
|
4559
4581
|
};
|
|
@@ -4658,6 +4680,70 @@ export type UiAnimationRequest = {
|
|
|
4658
4680
|
type?: UiAnimationType;
|
|
4659
4681
|
};
|
|
4660
4682
|
|
|
4683
|
+
export enum ZcashAddressType {
|
|
4684
|
+
UNIFIED_ORCHARD_P2PKH = 0,
|
|
4685
|
+
TRANSPARENT_P2PKH = 1,
|
|
4686
|
+
UNIFIED_ORCHARD = 2,
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
export enum ZcashAddressScope {
|
|
4690
|
+
EXTERNAL = 0,
|
|
4691
|
+
INTERNAL = 1,
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
// ZcashGetAddress
|
|
4695
|
+
export type ZcashGetAddress = {
|
|
4696
|
+
address_n: number[];
|
|
4697
|
+
show_display?: boolean;
|
|
4698
|
+
include_seed_fingerprint?: boolean;
|
|
4699
|
+
diversifier_index?: number;
|
|
4700
|
+
address_type?: ZcashAddressType;
|
|
4701
|
+
scope?: ZcashAddressScope;
|
|
4702
|
+
};
|
|
4703
|
+
|
|
4704
|
+
// ZcashAddress
|
|
4705
|
+
export type ZcashAddress = {
|
|
4706
|
+
address: string;
|
|
4707
|
+
seed_fingerprint?: string;
|
|
4708
|
+
};
|
|
4709
|
+
|
|
4710
|
+
// ZcashGetFullViewingKey
|
|
4711
|
+
export type ZcashGetFullViewingKey = {
|
|
4712
|
+
address_n: number[];
|
|
4713
|
+
};
|
|
4714
|
+
|
|
4715
|
+
// ZcashFullViewingKey
|
|
4716
|
+
export type ZcashFullViewingKey = {
|
|
4717
|
+
ufvk: string;
|
|
4718
|
+
};
|
|
4719
|
+
|
|
4720
|
+
// ZcashSignPczt
|
|
4721
|
+
export type ZcashSignPczt = {
|
|
4722
|
+
pczt_length: number;
|
|
4723
|
+
pczt_initial_chunk: string;
|
|
4724
|
+
};
|
|
4725
|
+
|
|
4726
|
+
// ZcashPcztChunkRequest
|
|
4727
|
+
export type ZcashPcztChunkRequest = {
|
|
4728
|
+
chunk_length: number;
|
|
4729
|
+
};
|
|
4730
|
+
|
|
4731
|
+
// ZcashPcztChunkAck
|
|
4732
|
+
export type ZcashPcztChunkAck = {
|
|
4733
|
+
data_chunk: string;
|
|
4734
|
+
};
|
|
4735
|
+
|
|
4736
|
+
// ZcashSignedPczt
|
|
4737
|
+
export type ZcashSignedPczt = {
|
|
4738
|
+
pczt_length: number;
|
|
4739
|
+
pczt_initial_chunk: string;
|
|
4740
|
+
};
|
|
4741
|
+
|
|
4742
|
+
// ZcashSignedPcztChunkRequest
|
|
4743
|
+
export type ZcashSignedPcztChunkRequest = {
|
|
4744
|
+
chunk_length: number;
|
|
4745
|
+
};
|
|
4746
|
+
|
|
4661
4747
|
// experimental_message
|
|
4662
4748
|
export type experimental_message = {};
|
|
4663
4749
|
|
|
@@ -5949,6 +6035,8 @@ export type MessageType = {
|
|
|
5949
6035
|
TronSignMessage: TronSignMessage;
|
|
5950
6036
|
TronMessageSignature: TronMessageSignature;
|
|
5951
6037
|
facotry: facotry;
|
|
6038
|
+
BabylonDeriveContextHash: BabylonDeriveContextHash;
|
|
6039
|
+
BabylonDerivedContextHash: BabylonDerivedContextHash;
|
|
5952
6040
|
TextMemo: TextMemo;
|
|
5953
6041
|
RefundMemo: RefundMemo;
|
|
5954
6042
|
CoinPurchaseMemo: CoinPurchaseMemo;
|
|
@@ -5964,6 +6052,15 @@ export type MessageType = {
|
|
|
5964
6052
|
UnlockPath: UnlockPath;
|
|
5965
6053
|
UnlockedPathRequest: UnlockedPathRequest;
|
|
5966
6054
|
UiAnimationRequest: UiAnimationRequest;
|
|
6055
|
+
ZcashGetAddress: ZcashGetAddress;
|
|
6056
|
+
ZcashAddress: ZcashAddress;
|
|
6057
|
+
ZcashGetFullViewingKey: ZcashGetFullViewingKey;
|
|
6058
|
+
ZcashFullViewingKey: ZcashFullViewingKey;
|
|
6059
|
+
ZcashSignPczt: ZcashSignPczt;
|
|
6060
|
+
ZcashPcztChunkRequest: ZcashPcztChunkRequest;
|
|
6061
|
+
ZcashPcztChunkAck: ZcashPcztChunkAck;
|
|
6062
|
+
ZcashSignedPczt: ZcashSignedPczt;
|
|
6063
|
+
ZcashSignedPcztChunkRequest: ZcashSignedPcztChunkRequest;
|
|
5967
6064
|
experimental_message: experimental_message;
|
|
5968
6065
|
experimental_field: experimental_field;
|
|
5969
6066
|
ProtocolInfoRequest: ProtocolInfoRequest;
|
package/src/types/transport.ts
CHANGED
|
@@ -34,6 +34,12 @@ export type OneKeyDeviceInfoWithSession = OneKeyUsbDeviceInfo & {
|
|
|
34
34
|
export type OneKeyMobileDeviceInfo = {
|
|
35
35
|
id: string;
|
|
36
36
|
name: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Color letter of the serial number ('A'-'Z') from the Pro2 / Neo BLE advertisement,
|
|
39
|
+
* possibly seen in an earlier scan. Set by the React Native and Electron BLE
|
|
40
|
+
* transports; absent when unknown.
|
|
41
|
+
*/
|
|
42
|
+
bleColorCode?: string;
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
export type OneKeyDeviceInfoBase = {
|
|
@@ -70,6 +76,19 @@ export type TransportWriteMetrics = {
|
|
|
70
76
|
frameBytes: number;
|
|
71
77
|
};
|
|
72
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Supplies the remaining chunks of one file for an ordered FilesystemFileWrite pipeline.
|
|
81
|
+
* The call's request data is the first chunk; `next` returns the following chunk in offset
|
|
82
|
+
* order, or undefined to stop. The call then resolves with the last confirmation (or the
|
|
83
|
+
* device Failure), so the caller must check `processed_byte` for completeness. Callbacks
|
|
84
|
+
* should not throw: an error escapes with chunks still in flight and invalidates the link.
|
|
85
|
+
*/
|
|
86
|
+
export type ProtocolV2FileWritePipeline = {
|
|
87
|
+
next: () => Promise<Record<string, unknown> | undefined>;
|
|
88
|
+
/** Called for each chunk after the device confirms it, in offset order. */
|
|
89
|
+
onResponse: (response: MessageFromOneKey) => void;
|
|
90
|
+
};
|
|
91
|
+
|
|
73
92
|
export type TransportCallOptions = {
|
|
74
93
|
timeoutMs?: number;
|
|
75
94
|
expectedTypes?: string[];
|
|
@@ -83,6 +102,12 @@ export type TransportCallOptions = {
|
|
|
83
102
|
onResponseAfterWrite?: (response: MessageFromOneKey) => void;
|
|
84
103
|
/** Prefer acknowledged BLE characteristic writes for this call when supported. */
|
|
85
104
|
writeWithResponse?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Protocol V2 only: keep up to PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW ordered
|
|
107
|
+
* FilesystemFileWrite frames of one file in flight within this call. `timeoutMs`
|
|
108
|
+
* bounds each frame write and each response instead of the whole file.
|
|
109
|
+
*/
|
|
110
|
+
fileWritePipeline?: ProtocolV2FileWritePipeline;
|
|
86
111
|
};
|
|
87
112
|
|
|
88
113
|
type ITransportInitFn = (
|