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

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 (58) hide show
  1. package/README.md +1 -1
  2. package/__tests__/messages.test.js +122 -0
  3. package/__tests__/protocol-v2-link-manager.test.js +351 -0
  4. package/__tests__/protocol-v2-usb-transport-base.test.js +296 -0
  5. package/__tests__/protocol-v2.test.js +384 -108
  6. package/dist/constants.d.ts +5 -3
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/index.d.ts +920 -369
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +587 -251
  11. package/dist/protocols/index.d.ts +11 -5
  12. package/dist/protocols/index.d.ts.map +1 -1
  13. package/dist/protocols/v2/constants.d.ts +1 -0
  14. package/dist/protocols/v2/constants.d.ts.map +1 -1
  15. package/dist/protocols/v2/decode.d.ts +3 -2
  16. package/dist/protocols/v2/decode.d.ts.map +1 -1
  17. package/dist/protocols/v2/encode.d.ts +2 -3
  18. package/dist/protocols/v2/encode.d.ts.map +1 -1
  19. package/dist/protocols/v2/index.d.ts +0 -1
  20. package/dist/protocols/v2/index.d.ts.map +1 -1
  21. package/dist/protocols/v2/link-manager.d.ts +36 -0
  22. package/dist/protocols/v2/link-manager.d.ts.map +1 -0
  23. package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
  24. package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
  25. package/dist/protocols/v2/session.d.ts +18 -3
  26. package/dist/protocols/v2/session.d.ts.map +1 -1
  27. package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
  28. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
  29. package/dist/serialization/protobuf/decode.d.ts.map +1 -1
  30. package/dist/serialization/protobuf/messages.d.ts.map +1 -1
  31. package/dist/types/messages.d.ts +534 -225
  32. package/dist/types/messages.d.ts.map +1 -1
  33. package/dist/types/transport.d.ts +5 -3
  34. package/dist/types/transport.d.ts.map +1 -1
  35. package/messages-protocol-v2.json +825 -709
  36. package/package.json +3 -3
  37. package/scripts/protobuf-build.sh +96 -310
  38. package/scripts/protobuf-patches/index.js +1 -0
  39. package/scripts/protobuf-types.js +12 -0
  40. package/src/constants.ts +21 -15
  41. package/src/index.ts +8 -0
  42. package/src/protocols/index.ts +25 -39
  43. package/src/protocols/v2/constants.ts +1 -0
  44. package/src/protocols/v2/crc8.ts +1 -1
  45. package/src/protocols/v2/decode.ts +38 -34
  46. package/src/protocols/v2/encode.ts +2 -28
  47. package/src/protocols/v2/index.ts +0 -1
  48. package/src/protocols/v2/link-manager.ts +162 -0
  49. package/src/protocols/v2/sequence-cursor.ts +10 -0
  50. package/src/protocols/v2/session.ts +132 -178
  51. package/src/protocols/v2/usb-transport-base.ts +194 -0
  52. package/src/serialization/protobuf/decode.ts +4 -1
  53. package/src/serialization/protobuf/messages.ts +6 -1
  54. package/src/types/messages.ts +636 -265
  55. package/src/types/transport.ts +3 -3
  56. package/dist/protocols/v2/debug.d.ts +0 -13
  57. package/dist/protocols/v2/debug.d.ts.map +0 -1
  58. package/src/protocols/v2/debug.ts +0 -26
@@ -6,10 +6,14 @@ 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 { decodeFrame as decodeV2Frame, encodeProtobufFrame } from './v2';
9
+ import {
10
+ decodeFrame as decodeV2Frame,
11
+ encodeProtobufFrame,
12
+ getAckSequence,
13
+ isAckFrame,
14
+ } from './v2';
10
15
 
11
16
  import type { Root } from 'protobufjs/light';
12
- import type { ProtocolV2DebugLogger } from './v2';
13
17
 
14
18
  export const PROTOCOL_V2_SYS_MESSAGE_THRESHOLD = 60000;
15
19
 
