@onekeyfe/hd-transport 1.2.0-alpha.11 → 1.2.0-alpha.110
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 +165 -13
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +120 -9
- package/__tests__/protocol-v2-usb-transport-base.test.js +36 -2
- package/__tests__/protocol-v2.test.js +553 -63
- package/__tests__/transport-log.test.js +79 -0
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +687 -255
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +661 -296
- package/dist/protocols/index.d.ts +25 -6
- package/dist/protocols/index.d.ts.map +1 -1
- package/dist/protocols/v2/ble-frame-writer.d.ts +15 -0
- package/dist/protocols/v2/ble-frame-writer.d.ts.map +1 -0
- package/dist/protocols/v2/decode.d.ts +12 -2
- package/dist/protocols/v2/decode.d.ts.map +1 -1
- package/dist/protocols/v2/encode.d.ts +2 -3
- package/dist/protocols/v2/encode.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 +2 -1
- package/dist/protocols/v2/index.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +4 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +10 -4
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +1 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/serialization/protobuf/encode.d.ts.map +1 -1
- package/dist/types/messages.d.ts +336 -155
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +20 -3
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/transportLog.d.ts +9 -0
- package/dist/utils/transportLog.d.ts.map +1 -0
- package/messages-protocol-v2.json +1030 -567
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +85 -217
- package/scripts/protobuf-patches/index.js +1 -0
- package/scripts/protobuf-types.js +19 -1
- package/src/constants.ts +30 -16
- package/src/index.ts +5 -1
- package/src/protocols/index.ts +128 -40
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +50 -35
- package/src/protocols/v2/encode.ts +2 -28
- package/src/protocols/v2/errors.ts +25 -0
- package/src/protocols/v2/frame-assembler.ts +6 -4
- package/src/protocols/v2/index.ts +2 -1
- package/src/protocols/v2/link-manager.ts +54 -5
- package/src/protocols/v2/session.ts +151 -200
- package/src/protocols/v2/usb-transport-base.ts +29 -10
- package/src/serialization/protobuf/decode.ts +4 -1
- package/src/serialization/protobuf/encode.ts +5 -1
- package/src/types/messages.ts +409 -197
- package/src/types/transport.ts +28 -3
- package/src/utils/transportLog.ts +102 -0
- package/dist/protocols/v2/debug.d.ts +0 -13
- package/dist/protocols/v2/debug.d.ts.map +0 -1
- package/src/protocols/v2/debug.ts +0 -26
|
@@ -2,12 +2,26 @@ const { ProtocolV2 } = require('../src/protocols');
|
|
|
2
2
|
const { parseConfigure } = require('../src/serialization/protobuf/messages');
|
|
3
3
|
const {
|
|
4
4
|
ProtocolV2FrameAssembler,
|
|
5
|
+
ProtocolV2LinkError,
|
|
5
6
|
ProtocolV2SequenceCursor,
|
|
6
7
|
ProtocolV2Session,
|
|
7
8
|
hexToBytes,
|
|
9
|
+
isProtocolV2HighThroughputCall,
|
|
8
10
|
probeProtocolV2,
|
|
9
11
|
} = require('../src/protocols/v2/session');
|
|
10
12
|
const protocolV2 = require('../src/protocols/v2');
|
|
13
|
+
const {
|
|
14
|
+
PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE,
|
|
15
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
16
|
+
PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
17
|
+
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
18
|
+
} = require('../src/constants');
|
|
19
|
+
|
|
20
|
+
test('keeps sensitive acknowledgements separate from high-throughput calls', () => {
|
|
21
|
+
expect(isProtocolV2HighThroughputCall('PassphraseAck')).toBe(false);
|
|
22
|
+
expect(isProtocolV2HighThroughputCall('PinMatrixAck')).toBe(false);
|
|
23
|
+
expect(isProtocolV2HighThroughputCall('FilesystemFileWrite')).toBe(true);
|
|
24
|
+
});
|
|
11
25
|
|
|
12
26
|
const protocolV1Messages = parseConfigure({
|
|
13
27
|
nested: {
|
|
@@ -68,14 +82,14 @@ const protocolV2Messages = parseConfigure({
|
|
|
68
82
|
type: 'uint32',
|
|
69
83
|
id: 1,
|
|
70
84
|
},
|
|
71
|
-
|
|
72
|
-
type: '
|
|
85
|
+
build_fingerprint: {
|
|
86
|
+
type: 'string',
|
|
73
87
|
id: 2,
|
|
74
|
-
rule: 'repeated',
|
|
75
88
|
},
|
|
76
|
-
|
|
77
|
-
type: '
|
|
89
|
+
supported_messages: {
|
|
90
|
+
type: 'uint32',
|
|
78
91
|
id: 3,
|
|
92
|
+
rule: 'repeated',
|
|
79
93
|
},
|
|
80
94
|
},
|
|
81
95
|
},
|
|
@@ -95,6 +109,22 @@ const protocolV2Messages = parseConfigure({
|
|
|
95
109
|
},
|
|
96
110
|
},
|
|
97
111
|
},
|
|
112
|
+
Failure: {
|
|
113
|
+
fields: {
|
|
114
|
+
code: {
|
|
115
|
+
type: 'uint32',
|
|
116
|
+
id: 1,
|
|
117
|
+
},
|
|
118
|
+
subcode: {
|
|
119
|
+
type: 'uint32',
|
|
120
|
+
id: 2,
|
|
121
|
+
},
|
|
122
|
+
message: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
id: 3,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
98
128
|
DeviceFirmwareTarget: {
|
|
99
129
|
fields: {
|
|
100
130
|
target_id: {
|
|
@@ -145,6 +175,18 @@ const protocolV2Messages = parseConfigure({
|
|
|
145
175
|
},
|
|
146
176
|
},
|
|
147
177
|
},
|
|
178
|
+
DeviceSession: {
|
|
179
|
+
fields: {
|
|
180
|
+
session_id: {
|
|
181
|
+
type: 'bytes',
|
|
182
|
+
id: 1,
|
|
183
|
+
},
|
|
184
|
+
btc_test_address: {
|
|
185
|
+
type: 'string',
|
|
186
|
+
id: 2,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
148
190
|
FileWrite: {
|
|
149
191
|
fields: {},
|
|
150
192
|
},
|
|
@@ -174,9 +216,11 @@ const protocolV2Messages = parseConfigure({
|
|
|
174
216
|
MessageType_ProtocolInfo: 60201,
|
|
175
217
|
MessageType_Ping: 60206,
|
|
176
218
|
MessageType_Success: 60207,
|
|
219
|
+
MessageType_Failure: 60208,
|
|
177
220
|
MessageType_FileWrite: 60805,
|
|
178
221
|
MessageType_DeviceFirmwareUpdateRequest: 61000,
|
|
179
222
|
MessageType_DeviceFirmwareUpdateStatus: 61002,
|
|
223
|
+
MessageType_DeviceSession: 61201,
|
|
180
224
|
MessageType_PartialNested: 62000,
|
|
181
225
|
},
|
|
182
226
|
},
|
|
@@ -187,9 +231,37 @@ const schemas = {
|
|
|
187
231
|
protocolV1: protocolV1Messages,
|
|
188
232
|
protocolV2: protocolV2Messages,
|
|
189
233
|
};
|
|
234
|
+
const productionProtocolV2Messages = parseConfigure(require('../messages-protocol-v2.json'));
|
|
235
|
+
|
|
236
|
+
const legacyProtocolInfoMessage = parseConfigure({
|
|
237
|
+
nested: {
|
|
238
|
+
ProtocolInfo: {
|
|
239
|
+
fields: {
|
|
240
|
+
version: {
|
|
241
|
+
type: 'uint32',
|
|
242
|
+
id: 1,
|
|
243
|
+
rule: 'required',
|
|
244
|
+
},
|
|
245
|
+
supported_messages: {
|
|
246
|
+
type: 'uint32',
|
|
247
|
+
id: 2,
|
|
248
|
+
rule: 'repeated',
|
|
249
|
+
options: {
|
|
250
|
+
packed: false,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
protobuf_definition: {
|
|
254
|
+
type: 'string',
|
|
255
|
+
id: 3,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
}).lookupType('ProtocolInfo');
|
|
190
261
|
|
|
191
262
|
const rewriteSeq = (frame, seq) => {
|
|
192
263
|
const copy = new Uint8Array(frame);
|
|
264
|
+
copy[4] = 1;
|
|
193
265
|
copy[6] = seq;
|
|
194
266
|
copy[copy.length - 1] = protocolV2.crc8(copy, copy.length - 1);
|
|
195
267
|
return copy;
|
|
@@ -199,8 +271,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
199
271
|
test('encodes and decodes Protocol V2 protobuf frames', () => {
|
|
200
272
|
const frame = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
201
273
|
version: 1,
|
|
274
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
202
275
|
supported_messages: [60200, 60201],
|
|
203
|
-
protobuf_definition: 'proto',
|
|
204
276
|
});
|
|
205
277
|
|
|
206
278
|
const parsed = protocolV2.decodeFrame(frame);
|
|
@@ -215,12 +287,70 @@ describe('Protocol V2 framing and session', () => {
|
|
|
215
287
|
seq: parsed.seq,
|
|
216
288
|
message: {
|
|
217
289
|
version: 1,
|
|
290
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
218
291
|
supported_messages: [60200, 60201],
|
|
219
|
-
protobuf_definition: 'proto',
|
|
220
292
|
},
|
|
221
293
|
});
|
|
222
294
|
});
|
|
223
295
|
|
|
296
|
+
test('decodes a two-byte legacy ProtocolInfo at the generic frame boundary', () => {
|
|
297
|
+
const frame = protocolV2.encodeProtobufFrame(60201, new Uint8Array([0x08, 0x01]));
|
|
298
|
+
const productionSchemas = {
|
|
299
|
+
protocolV1: protocolV1Messages,
|
|
300
|
+
protocolV2: productionProtocolV2Messages,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
expect(ProtocolV2.decodeFrame(productionSchemas, frame)).toMatchObject({
|
|
304
|
+
type: 'ProtocolInfo',
|
|
305
|
+
message: {
|
|
306
|
+
version: 1,
|
|
307
|
+
build_fingerprint: '',
|
|
308
|
+
supported_messages: [],
|
|
309
|
+
protobuf_definition: null,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('does not reinterpret a malformed current ProtocolInfo as the legacy layout', () => {
|
|
315
|
+
const frame = protocolV2.encodeProtobufFrame(60201, new Uint8Array([0x08, 0x01, 0x18, 0x01]));
|
|
316
|
+
|
|
317
|
+
expect(() =>
|
|
318
|
+
ProtocolV2.decodeFrame(
|
|
319
|
+
{
|
|
320
|
+
protocolV1: protocolV1Messages,
|
|
321
|
+
protocolV2: productionProtocolV2Messages,
|
|
322
|
+
},
|
|
323
|
+
frame
|
|
324
|
+
)
|
|
325
|
+
).toThrow('Protocol V2 protobuf decode failed for "ProtocolInfo"');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('preserves legacy ProtocolInfo capabilities when using the old field numbers', () => {
|
|
329
|
+
const payload = legacyProtocolInfoMessage
|
|
330
|
+
.encode({
|
|
331
|
+
version: 1,
|
|
332
|
+
supported_messages: [60602],
|
|
333
|
+
protobuf_definition: 'legacy',
|
|
334
|
+
})
|
|
335
|
+
.finish();
|
|
336
|
+
const frame = protocolV2.encodeProtobufFrame(60201, payload);
|
|
337
|
+
|
|
338
|
+
const decoded = ProtocolV2.decodeFrame(
|
|
339
|
+
{
|
|
340
|
+
protocolV1: protocolV1Messages,
|
|
341
|
+
protocolV2: productionProtocolV2Messages,
|
|
342
|
+
},
|
|
343
|
+
frame
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(decoded.message).toEqual({
|
|
347
|
+
version: 1,
|
|
348
|
+
build_fingerprint: '',
|
|
349
|
+
supported_messages: [60602],
|
|
350
|
+
protobuf_definition: null,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
224
354
|
test('does not encode V1-only messages into Protocol V2 frames', () => {
|
|
225
355
|
expect(() => ProtocolV2.encodeFrame(schemas, 'Ping', { message: 'ok' })).not.toThrow();
|
|
226
356
|
expect(() => ProtocolV2.encodeFrame(schemas, 'OnekeyGetFeatures', {})).toThrow(
|
|
@@ -241,6 +371,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
241
371
|
expect(decoded.message).toEqual({ message: 'ok' });
|
|
242
372
|
});
|
|
243
373
|
|
|
374
|
+
test('round-trips a DeviceSession response with a 32-byte session id', () => {
|
|
375
|
+
const sessionId = '01'.repeat(32);
|
|
376
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'DeviceSession', {
|
|
377
|
+
session_id: sessionId,
|
|
378
|
+
btc_test_address: 'tb1qwallet',
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const decoded = ProtocolV2.decodeFrame(schemas, frame);
|
|
382
|
+
|
|
383
|
+
expect(decoded.type).toBe('DeviceSession');
|
|
384
|
+
expect(decoded.message).toEqual({
|
|
385
|
+
session_id: sessionId,
|
|
386
|
+
btc_test_address: 'tb1qwallet',
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('reports malformed DeviceSession protobuf as a schema-compatible transport error', () => {
|
|
391
|
+
const malformedPayload = new Uint8Array(32);
|
|
392
|
+
malformedPayload[0] = 0x0a;
|
|
393
|
+
malformedPayload[1] = 101;
|
|
394
|
+
const frame = protocolV2.encodeProtobufFrame(61201, malformedPayload);
|
|
395
|
+
|
|
396
|
+
expect(() => ProtocolV2.decodeFrame(schemas, frame)).toThrow(
|
|
397
|
+
'Protocol V2 protobuf decode failed for "DeviceSession" (61201, 32-byte payload); ' +
|
|
398
|
+
'the payload is malformed or incompatible with the active SDK schema.'
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
|
|
244
402
|
test('decodes missing optional nested messages as null', () => {
|
|
245
403
|
const frame = ProtocolV2.encodeFrame(schemas, 'PartialNested', {
|
|
246
404
|
label: 'only label',
|
|
@@ -267,6 +425,60 @@ describe('Protocol V2 framing and session', () => {
|
|
|
267
425
|
expect(() => assembler.push(oversized)).toThrow('Protocol V2 frame too large');
|
|
268
426
|
});
|
|
269
427
|
|
|
428
|
+
test('enforces a transport-specific receive frame boundary', () => {
|
|
429
|
+
const assembler = new ProtocolV2FrameAssembler(2048);
|
|
430
|
+
const oversizedHeader = new Uint8Array([0x5a, 0x01, 0x08]);
|
|
431
|
+
|
|
432
|
+
expect(() => assembler.push(oversizedHeader)).toThrow('Protocol V2 frame too large: 2049');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('enforces the firmware 4200-byte Protocol V2 frame boundary', () => {
|
|
436
|
+
const boundaryFrame = ProtocolV2.encodeFrame(schemas, 'Ping', {
|
|
437
|
+
message: 'x'.repeat(4187),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(PROTOCOL_V2_FRAME_MAX_BYTES).toBe(4200);
|
|
441
|
+
expect(boundaryFrame).toHaveLength(PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
442
|
+
expect(() =>
|
|
443
|
+
ProtocolV2.encodeFrame(schemas, 'Ping', {
|
|
444
|
+
message: 'x'.repeat(4188),
|
|
445
|
+
})
|
|
446
|
+
).toThrow('Protocol V2 frame too large: 4201 > 4200');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('keeps optimized BLE firmware chunks inside the transport frame boundary', () => {
|
|
450
|
+
const productionSchemas = {
|
|
451
|
+
protocolV1: protocolV1Messages,
|
|
452
|
+
protocolV2: productionProtocolV2Messages,
|
|
453
|
+
};
|
|
454
|
+
const stagingPaths = [
|
|
455
|
+
'vol0:/bootloader.bin',
|
|
456
|
+
'vol0:/application_p1.bin',
|
|
457
|
+
'vol0:/application_p2.bin',
|
|
458
|
+
'vol0:/coprocessor.bin',
|
|
459
|
+
'vol0:/se01.bin',
|
|
460
|
+
'vol0:/se02.bin',
|
|
461
|
+
'vol0:/se03.bin',
|
|
462
|
+
'vol0:/se04.bin',
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
for (const path of stagingPaths) {
|
|
466
|
+
const frame = ProtocolV2.encodeFrame(productionSchemas, 'FilesystemFileWrite', {
|
|
467
|
+
file: {
|
|
468
|
+
path,
|
|
469
|
+
offset: 0xffffffff,
|
|
470
|
+
total_size: 0xffffffff,
|
|
471
|
+
data: new Uint8Array(PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE),
|
|
472
|
+
},
|
|
473
|
+
overwrite: true,
|
|
474
|
+
append: false,
|
|
475
|
+
ui_percentage: 100,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(frame.length).toBeLessThanOrEqual(PROTOCOL_V2_BLE_FRAME_MAX_BYTES);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
270
482
|
test('keeps bytes after the first complete frame for the next read', () => {
|
|
271
483
|
const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
272
484
|
version: 1,
|
|
@@ -312,12 +524,90 @@ describe('Protocol V2 framing and session', () => {
|
|
|
312
524
|
type: 'ProtocolInfo',
|
|
313
525
|
message: {
|
|
314
526
|
version: 2,
|
|
527
|
+
build_fingerprint: null,
|
|
315
528
|
supported_messages: [60206],
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('session decodes legacy ProtocolInfo through the generic frame boundary', async () => {
|
|
534
|
+
const productionSchemas = {
|
|
535
|
+
protocolV1: protocolV1Messages,
|
|
536
|
+
protocolV2: productionProtocolV2Messages,
|
|
537
|
+
};
|
|
538
|
+
const response = protocolV2.encodeProtobufFrame(60201, new Uint8Array([0x08, 0x01]));
|
|
539
|
+
const session = new ProtocolV2Session({
|
|
540
|
+
schemas: productionSchemas,
|
|
541
|
+
router: 1,
|
|
542
|
+
writeFrame: () => Promise.resolve(),
|
|
543
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await expect(
|
|
547
|
+
session.call(
|
|
548
|
+
'ProtocolInfoRequest',
|
|
549
|
+
{ eventless_wallet_session: true },
|
|
550
|
+
{
|
|
551
|
+
expectedTypes: ['ProtocolInfo'],
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
).resolves.toEqual({
|
|
555
|
+
type: 'ProtocolInfo',
|
|
556
|
+
message: {
|
|
557
|
+
version: 1,
|
|
558
|
+
build_fingerprint: '',
|
|
559
|
+
supported_messages: [],
|
|
316
560
|
protobuf_definition: null,
|
|
317
561
|
},
|
|
318
562
|
});
|
|
319
563
|
});
|
|
320
564
|
|
|
565
|
+
test('session does not log Protocol V2 TX and RX frames', async () => {
|
|
566
|
+
const written = [];
|
|
567
|
+
const logger = { debug: jest.fn() };
|
|
568
|
+
const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
569
|
+
version: 2,
|
|
570
|
+
build_fingerprint: 'application__1.0.0__sensitive__DEV__DEBUG',
|
|
571
|
+
supported_messages: [60206],
|
|
572
|
+
});
|
|
573
|
+
const session = new ProtocolV2Session({
|
|
574
|
+
schemas,
|
|
575
|
+
router: 1,
|
|
576
|
+
logger,
|
|
577
|
+
logPrefix: 'ProtocolV2 Test',
|
|
578
|
+
writeFrame: frame => {
|
|
579
|
+
written.push(frame);
|
|
580
|
+
return Promise.resolve();
|
|
581
|
+
},
|
|
582
|
+
readFrame: () =>
|
|
583
|
+
Promise.resolve(rewriteSeq(response, protocolV2.decodeFrame(written[0]).seq)),
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
await session.call('ProtocolInfoRequest', {});
|
|
587
|
+
|
|
588
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('session does not log RX frame metadata when protobuf payload decoding fails', async () => {
|
|
592
|
+
const logger = { debug: jest.fn() };
|
|
593
|
+
const response = protocolV2.encodeProtobufFrame(
|
|
594
|
+
60208,
|
|
595
|
+
hexToBytes('0801121648616e646c6572206e6f742072656769737465726564')
|
|
596
|
+
);
|
|
597
|
+
const session = new ProtocolV2Session({
|
|
598
|
+
schemas,
|
|
599
|
+
router: 1,
|
|
600
|
+
logger,
|
|
601
|
+
logPrefix: 'ProtocolV2 Test',
|
|
602
|
+
writeFrame: () => Promise.resolve(),
|
|
603
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toThrow();
|
|
607
|
+
|
|
608
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
609
|
+
});
|
|
610
|
+
|
|
321
611
|
test('session skips Proto Link ACK frames before decoding the protobuf response', async () => {
|
|
322
612
|
const ack = new Uint8Array(8);
|
|
323
613
|
ack[0] = 0x5a;
|
|
@@ -332,7 +622,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
332
622
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
333
623
|
message: 'ok',
|
|
334
624
|
});
|
|
335
|
-
const readFrame = jest
|
|
625
|
+
const readFrame = jest
|
|
626
|
+
.fn()
|
|
627
|
+
.mockResolvedValueOnce(ack)
|
|
628
|
+
.mockResolvedValueOnce(rewriteSeq(response, 1));
|
|
336
629
|
const session = new ProtocolV2Session({
|
|
337
630
|
schemas,
|
|
338
631
|
router: 1,
|
|
@@ -349,6 +642,96 @@ describe('Protocol V2 framing and session', () => {
|
|
|
349
642
|
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
350
643
|
});
|
|
351
644
|
|
|
645
|
+
test('session accepts a terminal response before an ACK', async () => {
|
|
646
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
647
|
+
message: 'ok',
|
|
648
|
+
});
|
|
649
|
+
const session = new ProtocolV2Session({
|
|
650
|
+
schemas,
|
|
651
|
+
router: 1,
|
|
652
|
+
writeFrame: () => Promise.resolve(),
|
|
653
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
await expect(
|
|
657
|
+
session.call('Ping', { message: 'hello' }, { expectedTypes: ['Success'] })
|
|
658
|
+
).resolves.toEqual({
|
|
659
|
+
type: 'Success',
|
|
660
|
+
message: {
|
|
661
|
+
message: 'ok',
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('session uses the unified response timeout before the first ACK', async () => {
|
|
667
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
668
|
+
message: 'ok',
|
|
669
|
+
});
|
|
670
|
+
const readFrame = jest.fn(() => Promise.resolve(rewriteSeq(response, 1)));
|
|
671
|
+
const session = new ProtocolV2Session({
|
|
672
|
+
schemas,
|
|
673
|
+
router: 1,
|
|
674
|
+
writeFrame: () => Promise.resolve(),
|
|
675
|
+
readFrame,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
await expect(
|
|
679
|
+
session.call(
|
|
680
|
+
'Ping',
|
|
681
|
+
{ message: 'hello' },
|
|
682
|
+
{
|
|
683
|
+
timeoutMs: 12_345,
|
|
684
|
+
expectedTypes: ['Success'],
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
).resolves.toEqual({
|
|
688
|
+
type: 'Success',
|
|
689
|
+
message: {
|
|
690
|
+
message: 'ok',
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
expect(readFrame).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 12_345 }));
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('session rejects an ACK that belongs to another request sequence', async () => {
|
|
697
|
+
const ack = new Uint8Array(8);
|
|
698
|
+
ack[0] = 0x5a;
|
|
699
|
+
ack[1] = 8;
|
|
700
|
+
ack[4] = 1;
|
|
701
|
+
ack[5] = 1;
|
|
702
|
+
ack[6] = 2;
|
|
703
|
+
ack[3] = protocolV2.crc8(ack, 3);
|
|
704
|
+
ack[7] = protocolV2.crc8(ack, 7);
|
|
705
|
+
const session = new ProtocolV2Session({
|
|
706
|
+
schemas,
|
|
707
|
+
router: 1,
|
|
708
|
+
writeFrame: () => Promise.resolve(),
|
|
709
|
+
readFrame: () => Promise.resolve(ack),
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toEqual(
|
|
713
|
+
expect.objectContaining({
|
|
714
|
+
name: 'ProtocolV2LinkError',
|
|
715
|
+
code: 'ack-sequence',
|
|
716
|
+
message: 'Protocol V2 ACK sequence mismatch: expected 1, got 2',
|
|
717
|
+
})
|
|
718
|
+
);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('session rejects a response from another routing channel', async () => {
|
|
722
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'wrong route' });
|
|
723
|
+
const session = new ProtocolV2Session({
|
|
724
|
+
schemas,
|
|
725
|
+
router: 1,
|
|
726
|
+
writeFrame: () => Promise.resolve(),
|
|
727
|
+
readFrame: () => Promise.resolve(response),
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toBeInstanceOf(
|
|
731
|
+
ProtocolV2LinkError
|
|
732
|
+
);
|
|
733
|
+
});
|
|
734
|
+
|
|
352
735
|
test('session rejects BLE frames above its configured frame limit before writing', async () => {
|
|
353
736
|
const writeFrame = jest.fn().mockResolvedValue(undefined);
|
|
354
737
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
@@ -359,7 +742,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
359
742
|
router: 1,
|
|
360
743
|
maxFrameBytes: 2048,
|
|
361
744
|
writeFrame,
|
|
362
|
-
readFrame: () => Promise.resolve(response),
|
|
745
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
363
746
|
});
|
|
364
747
|
|
|
365
748
|
await expect(session.call('Ping', { message: 'x'.repeat(2048) })).rejects.toThrow(
|
|
@@ -368,31 +751,43 @@ describe('Protocol V2 framing and session', () => {
|
|
|
368
751
|
expect(writeFrame).not.toHaveBeenCalled();
|
|
369
752
|
});
|
|
370
753
|
|
|
371
|
-
test('session
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
});
|
|
754
|
+
test('session watchdog covers a write that never settles', async () => {
|
|
755
|
+
const readFrame = jest.fn();
|
|
756
|
+
let writeSignal;
|
|
375
757
|
const session = new ProtocolV2Session({
|
|
376
758
|
schemas,
|
|
377
759
|
router: 1,
|
|
378
|
-
writeFrame: () =>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
readFrame
|
|
760
|
+
writeFrame: (_frame, context) => {
|
|
761
|
+
writeSignal = context.signal;
|
|
762
|
+
return new Promise(() => {});
|
|
763
|
+
},
|
|
764
|
+
readFrame,
|
|
383
765
|
});
|
|
384
766
|
|
|
385
767
|
await expect(
|
|
386
768
|
session.call('Ping', { message: 'hello' }, { timeoutMs: 10, expectedTypes: ['Success'] })
|
|
387
|
-
).
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
769
|
+
).rejects.toThrow('Protocol V2 response timeout after 10ms for Ping');
|
|
770
|
+
expect(writeSignal.aborted).toBe(true);
|
|
771
|
+
expect(readFrame).not.toHaveBeenCalled();
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test('session watchdog covers prepareCall before writing', async () => {
|
|
775
|
+
const writeFrame = jest.fn();
|
|
776
|
+
const session = new ProtocolV2Session({
|
|
777
|
+
schemas,
|
|
778
|
+
router: 1,
|
|
779
|
+
prepareCall: () => new Promise(() => {}),
|
|
780
|
+
writeFrame,
|
|
781
|
+
readFrame: jest.fn(),
|
|
392
782
|
});
|
|
783
|
+
|
|
784
|
+
await expect(session.call('Ping', { message: 'hello' }, { timeoutMs: 10 })).rejects.toThrow(
|
|
785
|
+
'Protocol V2 response timeout after 10ms for Ping'
|
|
786
|
+
);
|
|
787
|
+
expect(writeFrame).not.toHaveBeenCalled();
|
|
393
788
|
});
|
|
394
789
|
|
|
395
|
-
test('session
|
|
790
|
+
test('session logs a device-owned response seq without rejecting the frame', async () => {
|
|
396
791
|
const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
397
792
|
version: 2,
|
|
398
793
|
supported_messages: [],
|
|
@@ -412,14 +807,49 @@ describe('Protocol V2 framing and session', () => {
|
|
|
412
807
|
type: 'ProtocolInfo',
|
|
413
808
|
message: {
|
|
414
809
|
version: 2,
|
|
810
|
+
build_fingerprint: null,
|
|
415
811
|
supported_messages: [],
|
|
416
|
-
protobuf_definition: null,
|
|
417
812
|
},
|
|
418
813
|
});
|
|
419
|
-
expect(logger.debug).
|
|
814
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test('session accepts gaps in the firmware-global response sequence', async () => {
|
|
818
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
819
|
+
const readFrame = jest
|
|
820
|
+
.fn()
|
|
821
|
+
.mockResolvedValueOnce(rewriteSeq(response, 1))
|
|
822
|
+
.mockResolvedValueOnce(rewriteSeq(response, 4));
|
|
823
|
+
const session = new ProtocolV2Session({
|
|
824
|
+
schemas,
|
|
825
|
+
router: 1,
|
|
826
|
+
writeFrame: () => Promise.resolve(),
|
|
827
|
+
readFrame,
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
await session.call('Ping', { message: 'first' });
|
|
831
|
+
await expect(session.call('Ping', { message: 'second' })).resolves.toMatchObject({
|
|
832
|
+
type: 'Success',
|
|
833
|
+
});
|
|
420
834
|
});
|
|
421
835
|
|
|
422
|
-
test('session
|
|
836
|
+
test('session rejects a duplicate device response sequence', async () => {
|
|
837
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
838
|
+
const readFrame = jest.fn(() => Promise.resolve(rewriteSeq(response, 7)));
|
|
839
|
+
const session = new ProtocolV2Session({
|
|
840
|
+
schemas,
|
|
841
|
+
router: 1,
|
|
842
|
+
writeFrame: () => Promise.resolve(),
|
|
843
|
+
readFrame,
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
await session.call('Ping', { message: 'first' });
|
|
847
|
+
await expect(session.call('Ping', { message: 'second' })).rejects.toThrow(
|
|
848
|
+
'Protocol V2 duplicate response sequence: 7'
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test('session does not log transmit or receive payload details', async () => {
|
|
423
853
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
424
854
|
message: 'accepted',
|
|
425
855
|
});
|
|
@@ -430,7 +860,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
430
860
|
schemas,
|
|
431
861
|
router: 1,
|
|
432
862
|
writeFrame: () => Promise.resolve(),
|
|
433
|
-
readFrame: () => Promise.resolve(response),
|
|
863
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
434
864
|
logger,
|
|
435
865
|
logPrefix: 'ProtocolV2 Test',
|
|
436
866
|
});
|
|
@@ -442,30 +872,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
442
872
|
},
|
|
443
873
|
});
|
|
444
874
|
|
|
445
|
-
expect(logger.debug).
|
|
446
|
-
message: 'hello',
|
|
447
|
-
});
|
|
448
|
-
expect(logger.debug).toHaveBeenCalledWith(
|
|
449
|
-
'[ProtocolV2 Test] encode raw frame',
|
|
450
|
-
expect.objectContaining({
|
|
451
|
-
context: 'tx:Ping',
|
|
452
|
-
messageTypeId: 60206,
|
|
453
|
-
router: 1,
|
|
454
|
-
})
|
|
455
|
-
);
|
|
456
|
-
expect(logger.debug).toHaveBeenCalledWith(
|
|
457
|
-
'[ProtocolV2 Test] decode raw frame',
|
|
458
|
-
expect.objectContaining({
|
|
459
|
-
context: 'rx:Ping',
|
|
460
|
-
messageTypeId: 60207,
|
|
461
|
-
})
|
|
462
|
-
);
|
|
463
|
-
expect(logger.debug).toHaveBeenCalledWith(
|
|
464
|
-
'[ProtocolV2 Test] RX payload type=Success messageTypeId=60207',
|
|
465
|
-
{
|
|
466
|
-
message: 'accepted',
|
|
467
|
-
}
|
|
468
|
-
);
|
|
875
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
469
876
|
});
|
|
470
877
|
|
|
471
878
|
test('session suppresses debug logs for file transfer calls', async () => {
|
|
@@ -479,7 +886,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
479
886
|
schemas,
|
|
480
887
|
router: 1,
|
|
481
888
|
writeFrame: () => Promise.resolve(),
|
|
482
|
-
readFrame: () => Promise.resolve(response),
|
|
889
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
483
890
|
logger,
|
|
484
891
|
});
|
|
485
892
|
|
|
@@ -493,6 +900,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
493
900
|
expect(logger.debug).not.toHaveBeenCalled();
|
|
494
901
|
});
|
|
495
902
|
|
|
903
|
+
test('session reports when the complete request frame has been written', async () => {
|
|
904
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
905
|
+
message: 'ok',
|
|
906
|
+
});
|
|
907
|
+
const events = [];
|
|
908
|
+
const onWriteCompleted = jest.fn(metrics => events.push(['written', metrics]));
|
|
909
|
+
const session = new ProtocolV2Session({
|
|
910
|
+
schemas,
|
|
911
|
+
router: 1,
|
|
912
|
+
writeFrame: frame => {
|
|
913
|
+
events.push(['write', frame.byteLength]);
|
|
914
|
+
return Promise.resolve();
|
|
915
|
+
},
|
|
916
|
+
readFrame: () => {
|
|
917
|
+
events.push(['read']);
|
|
918
|
+
return Promise.resolve(rewriteSeq(response, 1));
|
|
919
|
+
},
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
await session.call('Ping', { message: 'metrics' }, { onWriteCompleted });
|
|
923
|
+
|
|
924
|
+
expect(events.map(event => event[0])).toEqual(['write', 'written', 'read']);
|
|
925
|
+
expect(onWriteCompleted).toHaveBeenCalledWith({
|
|
926
|
+
elapsedMs: expect.any(Number),
|
|
927
|
+
frameBytes: expect.any(Number),
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
496
931
|
test('session skips unrelated terminal frames when expected response types are provided', async () => {
|
|
497
932
|
const stale = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
498
933
|
message: 'stale response',
|
|
@@ -504,7 +939,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
504
939
|
const logger = {
|
|
505
940
|
debug: jest.fn(),
|
|
506
941
|
};
|
|
507
|
-
const readFrame = jest
|
|
942
|
+
const readFrame = jest
|
|
943
|
+
.fn()
|
|
944
|
+
.mockResolvedValueOnce(rewriteSeq(stale, 1))
|
|
945
|
+
.mockResolvedValueOnce(rewriteSeq(response, 2));
|
|
508
946
|
const session = new ProtocolV2Session({
|
|
509
947
|
schemas,
|
|
510
948
|
router: 1,
|
|
@@ -519,8 +957,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
519
957
|
type: 'ProtocolInfo',
|
|
520
958
|
message: {
|
|
521
959
|
version: 2,
|
|
960
|
+
build_fingerprint: null,
|
|
522
961
|
supported_messages: [],
|
|
523
|
-
protobuf_definition: null,
|
|
524
962
|
},
|
|
525
963
|
});
|
|
526
964
|
|
|
@@ -541,7 +979,9 @@ describe('Protocol V2 framing and session', () => {
|
|
|
541
979
|
const [writtenFrame] = written;
|
|
542
980
|
const { seq } = protocolV2.decodeFrame(writtenFrame);
|
|
543
981
|
return Promise.resolve(
|
|
544
|
-
readFrame.mock.calls.length === 1
|
|
982
|
+
readFrame.mock.calls.length === 1
|
|
983
|
+
? rewriteSeq(progress, seq)
|
|
984
|
+
: rewriteSeq(success, protocolV2.nextProtoSeq(seq))
|
|
545
985
|
);
|
|
546
986
|
});
|
|
547
987
|
const session = new ProtocolV2Session({
|
|
@@ -718,7 +1158,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
718
1158
|
}
|
|
719
1159
|
return Promise.resolve();
|
|
720
1160
|
},
|
|
721
|
-
readFrame: () => Promise.resolve(response),
|
|
1161
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
722
1162
|
});
|
|
723
1163
|
|
|
724
1164
|
await expect(session.call('Ping', { message: '1' })).rejects.toThrow('transport write failed');
|
|
@@ -787,6 +1227,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
787
1227
|
const writeContexts = [];
|
|
788
1228
|
const readContexts = [];
|
|
789
1229
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
1230
|
+
let responseSeq = 0;
|
|
790
1231
|
const session = new ProtocolV2Session({
|
|
791
1232
|
schemas,
|
|
792
1233
|
router: 1,
|
|
@@ -797,18 +1238,67 @@ describe('Protocol V2 framing and session', () => {
|
|
|
797
1238
|
},
|
|
798
1239
|
readFrame: context => {
|
|
799
1240
|
readContexts.push(context);
|
|
800
|
-
|
|
1241
|
+
responseSeq = protocolV2.nextProtoSeq(responseSeq);
|
|
1242
|
+
return Promise.resolve(rewriteSeq(response, responseSeq));
|
|
801
1243
|
},
|
|
802
1244
|
});
|
|
803
1245
|
|
|
804
1246
|
await session.call('Ping', { message: 'ping' }, { timeoutMs: 123 });
|
|
805
|
-
await session.call('FileWrite', {}, { timeoutMs: 456 });
|
|
1247
|
+
await session.call('FileWrite', {}, { timeoutMs: 456, writeWithResponse: true });
|
|
1248
|
+
await session.call('Ping', { message: 'default-timeout' });
|
|
806
1249
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1250
|
+
const normalizeContext = ({ signal, ...context }) => ({
|
|
1251
|
+
...context,
|
|
1252
|
+
signalAborted: signal.aborted,
|
|
1253
|
+
});
|
|
1254
|
+
expect(writeContexts.map(normalizeContext)).toEqual([
|
|
1255
|
+
{
|
|
1256
|
+
messageName: 'Ping',
|
|
1257
|
+
timeoutMs: 123,
|
|
1258
|
+
highThroughput: false,
|
|
1259
|
+
generation: 7,
|
|
1260
|
+
signalAborted: false,
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
messageName: 'FileWrite',
|
|
1264
|
+
timeoutMs: 456,
|
|
1265
|
+
highThroughput: true,
|
|
1266
|
+
writeWithResponse: true,
|
|
1267
|
+
generation: 7,
|
|
1268
|
+
signalAborted: false,
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
messageName: 'Ping',
|
|
1272
|
+
timeoutMs: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
1273
|
+
highThroughput: false,
|
|
1274
|
+
generation: 7,
|
|
1275
|
+
signalAborted: false,
|
|
1276
|
+
},
|
|
1277
|
+
]);
|
|
1278
|
+
expect(readContexts.map(normalizeContext)).toEqual([
|
|
1279
|
+
{
|
|
1280
|
+
messageName: 'Ping',
|
|
1281
|
+
timeoutMs: 123,
|
|
1282
|
+
highThroughput: false,
|
|
1283
|
+
generation: 7,
|
|
1284
|
+
signalAborted: false,
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
messageName: 'FileWrite',
|
|
1288
|
+
timeoutMs: 456,
|
|
1289
|
+
highThroughput: true,
|
|
1290
|
+
writeWithResponse: true,
|
|
1291
|
+
generation: 7,
|
|
1292
|
+
signalAborted: false,
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
messageName: 'Ping',
|
|
1296
|
+
timeoutMs: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
1297
|
+
highThroughput: false,
|
|
1298
|
+
generation: 7,
|
|
1299
|
+
signalAborted: false,
|
|
1300
|
+
},
|
|
810
1301
|
]);
|
|
811
|
-
expect(readContexts).toEqual(writeContexts);
|
|
812
1302
|
});
|
|
813
1303
|
|
|
814
1304
|
test('assembler throws and resets on frames with an impossible length field', () => {
|