@onekeyfe/hd-transport 1.2.0-alpha.21 → 1.2.0-alpha.23
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__/messages.test.js +24 -3
- package/__tests__/protocol-v2-link-manager.test.js +27 -9
- package/__tests__/protocol-v2.test.js +226 -28
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +81 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +164 -58
- package/dist/protocols/index.d.ts +5 -1
- package/dist/protocols/index.d.ts.map +1 -1
- package/dist/protocols/v2/decode.d.ts +11 -0
- package/dist/protocols/v2/decode.d.ts.map +1 -1
- package/dist/protocols/v2/errors.d.ts +8 -0
- package/dist/protocols/v2/errors.d.ts.map +1 -0
- package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
- package/dist/protocols/v2/index.d.ts +1 -0
- package/dist/protocols/v2/index.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +4 -1
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/types/messages.d.ts +21 -2
- package/dist/types/messages.d.ts.map +1 -1
- package/messages-protocol-v2.json +35 -1
- package/package.json +2 -2
- package/scripts/protobuf-types.js +7 -1
- package/src/constants.ts +8 -0
- package/src/protocols/index.ts +11 -2
- package/src/protocols/v2/decode.ts +49 -12
- package/src/protocols/v2/errors.ts +23 -0
- package/src/protocols/v2/frame-assembler.ts +6 -4
- package/src/protocols/v2/index.ts +1 -0
- package/src/protocols/v2/session.ts +125 -68
- package/src/types/messages.ts +25 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.23",
|
|
4
4
|
"description": "Transport layer abstractions and utilities for OneKey hardware SDK.",
|
|
5
5
|
"author": "OneKey",
|
|
6
6
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"long": "^4.0.0",
|
|
29
29
|
"protobufjs": "^6.11.2"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "7cadb40ca6414053a488756f28a9f413164b235d"
|
|
32
32
|
}
|
|
@@ -69,6 +69,9 @@ const ENUM_KEYS = [
|
|
|
69
69
|
'ProtocolV2Capability',
|
|
70
70
|
];
|
|
71
71
|
|
|
72
|
+
// Safety checks are accepted as both protobuf enum names and numeric legacy values.
|
|
73
|
+
const ENUM_KEYS_WITH_NUMERIC_VALUES = ['SafetyCheckLevel'];
|
|
74
|
+
|
|
72
75
|
const parseEnumTypescript = (itemName, item) => {
|
|
73
76
|
const value = [];
|
|
74
77
|
const IS_KEY = ENUM_KEYS.includes(itemName);
|
|
@@ -87,7 +90,10 @@ const parseEnumTypescript = (itemName, item) => {
|
|
|
87
90
|
value.push('}');
|
|
88
91
|
|
|
89
92
|
if (IS_KEY) {
|
|
90
|
-
|
|
93
|
+
const numericValue = ENUM_KEYS_WITH_NUMERIC_VALUES.includes(itemName)
|
|
94
|
+
? ` | Enum_${itemName}`
|
|
95
|
+
: '';
|
|
96
|
+
value.push(`export type ${itemName} = keyof typeof Enum_${itemName}${numericValue};`);
|
|
91
97
|
}
|
|
92
98
|
// empty line
|
|
93
99
|
value.push('');
|
package/src/constants.ts
CHANGED
|
@@ -48,3 +48,11 @@ export const PROTOCOL_V2_CHANNEL_SOCKET = 2;
|
|
|
48
48
|
|
|
49
49
|
/** packet_src for protobuf commands; firmware routes zero to the protobuf dispatcher. */
|
|
50
50
|
export const PROTOCOL_V2_PACKET_SRC_COMMAND = 0;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Shared upper bound for a Protocol V2 request without a method-specific timeout.
|
|
54
|
+
* Interactive and signing calls may wait on the device UI, so keep this longer than
|
|
55
|
+
* ordinary transport timeouts while still preventing a stalled link from blocking
|
|
56
|
+
* the per-device queue forever.
|
|
57
|
+
*/
|
|
58
|
+
export const PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS = 5 * 60 * 1000;
|
package/src/protocols/index.ts
CHANGED
|
@@ -6,7 +6,12 @@ import { decodeMessage as decodeV1Message } from './v1/receive';
|
|
|
6
6
|
import { createMessageFromName, createMessageFromType } from '../serialization/protobuf/messages';
|
|
7
7
|
import { encode as encodeProtobuf } from '../serialization/protobuf/encode';
|
|
8
8
|
import { decode as decodeProtobuf } from '../serialization/protobuf/decode';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
decodeFrame as decodeV2Frame,
|
|
11
|
+
encodeProtobufFrame,
|
|
12
|
+
inspectFrameHeader,
|
|
13
|
+
isAckFrame,
|
|
14
|
+
} from './v2';
|
|
10
15
|
|
|
11
16
|
import type { Root } from 'protobufjs/light';
|
|
12
17
|
|
|
@@ -71,15 +76,19 @@ export const ProtocolV1 = {
|
|
|
71
76
|
|
|
72
77
|
export const ProtocolV2 = {
|
|
73
78
|
isAckFrame,
|
|
79
|
+
inspectFrameHeader,
|
|
74
80
|
|
|
75
81
|
inspectFrame(schemas: ProtocolV2Schemas, frame: Uint8Array) {
|
|
76
|
-
const { messageTypeId, pbPayload, seq } = decodeV2Frame(frame);
|
|
82
|
+
const { messageTypeId, pbPayload, seq, router, packetSrc, dataType } = decodeV2Frame(frame);
|
|
77
83
|
const { messageName } = createProtocolV2MessageFromType(messageTypeId, schemas);
|
|
78
84
|
return {
|
|
79
85
|
messageName,
|
|
80
86
|
messageTypeId,
|
|
81
87
|
pbPayload,
|
|
82
88
|
seq,
|
|
89
|
+
router,
|
|
90
|
+
packetSrc,
|
|
91
|
+
dataType,
|
|
83
92
|
type: messageName,
|
|
84
93
|
};
|
|
85
94
|
},
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
PROTO_DATA_TYPE_ACK,
|
|
3
|
+
PROTO_DATA_TYPE_PACKET,
|
|
4
|
+
PROTO_HEAD_CRC_SIZE,
|
|
5
|
+
PROTO_HEAD_SOF,
|
|
6
|
+
} from './constants';
|
|
2
7
|
import { crc8 } from './crc8';
|
|
3
8
|
|
|
4
9
|
export interface ProtoV2Frame {
|
|
@@ -8,9 +13,23 @@ export interface ProtoV2Frame {
|
|
|
8
13
|
pbPayload: Uint8Array;
|
|
9
14
|
/** Sequence number from the frame header */
|
|
10
15
|
seq: number;
|
|
16
|
+
/** Routing channel from the frame header */
|
|
17
|
+
router: number;
|
|
18
|
+
/** Packet source from the frame header */
|
|
19
|
+
packetSrc: number;
|
|
20
|
+
/** Packet or ACK discriminator from the frame header */
|
|
21
|
+
dataType: number;
|
|
11
22
|
}
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
export type ProtoV2FrameHeader = {
|
|
25
|
+
frameLen: number;
|
|
26
|
+
router: number;
|
|
27
|
+
packetSrc: number;
|
|
28
|
+
dataType: number;
|
|
29
|
+
seq: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function inspectFrameHeader(data: Uint8Array): ProtoV2FrameHeader {
|
|
14
33
|
if (data.length < PROTO_HEAD_CRC_SIZE) {
|
|
15
34
|
throw new Error(`Protocol V2 frame too short: ${data.length} bytes`);
|
|
16
35
|
}
|
|
@@ -23,8 +42,10 @@ function validateFrame(data: Uint8Array): number {
|
|
|
23
42
|
|
|
24
43
|
const frameLen = data[1] + data[2] * 256;
|
|
25
44
|
|
|
26
|
-
if (data.length
|
|
27
|
-
throw new Error(
|
|
45
|
+
if (data.length !== frameLen) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Protocol V2 frame length mismatch: expected ${frameLen} bytes, got ${data.length}`
|
|
48
|
+
);
|
|
28
49
|
}
|
|
29
50
|
|
|
30
51
|
const expectedHeaderCrc = crc8(data, 3);
|
|
@@ -45,7 +66,22 @@ function validateFrame(data: Uint8Array): number {
|
|
|
45
66
|
);
|
|
46
67
|
}
|
|
47
68
|
|
|
48
|
-
|
|
69
|
+
// eslint-disable-next-line no-bitwise
|
|
70
|
+
const dataType = data[5] & 0x03;
|
|
71
|
+
// eslint-disable-next-line no-bitwise
|
|
72
|
+
const packetSrc = (data[5] >> 2) & 0x0f;
|
|
73
|
+
const seq = data[6];
|
|
74
|
+
if (seq === 0) {
|
|
75
|
+
throw new Error('Invalid Protocol V2 sequence: 0 is reserved');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
frameLen,
|
|
80
|
+
router: data[4],
|
|
81
|
+
packetSrc,
|
|
82
|
+
dataType,
|
|
83
|
+
seq,
|
|
84
|
+
};
|
|
49
85
|
}
|
|
50
86
|
|
|
51
87
|
export function isAckFrame(data: Uint8Array): boolean {
|
|
@@ -54,9 +90,9 @@ export function isAckFrame(data: Uint8Array): boolean {
|
|
|
54
90
|
return false;
|
|
55
91
|
}
|
|
56
92
|
|
|
57
|
-
const
|
|
58
|
-
if (frameLen !== PROTO_HEAD_CRC_SIZE) {
|
|
59
|
-
throw new Error(`Invalid Protocol V2 ACK frame length: ${frameLen}`);
|
|
93
|
+
const header = inspectFrameHeader(data);
|
|
94
|
+
if (header.frameLen !== PROTO_HEAD_CRC_SIZE) {
|
|
95
|
+
throw new Error(`Invalid Protocol V2 ACK frame length: ${header.frameLen}`);
|
|
60
96
|
}
|
|
61
97
|
return true;
|
|
62
98
|
}
|
|
@@ -72,9 +108,10 @@ export function isAckFrame(data: Uint8Array): boolean {
|
|
|
72
108
|
* Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
|
|
73
109
|
*/
|
|
74
110
|
export function decodeFrame(data: Uint8Array): ProtoV2Frame {
|
|
75
|
-
const frameLen =
|
|
76
|
-
|
|
77
|
-
|
|
111
|
+
const { frameLen, router, packetSrc, dataType, seq } = inspectFrameHeader(data);
|
|
112
|
+
if (dataType !== PROTO_DATA_TYPE_PACKET) {
|
|
113
|
+
throw new Error(`Invalid Protocol V2 data type: expected packet, got ${dataType}`);
|
|
114
|
+
}
|
|
78
115
|
// Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte
|
|
79
116
|
const payloadData = data.slice(7, frameLen - 1);
|
|
80
117
|
|
|
@@ -85,5 +122,5 @@ export function decodeFrame(data: Uint8Array): ProtoV2Frame {
|
|
|
85
122
|
const messageTypeId = payloadData[0] + payloadData[1] * 256;
|
|
86
123
|
const pbPayload = payloadData.slice(2);
|
|
87
124
|
|
|
88
|
-
return { messageTypeId, pbPayload, seq };
|
|
125
|
+
return { messageTypeId, pbPayload, seq, router, packetSrc, dataType };
|
|
89
126
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ProtocolV2LinkErrorCode =
|
|
2
|
+
| 'response-timeout'
|
|
3
|
+
| 'router'
|
|
4
|
+
| 'packet-source'
|
|
5
|
+
| 'ack-sequence'
|
|
6
|
+
| 'response-sequence'
|
|
7
|
+
| 'frame';
|
|
8
|
+
|
|
9
|
+
export class ProtocolV2LinkError extends Error {
|
|
10
|
+
readonly code: ProtocolV2LinkErrorCode;
|
|
11
|
+
|
|
12
|
+
readonly cause?: unknown;
|
|
13
|
+
|
|
14
|
+
constructor(code: ProtocolV2LinkErrorCode, message: string, cause?: unknown) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'ProtocolV2LinkError';
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.cause = cause;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const isProtocolV2LinkError = (error: unknown): error is ProtocolV2LinkError =>
|
|
23
|
+
error instanceof ProtocolV2LinkError;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
|
|
2
2
|
import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF, PROTO_PRE_HEAD_SIZE } from './constants';
|
|
3
3
|
import { crc8 } from './crc8';
|
|
4
|
+
import { ProtocolV2LinkError } from './errors';
|
|
4
5
|
|
|
5
6
|
export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
6
7
|
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
@@ -58,7 +59,7 @@ export class ProtocolV2FrameAssembler {
|
|
|
58
59
|
|
|
59
60
|
if (this.buffer[0] !== PROTO_HEAD_SOF) {
|
|
60
61
|
this.reset();
|
|
61
|
-
throw new
|
|
62
|
+
throw new ProtocolV2LinkError('frame', 'Invalid Protocol V2 SOF');
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
const expectedLen = this.buffer[1] + this.buffer[2] * 256;
|
|
@@ -67,11 +68,11 @@ export class ProtocolV2FrameAssembler {
|
|
|
67
68
|
// complete frame: without resetting, this poison prefix would stay in
|
|
68
69
|
// the buffer forever and deadlock the caller's drain loop.
|
|
69
70
|
this.reset();
|
|
70
|
-
throw new
|
|
71
|
+
throw new ProtocolV2LinkError('frame', `Protocol V2 frame length too small: ${expectedLen}`);
|
|
71
72
|
}
|
|
72
73
|
if (expectedLen > this.maxFrameBytes) {
|
|
73
74
|
this.reset();
|
|
74
|
-
throw new
|
|
75
|
+
throw new ProtocolV2LinkError('frame', `Protocol V2 frame too large: ${expectedLen}`);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
if (this.buffer.length < PROTO_PRE_HEAD_SIZE) return undefined;
|
|
@@ -82,7 +83,8 @@ export class ProtocolV2FrameAssembler {
|
|
|
82
83
|
const expectedHeaderCrc = crc8(this.buffer, 3);
|
|
83
84
|
if (this.buffer[3] !== expectedHeaderCrc) {
|
|
84
85
|
this.reset();
|
|
85
|
-
throw new
|
|
86
|
+
throw new ProtocolV2LinkError(
|
|
87
|
+
'frame',
|
|
86
88
|
`Protocol V2 header CRC mismatch: expected 0x${expectedHeaderCrc
|
|
87
89
|
.toString(16)
|
|
88
90
|
.padStart(2, '0')}`
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
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 = {
|
|
@@ -114,11 +121,13 @@ export async function withProtocolTimeout<T>(
|
|
|
114
121
|
promise: Promise<T>,
|
|
115
122
|
timeoutMs: number | undefined,
|
|
116
123
|
createTimeoutError: () => Error,
|
|
117
|
-
onTimeout?: () => void
|
|
124
|
+
onTimeout?: () => void,
|
|
125
|
+
abortSignal?: AbortSignal
|
|
118
126
|
): Promise<T> {
|
|
119
127
|
if (!timeoutMs) return promise;
|
|
120
128
|
|
|
121
129
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
130
|
+
let abortHandler: (() => void) | undefined;
|
|
122
131
|
try {
|
|
123
132
|
return await Promise.race([
|
|
124
133
|
promise,
|
|
@@ -130,9 +139,26 @@ export async function withProtocolTimeout<T>(
|
|
|
130
139
|
reject(createTimeoutError());
|
|
131
140
|
}, timeoutMs);
|
|
132
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
|
+
: []),
|
|
133
156
|
]);
|
|
134
157
|
} finally {
|
|
135
158
|
if (timer) clearTimeout(timer);
|
|
159
|
+
if (abortHandler) {
|
|
160
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
161
|
+
}
|
|
136
162
|
}
|
|
137
163
|
}
|
|
138
164
|
|
|
@@ -145,6 +171,8 @@ export class ProtocolV2Session {
|
|
|
145
171
|
// in-flight calls on the same session would steal each other's responses.
|
|
146
172
|
private pendingCall: Promise<unknown> = Promise.resolve();
|
|
147
173
|
|
|
174
|
+
private lastResponseSequence?: number;
|
|
175
|
+
|
|
148
176
|
constructor(options: ProtocolV2SessionOptions) {
|
|
149
177
|
this.options = options;
|
|
150
178
|
this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
|
|
@@ -163,7 +191,7 @@ export class ProtocolV2Session {
|
|
|
163
191
|
return result;
|
|
164
192
|
}
|
|
165
193
|
|
|
166
|
-
private
|
|
194
|
+
private executeCall(
|
|
167
195
|
name: string,
|
|
168
196
|
data: Record<string, unknown>,
|
|
169
197
|
callOptions: ProtocolV2CallOptions
|
|
@@ -181,78 +209,105 @@ export class ProtocolV2Session {
|
|
|
181
209
|
createTimeoutError,
|
|
182
210
|
generation = 0,
|
|
183
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();
|
|
184
217
|
|
|
185
218
|
const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
|
|
186
|
-
const
|
|
219
|
+
const baseCallContext: ProtocolV2CallContext = {
|
|
187
220
|
messageName: name,
|
|
188
|
-
timeoutMs
|
|
221
|
+
timeoutMs,
|
|
189
222
|
highVolume: shouldReduceDebug,
|
|
190
223
|
generation,
|
|
224
|
+
signal: abortController.signal,
|
|
191
225
|
};
|
|
192
|
-
await prepareCall?.(callContext);
|
|
193
|
-
const protoSeq = this.sequenceCursor.next();
|
|
194
|
-
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
195
|
-
packetSrc,
|
|
196
|
-
router,
|
|
197
|
-
seq: protoSeq,
|
|
198
|
-
});
|
|
199
|
-
// Suppress Protocol V2 TX frame logs to avoid excessive transport diagnostics.
|
|
200
|
-
/*
|
|
201
|
-
const txMetadata = ProtocolV2.inspectFrame(schemas, frame);
|
|
202
|
-
|
|
203
|
-
if (!shouldReduceDebug) {
|
|
204
|
-
logger?.debug?.(`[${logPrefix}] TX`, {
|
|
205
|
-
method: name,
|
|
206
|
-
type: name,
|
|
207
|
-
typeId: txMetadata.messageTypeId,
|
|
208
|
-
seq: txMetadata.seq,
|
|
209
|
-
bytes: frame.length,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
*/
|
|
213
226
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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,
|
|
234
|
+
});
|
|
219
235
|
|
|
220
|
-
|
|
236
|
+
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
221
241
|
|
|
222
|
-
|
|
223
|
-
// Promise.race alone would leave this loop running as a zombie that keeps
|
|
224
|
-
// consuming frames meant for the next call. The timeout callback flips the
|
|
225
|
-
// flag so the loop exits and discards any late frame.
|
|
226
|
-
const cancellation = { cancelled: false };
|
|
242
|
+
await writeFrame(frame, baseCallContext);
|
|
227
243
|
|
|
228
|
-
const readResponse = async (): Promise<MessageFromOneKey> => {
|
|
229
244
|
// Some Protocol V2 operations emit progress notifications before the
|
|
230
245
|
// terminal response. Consume those frames here so callers still see a
|
|
231
246
|
// request/terminal-response shaped API.
|
|
232
|
-
while (!
|
|
233
|
-
const rxFrame = await readFrame(
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
);
|
|
237
272
|
}
|
|
238
|
-
const isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
239
|
-
if (!isAck) {
|
|
240
|
-
// Suppress Protocol V2 RX frame logs to avoid excessive transport diagnostics.
|
|
241
|
-
/*
|
|
242
|
-
const rxMetadata = ProtocolV2.inspectFrame(schemas, rxFrame);
|
|
243
|
-
|
|
244
|
-
if (!shouldReduceDebug) {
|
|
245
|
-
logger?.debug?.(`[${logPrefix}] RX`, {
|
|
246
|
-
method: name,
|
|
247
|
-
type: rxMetadata.type,
|
|
248
|
-
typeId: rxMetadata.messageTypeId,
|
|
249
|
-
seq: rxMetadata.seq,
|
|
250
|
-
bytes: rxFrame.length,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
*/
|
|
254
273
|
|
|
255
|
-
|
|
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
|
+
);
|
|
283
|
+
}
|
|
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
|
+
);
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
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
|
+
);
|
|
301
|
+
}
|
|
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;
|
|
256
311
|
|
|
257
312
|
const response = check.call(decoded);
|
|
258
313
|
if (callOptions.intermediateTypes?.includes(response.type)) {
|
|
@@ -268,20 +323,19 @@ export class ProtocolV2Session {
|
|
|
268
323
|
}
|
|
269
324
|
}
|
|
270
325
|
}
|
|
271
|
-
|
|
272
|
-
// rejected by the timeout, so this rejection is consumed by the race.
|
|
326
|
+
|
|
273
327
|
throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
|
|
274
328
|
};
|
|
275
329
|
|
|
276
330
|
return withProtocolTimeout(
|
|
277
|
-
|
|
278
|
-
|
|
331
|
+
runCall(),
|
|
332
|
+
timeoutMs,
|
|
279
333
|
() =>
|
|
280
334
|
createTimeoutError
|
|
281
|
-
? createTimeoutError(name,
|
|
282
|
-
: new Error(`Protocol V2 response timeout after ${
|
|
335
|
+
? createTimeoutError(name, timeoutMs)
|
|
336
|
+
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
|
|
283
337
|
() => {
|
|
284
|
-
|
|
338
|
+
abortController.abort();
|
|
285
339
|
}
|
|
286
340
|
);
|
|
287
341
|
}
|
|
@@ -312,7 +366,10 @@ export async function probeProtocolV2({
|
|
|
312
366
|
const response = await call(
|
|
313
367
|
'Ping',
|
|
314
368
|
{ message: 'protocol-v2-probe' },
|
|
315
|
-
{
|
|
369
|
+
{
|
|
370
|
+
timeoutMs,
|
|
371
|
+
expectedTypes: ['Success'],
|
|
372
|
+
}
|
|
316
373
|
);
|
|
317
374
|
if (response.type === 'Success') {
|
|
318
375
|
return true;
|
package/src/types/messages.ts
CHANGED
|
@@ -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,6 +5051,15 @@ export type ProtocolV2DeviceInfo = {
|
|
|
5049
5051
|
status?: DeviceStatus;
|
|
5050
5052
|
};
|
|
5051
5053
|
|
|
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
|
+
|
|
5052
5063
|
// DeviceSessionGet
|
|
5053
5064
|
export type DeviceSessionGet = {
|
|
5054
5065
|
session_id?: string;
|
|
@@ -5060,8 +5071,19 @@ export type DeviceSession = {
|
|
|
5060
5071
|
btc_test_address?: string;
|
|
5061
5072
|
};
|
|
5062
5073
|
|
|
5074
|
+
export enum DeviceSessionPinType {
|
|
5075
|
+
Any = 1,
|
|
5076
|
+
Main = 2,
|
|
5077
|
+
AttachToPin = 3,
|
|
5078
|
+
}
|
|
5079
|
+
|
|
5063
5080
|
// DeviceSessionAskPin
|
|
5064
|
-
export type DeviceSessionAskPin = {
|
|
5081
|
+
export type DeviceSessionAskPin = {
|
|
5082
|
+
type?: DeviceSessionPinType;
|
|
5083
|
+
};
|
|
5084
|
+
|
|
5085
|
+
// DeviceSessionAskPassphrase
|
|
5086
|
+
export type DeviceSessionAskPassphrase = {};
|
|
5065
5087
|
|
|
5066
5088
|
export enum DeviceSessionAskPin_FailureSubCodes {
|
|
5067
5089
|
UserCancel = 1,
|
|
@@ -5884,6 +5906,7 @@ export type MessageType = {
|
|
|
5884
5906
|
DeviceSessionGet: DeviceSessionGet;
|
|
5885
5907
|
DeviceSession: DeviceSession;
|
|
5886
5908
|
DeviceSessionAskPin: DeviceSessionAskPin;
|
|
5909
|
+
DeviceSessionAskPassphrase: DeviceSessionAskPassphrase;
|
|
5887
5910
|
DeviceStatus: DeviceStatus;
|
|
5888
5911
|
DeviceStatusGet: DeviceStatusGet;
|
|
5889
5912
|
DevOnboardingSetupStatus: DevOnboardingSetupStatus;
|