@@ -23,9 +27,6 @@ type ProtocolV2FrameOptions = {
23
27
  router?: number;
24
28
  /** Sequence number (1-255). Managed per-session by ProtocolV2Session. */
25
29
  seq?: number;
26
- logger?: ProtocolV2DebugLogger;
27
- logPrefix?: string;
28
- context?: string;
29
30
  };
30
31
 
31
32
  const resolveProtocolV2EncodeSchema = (name: string, schemas: ProtocolV2Schemas) => {
@@ -38,6 +39,7 @@ const resolveProtocolV2EncodeSchema = (name: string, schemas: ProtocolV2Schemas)
38
39
  };
39
40
 
40
41
  const PROTOCOL_V2_LEGACY_DECODE_ALLOWLIST = new Set([
42
+ 'Failure',
41
43
  'ButtonRequest',
42
44
  'EntropyRequest',
43
45
  'PinMatrixRequest',
@@ -73,6 +75,21 @@ export const ProtocolV1 = {
73
75
  };
74
76
 
75
77
  export const ProtocolV2 = {
78
+ isAckFrame,
79
+ getAckSequence,
80
+
81
+ inspectFrame(schemas: ProtocolV2Schemas, frame: Uint8Array) {
82
+ const { messageTypeId, pbPayload, seq } = decodeV2Frame(frame);
83
+ const { messageName } = createProtocolV2MessageFromType(messageTypeId, schemas);
84
+ return {
85
+ messageName,
86
+ messageTypeId,
87
+ pbPayload,
88
+ seq,
89
+ type: messageName,
90
+ };
91
+ },
92
+
76
93
  encodeFrame(
77
94
  schemas: ProtocolV2Schemas,
78
95
  name: string,
@@ -86,52 +103,21 @@ export const ProtocolV2 = {
86
103
  const rawPbBuffer = pbBuffer.toBuffer() as unknown as ArrayBuffer;
87
104
  const pbBytes = new Uint8Array(rawPbBuffer);
88
105
 
89
- options.logger?.debug?.(`[${options.logPrefix ?? 'ProtocolV2'}] encode protobuf`, {
90
- context: options.context ?? `encode:${name}`,
91
- messageName: name,
92
- messageTypeId,
93
- pbPayloadLength: pbBytes.length,
94
- packetSrc: options.packetSrc ?? 0,
95
- router: options.router ?? 0,
96
- });
97
-
98
106
  return encodeProtobufFrame(
99
107
  messageTypeId,
100
108
  pbBytes,
101
109
  options.packetSrc,
102
110
  options.router,
103
- {
104
- logger: options.logger,
105
- logPrefix: options.logPrefix,
106
- context: options.context ?? `encode:${name}`,
107
- messageName: name,
108
- },
109
111
  options.seq
110
112
  );
111
113
  },
112
114
 
113
- decodeFrame(
114
- schemas: ProtocolV2Schemas,
115
- frame: Uint8Array,
116
- options: Pick<ProtocolV2FrameOptions, 'logger' | 'logPrefix' | 'context'> = {}
117
- ) {
118
- const { messageTypeId, pbPayload, seq } = decodeV2Frame(frame, {
119
- logger: options.logger,
120
- logPrefix: options.logPrefix,
121
- context: options.context ?? 'decode',
122
- });
123
- const { Message, messageName } = createProtocolV2MessageFromType(messageTypeId, schemas);
115
+ decodeFrame(schemas: ProtocolV2Schemas, frame: Uint8Array) {
116
+ const { messageTypeId, pbPayload, seq, messageName } = this.inspectFrame(schemas, frame);
117
+ const { Message } = createProtocolV2MessageFromType(messageTypeId, schemas);
124
118
  const rxByteBuffer = ByteBuffer.wrap(Buffer.from(pbPayload) as unknown as ArrayBuffer);
125
119
  const message = decodeProtobuf(Message, rxByteBuffer);
126
120
 
127
- options.logger?.debug?.(`[${options.logPrefix ?? 'ProtocolV2'}] decode protobuf`, {
128
- context: options.context ?? `decode:${messageName}`,
129
- messageName,
130
- messageTypeId,
131
- pbPayloadLength: pbPayload.length,
132
- seq,
133
- });
134
-
135
121
  return {
136
122
  message,
137
123
  messageName,
@@ -4,3 +4,4 @@ export const PROTO_HEAD_CRC_SIZE = 8;
4
4
  export const CRC8_INIT = 0x30;
5
5
  export const PACKET_SIZE = 4096;
6
6
  export const PROTO_DATA_TYPE_PACKET = 0;
7
+ export const PROTO_DATA_TYPE_ACK = 1;
@@ -22,7 +22,7 @@ export const CRC8_TABLE = new Uint8Array([
22
22
  ]);
23
23
 
24
24
  /**
25
- * 计算 data len 个字节的 CRC-8,使用与固件侧一致的初始值。
25
+ * Compute CRC-8 over the first len bytes using the firmware-compatible initial value.
26
26
  */
27
27
  export function crc8(data: Uint8Array, len: number): number {
28
28
  let crc = CRC8_INIT;
@@ -1,8 +1,5 @@
1
- import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
1
+ import { PROTO_DATA_TYPE_ACK, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
2
2
  import { crc8 } from './crc8';
3
- import { logProtocolV2Debug } from './debug';
4
-
5
- import type { ProtocolV2FrameDebugOptions } from './debug';
6
3
 
7
4
  export interface ProtoV2Frame {
8
5
  /** Little-endian message type ID */
@@ -13,20 +10,7 @@ export interface ProtoV2Frame {
13
10
  seq: number;
14
11
  }
15
12
 
16
- /**
17
- * Parse and validate a Protocol V2 response frame.
18
- *
19
- * Validates:
20
- * - SOF byte (0x5A)
21
- * - Header CRC (bytes 0-2)
22
- * - Frame CRC (full frame except last byte)
23
- *
24
- * Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
25
- */
26
- export function decodeFrame(
27
- data: Uint8Array,
28
- debugOptions?: ProtocolV2FrameDebugOptions
29
- ): ProtoV2Frame {
13
+ function validateFrame(data: Uint8Array): number {
30
14
  if (data.length < PROTO_HEAD_CRC_SIZE) {
31
15
  throw new Error(`Protocol V2 frame too short: ${data.length} bytes`);
32
16
  }
@@ -43,7 +27,6 @@ export function decodeFrame(
43
27
  throw new Error(`Frame truncated: expected ${frameLen} bytes, got ${data.length}`);
44
28
  }
45
29
 
46
- // Verify pre-header CRC (bytes 0-2)
47
30
  const expectedHeaderCrc = crc8(data, 3);
48
31
  if (data[3] !== expectedHeaderCrc) {
49
32
  throw new Error(
@@ -53,7 +36,6 @@ export function decodeFrame(
53
36
  );
54
37
  }
55
38
 
56
- // Verify frame CRC (all bytes except last)
57
39
  const expectedFrameCrc = crc8(data, frameLen - 1);
58
40
  if (data[frameLen - 1] !== expectedFrameCrc) {
59
41
  throw new Error(
@@ -63,6 +45,42 @@ export function decodeFrame(
63
45
  );
64
46
  }
65
47
 
48
+ return frameLen;
49
+ }
50
+
51
+ export function isAckFrame(data: Uint8Array): boolean {
52
+ // eslint-disable-next-line no-bitwise
53
+ if (data.length < 6 || (data[5] & 0x03) !== PROTO_DATA_TYPE_ACK) {
54
+ return false;
55
+ }
56
+
57
+ const frameLen = validateFrame(data);
58
+ if (frameLen !== PROTO_HEAD_CRC_SIZE) {
59
+ throw new Error(`Invalid Protocol V2 ACK frame length: ${frameLen}`);
60
+ }
61
+ return true;
62
+ }
63
+
64
+ export function getAckSequence(data: Uint8Array): number | undefined {
65
+ if (!isAckFrame(data)) {
66
+ return undefined;
67
+ }
68
+ return data[6];
69
+ }
70
+
71
+ /**
72
+ * Parse and validate a Protocol V2 response frame.
73
+ *
74
+ * Validates:
75
+ * - SOF byte (0x5A)
76
+ * - Header CRC (bytes 0-2)
77
+ * - Frame CRC (full frame except last byte)
78
+ *
79
+ * Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
80
+ */
81
+ export function decodeFrame(data: Uint8Array): ProtoV2Frame {
82
+ const frameLen = validateFrame(data);
83
+
66
84
  const seq = data[6];
67
85
  // Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte
68
86
  const payloadData = data.slice(7, frameLen - 1);
@@ -74,19 +92,5 @@ export function decodeFrame(
74
92
  const messageTypeId = payloadData[0] + payloadData[1] * 256;
75
93
  const pbPayload = payloadData.slice(2);
76
94
 
77
- logProtocolV2Debug(debugOptions, 'decode raw frame', {
78
- frameLen,
79
- dataLength: data.length,
80
- router: data[4],
81
- attr: data[5],
82
- seq,
83
- headerCrc: data[3],
84
- expectedHeaderCrc,
85
- frameCrc: data[frameLen - 1],
86
- expectedFrameCrc,
87
- messageTypeId,
88
- pbPayloadLength: pbPayload.length,
89
- });
90
-
91
95
  return { messageTypeId, pbPayload, seq };
92
96
  }
@@ -1,10 +1,7 @@
1
1
  import { PROTO_DATA_TYPE_PACKET, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
2
2
  import { crc8 } from './crc8';
3
- import { logProtocolV2Debug } from './debug';
4
3
  import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
5
4
 
6
- import type { ProtocolV2FrameDebugOptions } from './debug';
7
-
8
5
  // Default sequence number when callers do not manage one themselves.
9
6
  // Stateful per-session sequencing lives in ProtocolV2Session (session.ts),
10
7
  // which advances the counter via nextProtoSeq() and passes it in explicitly.
@@ -35,7 +32,6 @@ export function encodeFrame(
35
32
  payload: Uint8Array | null,
36
33
  packetSrc?: number,
37
34
  router?: number,
38
- debugOptions?: ProtocolV2FrameDebugOptions,
39
35
  seq?: number
40
36
  ): Uint8Array {
41
37
  const resolvedPacketSrc = packetSrc ?? 0;
@@ -47,7 +43,7 @@ export function encodeFrame(
47
43
  const payloadLen = payload ? payload.length : 0;
48
44
  const frameLen = payloadLen + PROTO_HEAD_CRC_SIZE;
49
45
  if (frameLen > PROTOCOL_V2_FRAME_MAX_BYTES) {
50
- throw new Error(`Protocol V2 frame too large: ${frameLen}`);
46
+ throw new Error(`Protocol V2 frame too large: ${frameLen} > ${PROTOCOL_V2_FRAME_MAX_BYTES}`);
51
47
  }
52
48
  const frame = new Uint8Array(frameLen);
53
49
 
@@ -69,17 +65,6 @@ export function encodeFrame(
69
65
  // CRC8 over entire frame except last byte
70
66
  frame[frameLen - 1] = crc8(frame, frameLen - 1);
71
67
 
72
- logProtocolV2Debug(debugOptions, 'encode raw frame', {
73
- frameLen,
74
- payloadLen,
75
- packetSrc: resolvedPacketSrc,
76
- router: frame[4],
77
- attr: frame[5],
78
- seq: frame[6],
79
- headerCrc: frame[3],
80
- frameCrc: frame[frameLen - 1],
81
- });
82
-
83
68
  return frame;
84
69
  }
85
70
 
@@ -95,22 +80,11 @@ export function encodeProtobufFrame(
95
80
  pbPayload: Uint8Array,
96
81
  packetSrc?: number,
97
82
  router?: number,
98
- debugOptions?: ProtocolV2FrameDebugOptions,
99
83
  seq?: number
100
84
  ): Uint8Array {
101
85
  const payload = new Uint8Array(2 + pbPayload.length);
102
86
  payload[0] = messageTypeId % 256;
103
87
  payload[1] = Math.floor(messageTypeId / 256) % 256;
104
88
  payload.set(pbPayload, 2);
105
- return encodeFrame(
106
- payload,
107
- packetSrc,
108
- router,
109
- {
110
- ...debugOptions,
111
- messageTypeId,
112
- pbPayloadLength: pbPayload.length,
113
- },
114
- seq
115
- );
89
+ return encodeFrame(payload, packetSrc, router, seq);
116
90
  }
@@ -1,6 +1,5 @@
1
1
  export * from './constants';
2
2
  export * from './crc8';
3
- export * from './debug';
4
3
  export * from './encode';
5
4
  export * from './decode';
6
5
  export * from './frame-assembler';
@@ -0,0 +1,162 @@
1
+ import { ProtocolV2SequenceCursor } from './sequence-cursor';
2
+ import { ProtocolV2Session, getErrorMessage } from './session';
3
+
4
+ import type { MessageFromOneKey, TransportCallOptions } from '../../types';
5
+ import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
6
+
7
+ export type ProtocolV2LinkErrorClassification = 'link-fatal' | 'recoverable';
8
+
9
+ export interface ProtocolV2LinkAdapter {
10
+ router: number;
11
+ maxFrameBytes?: number;
12
+ generation: number;
13
+ prepareCall(context: ProtocolV2CallContext): Promise<void> | void;
14
+ writeFrame(frame: Uint8Array, context: ProtocolV2CallContext): Promise<void>;
15
+ readFrame(context: ProtocolV2CallContext): Promise<Uint8Array>;
16
+ reset(reason: string): Promise<void> | void;
17
+ logger?: ProtocolV2SessionOptions['logger'];
18
+ logPrefix?: string;
19
+ createTimeoutError?: ProtocolV2SessionOptions['createTimeoutError'];
20
+ writeTimeoutMs?: number;
21
+ }
22
+
23
+ export type ProtocolV2LinkManagerOptions<Key> = {
24
+ getSchemas: () => ProtocolV2Schemas;
25
+ classifyError: (error: unknown) => ProtocolV2LinkErrorClassification;
26
+ onLinkInvalidated?: (key: Key, reason: string) => Promise<void> | void;
27
+ };
28
+
29
+ type ProtocolV2Link = {
30
+ adapter: ProtocolV2LinkAdapter;
31
+ session: ProtocolV2Session;
32
+ state: {
33
+ invalidatedReason?: string;
34
+ };
35
+ };
36
+
37
+ export class ProtocolV2LinkManager<Key> {
38
+ private readonly links = new Map<Key, ProtocolV2Link>();
39
+
40
+ private readonly sequences = new Map<Key, ProtocolV2SequenceCursor>();
41
+
42
+ private readonly callQueues = new Map<Key, Promise<unknown>>();
43
+
44
+ private readonly options: ProtocolV2LinkManagerOptions<Key>;
45
+
46
+ constructor(options: ProtocolV2LinkManagerOptions<Key>) {
47
+ this.options = options;
48
+ }
49
+
50
+ call(
51
+ key: Key,
52
+ createAdapter: () => ProtocolV2LinkAdapter,
53
+ name: string,
54
+ data: Record<string, unknown>,
55
+ options?: TransportCallOptions
56
+ ): Promise<MessageFromOneKey> {
57
+ const run = () => this.executeCall(key, createAdapter, name, data, options);
58
+ const previous = this.callQueues.get(key) ?? Promise.resolve();
59
+ const result = previous.then(run, run);
60
+ const queue = result.catch(() => undefined);
61
+ this.callQueues.set(key, queue);
62
+ result
63
+ .then(
64
+ () => this.clearSettledCallQueue(key, queue),
65
+ () => this.clearSettledCallQueue(key, queue)
66
+ )
67
+ .catch(() => undefined);
68
+ return result;
69
+ }
70
+
71
+ async invalidateLink(key: Key, reason: string): Promise<void> {
72
+ const link = this.links.get(key);
73
+ if (!link) return;
74
+
75
+ this.links.delete(key);
76
+ link.state.invalidatedReason = reason;
77
+ await link.adapter.reset(reason);
78
+ await this.options.onLinkInvalidated?.(key, reason);
79
+ }
80
+
81
+ async invalidateAllLinks(reason: string): Promise<void> {
82
+ await Promise.all(Array.from(this.links.keys(), key => this.invalidateLink(key, reason)));
83
+ }
84
+
85
+ async dispose(reason: string): Promise<void> {
86
+ await this.invalidateAllLinks(reason);
87
+ this.sequences.clear();
88
+ this.callQueues.clear();
89
+ }
90
+
91
+ private getOrCreateLink(key: Key, createAdapter: () => ProtocolV2LinkAdapter): ProtocolV2Link {
92
+ const existing = this.links.get(key);
93
+ if (existing) return existing;
94
+
95
+ const adapter = createAdapter();
96
+ const state: ProtocolV2Link['state'] = {};
97
+ const assertLinkActive = () => {
98
+ if (state.invalidatedReason) {
99
+ throw new Error(state.invalidatedReason);
100
+ }
101
+ };
102
+ let sequenceCursor = this.sequences.get(key);
103
+ if (!sequenceCursor) {
104
+ sequenceCursor = new ProtocolV2SequenceCursor();
105
+ this.sequences.set(key, sequenceCursor);
106
+ }
107
+ const session = new ProtocolV2Session({
108
+ schemas: this.options.getSchemas(),
109
+ router: adapter.router,
110
+ maxFrameBytes: adapter.maxFrameBytes,
111
+ generation: adapter.generation,
112
+ sequenceCursor,
113
+ prepareCall: async context => {
114
+ assertLinkActive();
115
+ await adapter.prepareCall(context);
116
+ assertLinkActive();
117
+ },
118
+ writeFrame: async (frame, context) => {
119
+ assertLinkActive();
120
+ await adapter.writeFrame(frame, context);
121
+ assertLinkActive();
122
+ },
123
+ readFrame: async context => {
124
+ assertLinkActive();
125
+ const frame = await adapter.readFrame(context);
126
+ assertLinkActive();
127
+ return frame;
128
+ },
129
+ logger: adapter.logger,
130
+ logPrefix: adapter.logPrefix,
131
+ createTimeoutError: adapter.createTimeoutError,
132
+ writeTimeoutMs: adapter.writeTimeoutMs,
133
+ });
134
+ const link = { adapter, session, state };
135
+ this.links.set(key, link);
136
+ return link;
137
+ }
138
+
139
+ private async executeCall(
140
+ key: Key,
141
+ createAdapter: () => ProtocolV2LinkAdapter,
142
+ name: string,
143
+ data: Record<string, unknown>,
144
+ options?: TransportCallOptions
145
+ ): Promise<MessageFromOneKey> {
146
+ try {
147
+ return await this.getOrCreateLink(key, createAdapter).session.call(name, data, options);
148
+ } catch (error) {
149
+ if (this.options.classifyError(error) === 'link-fatal') {
150
+ const errorMessage = getErrorMessage(error) || 'unknown error';
151
+ await this.invalidateLink(key, `Protocol V2 link-fatal error: ${errorMessage}`);
152
+ }
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ private clearSettledCallQueue(key: Key, queue: Promise<unknown>) {
158
+ if (this.callQueues.get(key) === queue) {
159
+ this.callQueues.delete(key);
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,10 @@
1
+ import { nextProtoSeq } from './encode';
2
+
3
+ export class ProtocolV2SequenceCursor {
4
+ private current = 0;
5
+
6
+ next(): number {
7
+ this.current = nextProtoSeq(this.current);
8
+ return this.current;
9
+ }
10
+ }