@onekeyfe/hd-core 1.2.0-alpha.101 → 1.2.0-alpha.103
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__/base64Data.test.ts +70 -0
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +2 -0
- package/__tests__/device-pool-state.test.ts +29 -0
- package/__tests__/deviceUploadNft.test.ts +33 -4
- package/__tests__/get-device-state.test.ts +102 -13
- package/__tests__/protocol-v2-legacy-recovery.test.ts +29 -0
- package/__tests__/protocol-v2.test.ts +309 -17
- package/__tests__/refresh-device-state.test.ts +4 -1
- package/__tests__/resourceBase64Boundary.test.ts +48 -0
- package/__tests__/search-devices.test.ts +2 -0
- package/__tests__/ton-sign-message.test.ts +84 -0
- package/dist/api/FirmwareUpdateV4.d.ts +3 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetDeviceState.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
- package/dist/api/helpers/base64Data.d.ts +16 -0
- package/dist/api/helpers/base64Data.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/protocolV2LegacyRecovery.d.ts +5 -0
- package/dist/core/protocolV2LegacyRecovery.d.ts.map +1 -0
- package/dist/device/Device.d.ts +9 -2
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/index.d.ts +16 -14
- package/dist/index.js +266 -86
- package/dist/protocols/protocol-v2/features.d.ts +8 -0
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +2 -2
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/types/api/getDeviceState.d.ts +1 -0
- package/dist/types/api/getDeviceState.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +1 -1
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/utils/pro2Nft.d.ts +7 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/api/FirmwareUpdateV4.ts +70 -23
- package/src/api/GetDeviceState.ts +1 -0
- package/src/api/SearchDevices.ts +2 -0
- package/src/api/UploadPortfolio.ts +9 -2
- package/src/api/firmware/protocolV2Release.ts +1 -0
- package/src/api/helpers/base64Data.ts +85 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +56 -8
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +37 -18
- package/src/api/ton/TonSignMessage.ts +5 -3
- package/src/core/index.ts +4 -0
- package/src/core/protocolV2LegacyRecovery.ts +12 -0
- package/src/device/Device.ts +78 -20
- package/src/device/DevicePool.ts +17 -5
- package/src/protocols/protocol-v2/features.ts +10 -0
- package/src/protocols/protocol-v2/index.ts +2 -0
- package/src/types/api/getDeviceState.ts +2 -0
- package/src/types/api/protocolV2.ts +1 -1
- package/src/utils/pro2Nft.ts +31 -8
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
|
|
3
|
+
import { encode as encodeJpeg } from 'jpeg-js';
|
|
4
|
+
|
|
5
|
+
import { decodeCanonicalBase64, decodeJpegBase64ToRgba } from '../src/api/helpers/base64Data';
|
|
6
|
+
|
|
7
|
+
const createJpegBase64 = (width: number, height: number) => {
|
|
8
|
+
const rgba = new Uint8Array(width * height * 4).fill(0xff);
|
|
9
|
+
return encodeJpeg({ width, height, data: rgba }, 80).data.toString('base64');
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('Base64 resource data', () => {
|
|
13
|
+
test('decodes canonical Base64 to a detached Uint8Array', () => {
|
|
14
|
+
const decoded = decodeCanonicalBase64({
|
|
15
|
+
value: 'AQID',
|
|
16
|
+
parameterName: 'packageBase64',
|
|
17
|
+
maxBytes: 3,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(decoded).toBeInstanceOf(Uint8Array);
|
|
21
|
+
expect(Array.from(decoded)).toEqual([1, 2, 3]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test.each(['', 'not-base64', 'AB==', 'data:image/jpeg;base64,AQID'])(
|
|
25
|
+
'rejects non-canonical Base64 input %p',
|
|
26
|
+
value => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
decodeCanonicalBase64({ value, parameterName: 'data', maxBytes: 1024 })
|
|
29
|
+
).toThrow();
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
test('rejects oversized Base64 before decoding', () => {
|
|
34
|
+
const value = Buffer.alloc(64 * 1024 + 17).toString('base64');
|
|
35
|
+
expect(() =>
|
|
36
|
+
decodeCanonicalBase64({ value, parameterName: 'data', maxBytes: 64 * 1024 })
|
|
37
|
+
).toThrow('maximum supported size');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('decodes and validates a JPEG with the expected dimensions', () => {
|
|
41
|
+
const decoded = decodeJpegBase64ToRgba({
|
|
42
|
+
jpegBase64: createJpegBase64(2, 1),
|
|
43
|
+
parameterName: 'jpegBase64',
|
|
44
|
+
expectedWidth: 2,
|
|
45
|
+
expectedHeight: 1,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(decoded).toMatchObject({ width: 2, height: 1 });
|
|
49
|
+
expect(decoded.data).toHaveLength(8);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('rejects non-JPEG bytes and unexpected dimensions', () => {
|
|
53
|
+
expect(() =>
|
|
54
|
+
decodeJpegBase64ToRgba({
|
|
55
|
+
jpegBase64: 'AQID',
|
|
56
|
+
parameterName: 'jpegBase64',
|
|
57
|
+
expectedWidth: 2,
|
|
58
|
+
expectedHeight: 1,
|
|
59
|
+
})
|
|
60
|
+
).toThrow('JPEG image');
|
|
61
|
+
expect(() =>
|
|
62
|
+
decodeJpegBase64ToRgba({
|
|
63
|
+
jpegBase64: createJpegBase64(2, 1),
|
|
64
|
+
parameterName: 'jpegBase64',
|
|
65
|
+
expectedWidth: 1,
|
|
66
|
+
expectedHeight: 1,
|
|
67
|
+
})
|
|
68
|
+
).toThrow('1x1');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -346,6 +346,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
346
346
|
expect(typedCall).not.toHaveBeenCalled();
|
|
347
347
|
expect(getDeviceState).toHaveBeenCalledWith({
|
|
348
348
|
refreshSections: ['identity', 'versions'],
|
|
349
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
349
350
|
});
|
|
350
351
|
expect(method.getSupportedProtocols()).toEqual(['V1', 'V2']);
|
|
351
352
|
});
|
|
@@ -725,6 +726,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
725
726
|
});
|
|
726
727
|
expect(getDeviceState).toHaveBeenCalledWith({
|
|
727
728
|
refreshSections: ['identity', 'versions', 'verification'],
|
|
729
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
728
730
|
});
|
|
729
731
|
});
|
|
730
732
|
|
|
@@ -38,6 +38,35 @@ describe('DevicePool state lifecycle', () => {
|
|
|
38
38
|
expect(getDeviceState).toHaveBeenNthCalledWith(2, { refreshSections: ['settings'] });
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
test('keeps legacy ProtocolInfo compatibility scoped to a discovery refresh', async () => {
|
|
42
|
+
const getDeviceState = jest.fn().mockResolvedValue({ status: { mode: 'bootloader' } });
|
|
43
|
+
const run = jest.fn(async (callback: () => Promise<void>) => callback());
|
|
44
|
+
const device = {
|
|
45
|
+
isProtocolV2: () => true,
|
|
46
|
+
getDeviceState,
|
|
47
|
+
run,
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
await (DevicePool as any)._refreshRuntimeState(device, {
|
|
51
|
+
refreshRuntimeState: true,
|
|
52
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(run).toHaveBeenCalledWith(expect.any(Function), {
|
|
56
|
+
connectProtocol: undefined,
|
|
57
|
+
forceProtocolDetection: undefined,
|
|
58
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
59
|
+
});
|
|
60
|
+
expect(getDeviceState).toHaveBeenNthCalledWith(1, {
|
|
61
|
+
refreshSections: ['status'],
|
|
62
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
63
|
+
});
|
|
64
|
+
expect(getDeviceState).toHaveBeenNthCalledWith(2, {
|
|
65
|
+
refreshSections: ['settings'],
|
|
66
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
41
70
|
test('actively re-detects a cached device when protocol detection is forced', async () => {
|
|
42
71
|
const descriptor = { path: 'cached-path', protocolType: 'V1' } as any;
|
|
43
72
|
const device = Device.fromDescriptor(descriptor);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { encode as encodeJpeg } from 'jpeg-js';
|
|
2
3
|
|
|
3
4
|
import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
|
|
4
5
|
|
|
@@ -13,6 +14,19 @@ const createRgba = (width: number, height: number) => {
|
|
|
13
14
|
return data;
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
const jpegBase64Cache = new Map<string, string>();
|
|
18
|
+
|
|
19
|
+
const createJpegBase64 = (width: number, height: number) => {
|
|
20
|
+
const key = `${width}x${height}`;
|
|
21
|
+
const cached = jpegBase64Cache.get(key);
|
|
22
|
+
if (cached) return cached;
|
|
23
|
+
const value = encodeJpeg({ width, height, data: createRgba(width, height) }, 80).data.toString(
|
|
24
|
+
'base64'
|
|
25
|
+
);
|
|
26
|
+
jpegBase64Cache.set(key, value);
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
|
|
16
30
|
const createMethod = ({
|
|
17
31
|
typedCall,
|
|
18
32
|
supportedMessages = [60802, 60805, 60808, 61500],
|
|
@@ -26,8 +40,8 @@ const createMethod = ({
|
|
|
26
40
|
id: 1,
|
|
27
41
|
payload: {
|
|
28
42
|
method: 'deviceUploadNft',
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
imageJpegBase64: createJpegBase64(540, 540),
|
|
44
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
31
45
|
title: 'CryptoPunk #3100',
|
|
32
46
|
subtitle: 'CryptoPunks',
|
|
33
47
|
timestampMs: 1_760_000_000_000,
|
|
@@ -86,8 +100,8 @@ describe('DeviceUploadNft', () => {
|
|
|
86
100
|
id: 1,
|
|
87
101
|
payload: {
|
|
88
102
|
method: 'deviceUploadNft',
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
imageJpegBase64: createJpegBase64(540, 540),
|
|
104
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
91
105
|
title: 'CryptoPunk #3100',
|
|
92
106
|
subtitle: 'CryptoPunks',
|
|
93
107
|
timestampMs: 1_760_000_000_000,
|
|
@@ -102,6 +116,21 @@ describe('DeviceUploadNft', () => {
|
|
|
102
116
|
});
|
|
103
117
|
});
|
|
104
118
|
|
|
119
|
+
test('rejects invalid image Base64 before device communication', () => {
|
|
120
|
+
const method = new DeviceUploadNft({
|
|
121
|
+
id: 1,
|
|
122
|
+
payload: {
|
|
123
|
+
method: 'deviceUploadNft',
|
|
124
|
+
imageJpegBase64: 'not-base64',
|
|
125
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
126
|
+
title: 'NFT',
|
|
127
|
+
subtitle: '',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(() => method.init()).toThrow('canonical Base64');
|
|
132
|
+
});
|
|
133
|
+
|
|
105
134
|
test('uploads the triplet in order without creating the firmware-owned directory', async () => {
|
|
106
135
|
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
107
136
|
if (request === 'FilesystemPathInfoQuery') {
|
|
@@ -202,25 +202,111 @@ describe('getDeviceState', () => {
|
|
|
202
202
|
expect(state.versions.se01).toBe('1.0.0');
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
test.each([
|
|
206
|
-
'
|
|
207
|
-
|
|
205
|
+
test.each([
|
|
206
|
+
['bootloader', EDeviceType.Pro2, DeviceType.PRO2],
|
|
207
|
+
['romloader', EDeviceType.Pro2, DeviceType.PRO2],
|
|
208
|
+
['bootloader', EDeviceType.Neo, DeviceType.NEO],
|
|
209
|
+
['romloader', EDeviceType.Neo, DeviceType.NEO],
|
|
210
|
+
] as const)(
|
|
211
|
+
'refreshes cached %s state after %s reboots into the application',
|
|
212
|
+
async (mode, deviceType, protocolV2DeviceType) => {
|
|
208
213
|
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
209
|
-
if (requestType === '
|
|
214
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
215
|
+
return { message: protocolV2ApplicationInfo };
|
|
216
|
+
}
|
|
217
|
+
if (requestType === 'DeviceStatusGet') {
|
|
210
218
|
return {
|
|
211
|
-
message: {
|
|
212
|
-
|
|
219
|
+
message: { init_states: true, unlocked: true, device_id: 'wallet-1' },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
223
|
+
});
|
|
224
|
+
const device = createV2Device(typedCall);
|
|
225
|
+
device.updateState(
|
|
226
|
+
{
|
|
227
|
+
protocol: 'V2',
|
|
228
|
+
identity: { deviceType },
|
|
229
|
+
status: { mode },
|
|
230
|
+
raw: {
|
|
231
|
+
protocolV2DeviceInfo: {
|
|
232
|
+
hw: { Device_type: protocolV2DeviceType, serial_no: 'SERIAL-1' },
|
|
213
233
|
fw:
|
|
214
234
|
mode === 'romloader'
|
|
215
235
|
? { romloader: { version: '1.0.0' } }
|
|
216
236
|
: { bootloader: { version: '1.0.0' } },
|
|
217
237
|
},
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
protocolV2ProtocolInfo: getProtocolV2LoaderInfo(mode),
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
'initialize'
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const state = await device.getDeviceState({ refreshSections: ['status'] });
|
|
245
|
+
|
|
246
|
+
expect(state.status.mode).toBe('normal');
|
|
247
|
+
expect(state.identity.deviceId).toBe('wallet-1');
|
|
248
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
249
|
+
'ProtocolInfoRequest',
|
|
250
|
+
'DeviceStatusGet',
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
test.each([
|
|
256
|
+
[EDeviceType.Pro2, DeviceType.PRO2],
|
|
257
|
+
[EDeviceType.Neo, DeviceType.NEO],
|
|
258
|
+
] as const)('invalidates cached loader state when %s is cancelled', async (deviceType, type) => {
|
|
259
|
+
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
260
|
+
if (requestType === 'DeviceInfoGet') {
|
|
261
|
+
return {
|
|
262
|
+
message: {
|
|
263
|
+
protocol_version: 2,
|
|
264
|
+
hw: { Device_type: type, serial_no: 'SERIAL-1' },
|
|
265
|
+
fw: { application: { version: '5.0.0' } },
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
270
|
+
return { message: protocolV2ApplicationInfo };
|
|
271
|
+
}
|
|
272
|
+
if (requestType === 'DeviceStatusGet') {
|
|
273
|
+
return {
|
|
274
|
+
message: { init_states: true, unlocked: true, device_id: 'wallet-1' },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
278
|
+
});
|
|
279
|
+
const cancel = jest.fn().mockResolvedValue(undefined);
|
|
280
|
+
const device = createV2Device(typedCall);
|
|
281
|
+
(device as any).commands = { cancel, typedCall };
|
|
282
|
+
device.updateState(
|
|
283
|
+
{
|
|
284
|
+
protocol: 'V2',
|
|
285
|
+
identity: { deviceType },
|
|
286
|
+
status: { mode: 'bootloader' },
|
|
287
|
+
raw: { protocolV2ProtocolInfo: getProtocolV2LoaderInfo('bootloader') },
|
|
288
|
+
},
|
|
289
|
+
'initialize'
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
await device.interruptionFromUser();
|
|
293
|
+
await device.initialize();
|
|
294
|
+
|
|
295
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
296
|
+
expect(device.state?.status.mode).toBe('normal');
|
|
297
|
+
expect(device.state?.identity.deviceId).toBe('wallet-1');
|
|
298
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
299
|
+
'DeviceInfoGet',
|
|
300
|
+
'ProtocolInfoRequest',
|
|
301
|
+
'DeviceStatusGet',
|
|
302
|
+
]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test.each(['bootloader', 'romloader'] as const)(
|
|
306
|
+
'keeps live %s mode after refreshing cached loader state',
|
|
307
|
+
async mode => {
|
|
308
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
309
|
+
message: getProtocolV2LoaderInfo(mode),
|
|
224
310
|
});
|
|
225
311
|
const device = createV2Device(typedCall);
|
|
226
312
|
device.updateState(
|
|
@@ -236,7 +322,10 @@ describe('getDeviceState', () => {
|
|
|
236
322
|
const state = await device.getDeviceState({ refreshSections: ['status'] });
|
|
237
323
|
|
|
238
324
|
expect(state.status.mode).toBe(mode);
|
|
239
|
-
expect(typedCall).
|
|
325
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
326
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
327
|
+
eventless_wallet_session: true,
|
|
328
|
+
});
|
|
240
329
|
}
|
|
241
330
|
);
|
|
242
331
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isLegacyProtocolV2FirmwareRecoveryMethod } from '../src/core/protocolV2LegacyRecovery';
|
|
2
|
+
|
|
3
|
+
import type { BaseMethod } from '../src/api/BaseMethod';
|
|
4
|
+
|
|
5
|
+
const createMethod = (
|
|
6
|
+
name: string,
|
|
7
|
+
payload: Record<string, unknown>
|
|
8
|
+
): Pick<BaseMethod, 'name' | 'payload'> => ({
|
|
9
|
+
name,
|
|
10
|
+
payload: { method: name, ...payload },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('Protocol V2 legacy firmware recovery policy', () => {
|
|
14
|
+
test.each([
|
|
15
|
+
['firmware state read', 'getDeviceState', { scope: 'firmware' }],
|
|
16
|
+
['firmware release check', 'checkAllFirmwareRelease', {}],
|
|
17
|
+
['firmware update', 'firmwareUpdateV4', {}],
|
|
18
|
+
])('allows legacy ProtocolInfo for %s', (_label, name, payload) => {
|
|
19
|
+
expect(isLegacyProtocolV2FirmwareRecoveryMethod(createMethod(name, payload))).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test.each([
|
|
23
|
+
['runtime state read', 'getDeviceState', { scope: 'runtime' }],
|
|
24
|
+
['settings state read', 'getDeviceState', { scope: 'settings' }],
|
|
25
|
+
['ordinary API', 'getFeatures', {}],
|
|
26
|
+
])('rejects legacy ProtocolInfo for %s', (_label, name, payload) => {
|
|
27
|
+
expect(isLegacyProtocolV2FirmwareRecoveryMethod(createMethod(name, payload))).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|