@acala-network/chopsticks-core 0.9.10 → 0.9.11-2
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/dist/cjs/blockchain/index.d.ts +4 -1
- package/dist/cjs/blockchain/index.js +3 -1
- package/dist/cjs/blockchain/storage-layer.js +3 -3
- package/dist/cjs/blockchain/txpool.js +23 -0
- package/dist/cjs/setup.d.ts +2 -0
- package/dist/cjs/setup.js +2 -1
- package/dist/cjs/utils/index.js +1 -1
- package/dist/cjs/xcm/horizontal.js +9 -0
- package/dist/esm/blockchain/index.d.ts +4 -1
- package/dist/esm/blockchain/index.js +3 -1
- package/dist/esm/blockchain/storage-layer.js +3 -3
- package/dist/esm/blockchain/txpool.js +23 -0
- package/dist/esm/setup.d.ts +2 -0
- package/dist/esm/setup.js +2 -1
- package/dist/esm/utils/index.js +1 -1
- package/dist/esm/xcm/horizontal.js +9 -0
- package/package.json +8 -8
|
@@ -37,6 +37,8 @@ export interface Options {
|
|
|
37
37
|
offchainWorker?: boolean;
|
|
38
38
|
/** Max memory block count */
|
|
39
39
|
maxMemoryBlockCount?: number;
|
|
40
|
+
/** Whether to process queued messages */
|
|
41
|
+
processQueuedMessages?: boolean;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* Local blockchain which provides access to blocks, txpool and methods
|
|
@@ -76,10 +78,11 @@ export declare class Blockchain {
|
|
|
76
78
|
/** For subscribing and managing the head state. */
|
|
77
79
|
readonly headState: HeadState;
|
|
78
80
|
readonly offchainWorker: OffchainWorker | undefined;
|
|
81
|
+
readonly processQueuedMessages: boolean;
|
|
79
82
|
/**
|
|
80
83
|
* @param options - Options for instantiating the blockchain
|
|
81
84
|
*/
|
|
82
|
-
constructor({ api, buildBlockMode, inherentProviders, db, header, mockSignatureHost, allowUnresolvedImports, runtimeLogLevel, registeredTypes, offchainWorker, maxMemoryBlockCount, }: Options);
|
|
85
|
+
constructor({ api, buildBlockMode, inherentProviders, db, header, mockSignatureHost, allowUnresolvedImports, runtimeLogLevel, registeredTypes, offchainWorker, maxMemoryBlockCount, processQueuedMessages, }: Options);
|
|
83
86
|
get head(): Block;
|
|
84
87
|
get txPool(): TxPool;
|
|
85
88
|
get runtimeLogLevel(): number;
|
|
@@ -414,7 +414,7 @@ class Blockchain {
|
|
|
414
414
|
}
|
|
415
415
|
/**
|
|
416
416
|
* @param options - Options for instantiating the blockchain
|
|
417
|
-
*/ constructor({ api, buildBlockMode, inherentProviders, db, header, mockSignatureHost = false, allowUnresolvedImports = false, runtimeLogLevel = 0, registeredTypes = {}, offchainWorker = false, maxMemoryBlockCount = 500 }){
|
|
417
|
+
*/ constructor({ api, buildBlockMode, inherentProviders, db, header, mockSignatureHost = false, allowUnresolvedImports = false, runtimeLogLevel = 0, registeredTypes = {}, offchainWorker = false, maxMemoryBlockCount = 500, processQueuedMessages = true }){
|
|
418
418
|
_class_private_method_init(this, _registerBlock);
|
|
419
419
|
/** API instance, for getting on-chain data. */ _define_property(this, "api", void 0);
|
|
420
420
|
/** Datasource for caching storage and blocks data. */ _define_property(this, "db", void 0);
|
|
@@ -455,6 +455,7 @@ class Blockchain {
|
|
|
455
455
|
writable: true,
|
|
456
456
|
value: void 0
|
|
457
457
|
});
|
|
458
|
+
_define_property(this, "processQueuedMessages", true);
|
|
458
459
|
// first arg is used as cache key
|
|
459
460
|
_class_private_field_init(this, _registryBuilder, {
|
|
460
461
|
writable: true,
|
|
@@ -485,6 +486,7 @@ class Blockchain {
|
|
|
485
486
|
this.offchainWorker = new _offchain.OffchainWorker();
|
|
486
487
|
}
|
|
487
488
|
_class_private_field_set(this, _maxMemoryBlockCount, maxMemoryBlockCount);
|
|
489
|
+
this.processQueuedMessages = processQueuedMessages;
|
|
488
490
|
}
|
|
489
491
|
}
|
|
490
492
|
function registerBlock(block) {
|
|
@@ -290,8 +290,8 @@ class StorageLayer {
|
|
|
290
290
|
};
|
|
291
291
|
const res = [];
|
|
292
292
|
const foundNextKey = (key)=>{
|
|
293
|
-
// make sure keys are unique
|
|
294
|
-
if (!res.includes(key)) {
|
|
293
|
+
// make sure keys are unique and start with the prefix
|
|
294
|
+
if (!res.includes(key) && key.startsWith(prefix)) {
|
|
295
295
|
res.push(key);
|
|
296
296
|
}
|
|
297
297
|
};
|
|
@@ -311,7 +311,7 @@ class StorageLayer {
|
|
|
311
311
|
if (idx !== -1) {
|
|
312
312
|
if (includeFirst) {
|
|
313
313
|
const key = _class_private_field_get(this, _keys)[idx];
|
|
314
|
-
if (key) {
|
|
314
|
+
if (key && key.startsWith(prefix)) {
|
|
315
315
|
foundNextKey(key);
|
|
316
316
|
}
|
|
317
317
|
}
|
|
@@ -20,6 +20,7 @@ _export(exports, {
|
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
const _eventemitter3 = require("eventemitter3");
|
|
23
|
+
const _toU8a = require("@polkadot/util/hex/toU8a");
|
|
23
24
|
const _lodash = /*#__PURE__*/ _interop_require_default(require("lodash"));
|
|
24
25
|
const _index = require("../utils/index.js");
|
|
25
26
|
const _blockbuilder = require("./block-builder.js");
|
|
@@ -210,6 +211,28 @@ class TxPool {
|
|
|
210
211
|
horizontalMessages,
|
|
211
212
|
unsafeBlockHeight
|
|
212
213
|
});
|
|
214
|
+
// with the latest message queue, messages are processed in the upcoming block
|
|
215
|
+
if (!_class_private_field_get(this, _chain).processQueuedMessages) return;
|
|
216
|
+
// if block was built without horizontal or downward messages then skip
|
|
217
|
+
if (_lodash.default.isEmpty(horizontalMessages) && _lodash.default.isEmpty(downwardMessages)) return;
|
|
218
|
+
// messageQueue.bookStateFor
|
|
219
|
+
const prefix = '0xb8753e9383841da95f7b8871e5de326954e062a2cf8df68178ee2e5dbdf00bff';
|
|
220
|
+
const meta = await _class_private_field_get(this, _chain).head.meta;
|
|
221
|
+
const keys = await _class_private_field_get(this, _chain).head.getKeysPaged({
|
|
222
|
+
prefix,
|
|
223
|
+
pageSize: 1000
|
|
224
|
+
});
|
|
225
|
+
for (const key of keys){
|
|
226
|
+
const rawValue = await _class_private_field_get(this, _chain).head.get(key);
|
|
227
|
+
if (!rawValue) continue;
|
|
228
|
+
const message = meta.registry.createType('PalletMessageQueueBookState', (0, _toU8a.hexToU8a)(rawValue)).toJSON();
|
|
229
|
+
if (message.size > 0) {
|
|
230
|
+
logger.info('Queued messages detected, building a new block');
|
|
231
|
+
// build a new block to process the queued messages
|
|
232
|
+
await _class_private_field_get(this, _chain).newBlock();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
213
236
|
} catch (err) {
|
|
214
237
|
logger.error({
|
|
215
238
|
err
|
package/dist/cjs/setup.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type SetupOptions = {
|
|
|
17
17
|
registeredTypes?: RegisteredTypes;
|
|
18
18
|
offchainWorker?: boolean;
|
|
19
19
|
maxMemoryBlockCount?: number;
|
|
20
|
+
processQueuedMessages?: boolean;
|
|
20
21
|
};
|
|
21
22
|
export declare const genesisSetup: (chain: Blockchain, genesis: GenesisProvider) => Promise<void>;
|
|
22
23
|
export declare const processOptions: (options: SetupOptions) => Promise<{
|
|
@@ -33,5 +34,6 @@ export declare const processOptions: (options: SetupOptions) => Promise<{
|
|
|
33
34
|
registeredTypes?: RegisteredTypes | undefined;
|
|
34
35
|
offchainWorker?: boolean | undefined;
|
|
35
36
|
maxMemoryBlockCount?: number | undefined;
|
|
37
|
+
processQueuedMessages?: boolean | undefined;
|
|
36
38
|
}>;
|
|
37
39
|
export declare const setup: (options: SetupOptions) => Promise<Blockchain>;
|
package/dist/cjs/setup.js
CHANGED
|
@@ -129,7 +129,8 @@ const setup = async (options)=>{
|
|
|
129
129
|
runtimeLogLevel: opts.runtimeLogLevel,
|
|
130
130
|
registeredTypes: opts.registeredTypes || {},
|
|
131
131
|
offchainWorker: opts.offchainWorker,
|
|
132
|
-
maxMemoryBlockCount: opts.maxMemoryBlockCount
|
|
132
|
+
maxMemoryBlockCount: opts.maxMemoryBlockCount,
|
|
133
|
+
processQueuedMessages: opts.processQueuedMessages
|
|
133
134
|
});
|
|
134
135
|
if (opts.genesis) {
|
|
135
136
|
await genesisSetup(chain, opts.genesis);
|
package/dist/cjs/utils/index.js
CHANGED
|
@@ -170,5 +170,5 @@ const getCurrentTimestamp = async (chain)=>{
|
|
|
170
170
|
};
|
|
171
171
|
const getSlotDuration = async (chain)=>{
|
|
172
172
|
const meta = await chain.head.meta;
|
|
173
|
-
return meta.consts.babe ? meta.consts.babe.expectedBlockTime.toNumber() : meta.query.aura ? (0, _index.getAuraSlotDuration)(await chain.head.wasm) : 12_000;
|
|
173
|
+
return meta.consts.babe ? meta.consts.babe.expectedBlockTime.toNumber() : meta.query.aura ? (0, _index.getAuraSlotDuration)(await chain.head.wasm) : meta.consts.asyncBacking ? meta.consts.asyncBacking.expectedBlockTime.toNumber() : 12_000;
|
|
174
174
|
};
|
|
@@ -37,5 +37,14 @@ const connectHorizontal = async (parachains)=>{
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
|
+
const hrmpHeads = await chain.head.read('BTreeMap<u32, H256>', meta.query.parachainSystem.lastHrmpMqcHeads);
|
|
41
|
+
if (hrmpHeads && !process.env.DISABLE_AUTO_HRMP) {
|
|
42
|
+
const existingChannels = Array.from(hrmpHeads.keys()).map((x)=>x.toNumber());
|
|
43
|
+
for (const paraId of Object.keys(parachains).filter((x)=>x !== id)){
|
|
44
|
+
if (!existingChannels.includes(Number(paraId))) {
|
|
45
|
+
chain.submitHorizontalMessages(Number(paraId), []);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
50
|
};
|
|
@@ -37,6 +37,8 @@ export interface Options {
|
|
|
37
37
|
offchainWorker?: boolean;
|
|
38
38
|
/** Max memory block count */
|
|
39
39
|
maxMemoryBlockCount?: number;
|
|
40
|
+
/** Whether to process queued messages */
|
|
41
|
+
processQueuedMessages?: boolean;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* Local blockchain which provides access to blocks, txpool and methods
|
|
@@ -76,10 +78,11 @@ export declare class Blockchain {
|
|
|
76
78
|
/** For subscribing and managing the head state. */
|
|
77
79
|
readonly headState: HeadState;
|
|
78
80
|
readonly offchainWorker: OffchainWorker | undefined;
|
|
81
|
+
readonly processQueuedMessages: boolean;
|
|
79
82
|
/**
|
|
80
83
|
* @param options - Options for instantiating the blockchain
|
|
81
84
|
*/
|
|
82
|
-
constructor({ api, buildBlockMode, inherentProviders, db, header, mockSignatureHost, allowUnresolvedImports, runtimeLogLevel, registeredTypes, offchainWorker, maxMemoryBlockCount, }: Options);
|
|
85
|
+
constructor({ api, buildBlockMode, inherentProviders, db, header, mockSignatureHost, allowUnresolvedImports, runtimeLogLevel, registeredTypes, offchainWorker, maxMemoryBlockCount, processQueuedMessages, }: Options);
|
|
83
86
|
get head(): Block;
|
|
84
87
|
get txPool(): TxPool;
|
|
85
88
|
get runtimeLogLevel(): number;
|
|
@@ -51,6 +51,7 @@ const logger = defaultLogger.child({
|
|
|
51
51
|
/** For subscribing and managing the head state. */ headState;
|
|
52
52
|
offchainWorker;
|
|
53
53
|
#maxMemoryBlockCount;
|
|
54
|
+
processQueuedMessages = true;
|
|
54
55
|
// first arg is used as cache key
|
|
55
56
|
#registryBuilder = _.memoize(async (_cacheKey, metadata, version)=>{
|
|
56
57
|
const chain = await this.api.chain;
|
|
@@ -65,7 +66,7 @@ const logger = defaultLogger.child({
|
|
|
65
66
|
});
|
|
66
67
|
/**
|
|
67
68
|
* @param options - Options for instantiating the blockchain
|
|
68
|
-
*/ constructor({ api, buildBlockMode, inherentProviders, db, header, mockSignatureHost = false, allowUnresolvedImports = false, runtimeLogLevel = 0, registeredTypes = {}, offchainWorker = false, maxMemoryBlockCount = 500 }){
|
|
69
|
+
*/ constructor({ api, buildBlockMode, inherentProviders, db, header, mockSignatureHost = false, allowUnresolvedImports = false, runtimeLogLevel = 0, registeredTypes = {}, offchainWorker = false, maxMemoryBlockCount = 500, processQueuedMessages = true }){
|
|
69
70
|
this.api = api;
|
|
70
71
|
this.db = db;
|
|
71
72
|
this.mockSignatureHost = mockSignatureHost;
|
|
@@ -81,6 +82,7 @@ const logger = defaultLogger.child({
|
|
|
81
82
|
this.offchainWorker = new OffchainWorker();
|
|
82
83
|
}
|
|
83
84
|
this.#maxMemoryBlockCount = maxMemoryBlockCount;
|
|
85
|
+
this.processQueuedMessages = processQueuedMessages;
|
|
84
86
|
}
|
|
85
87
|
#registerBlock(block) {
|
|
86
88
|
// if exceed max memory block count, delete the oldest block
|
|
@@ -219,8 +219,8 @@ export class StorageLayer {
|
|
|
219
219
|
};
|
|
220
220
|
const res = [];
|
|
221
221
|
const foundNextKey = (key)=>{
|
|
222
|
-
// make sure keys are unique
|
|
223
|
-
if (!res.includes(key)) {
|
|
222
|
+
// make sure keys are unique and start with the prefix
|
|
223
|
+
if (!res.includes(key) && key.startsWith(prefix)) {
|
|
224
224
|
res.push(key);
|
|
225
225
|
}
|
|
226
226
|
};
|
|
@@ -240,7 +240,7 @@ export class StorageLayer {
|
|
|
240
240
|
if (idx !== -1) {
|
|
241
241
|
if (includeFirst) {
|
|
242
242
|
const key = this.#keys[idx];
|
|
243
|
-
if (key) {
|
|
243
|
+
if (key && key.startsWith(prefix)) {
|
|
244
244
|
foundNextKey(key);
|
|
245
245
|
}
|
|
246
246
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { hexToU8a } from '@polkadot/util/hex/toU8a';
|
|
2
3
|
import _ from 'lodash';
|
|
3
4
|
import { defer } from '../utils/index.js';
|
|
4
5
|
import { buildBlock } from './block-builder.js';
|
|
@@ -155,6 +156,28 @@ export class TxPool {
|
|
|
155
156
|
horizontalMessages,
|
|
156
157
|
unsafeBlockHeight
|
|
157
158
|
});
|
|
159
|
+
// with the latest message queue, messages are processed in the upcoming block
|
|
160
|
+
if (!this.#chain.processQueuedMessages) return;
|
|
161
|
+
// if block was built without horizontal or downward messages then skip
|
|
162
|
+
if (_.isEmpty(horizontalMessages) && _.isEmpty(downwardMessages)) return;
|
|
163
|
+
// messageQueue.bookStateFor
|
|
164
|
+
const prefix = '0xb8753e9383841da95f7b8871e5de326954e062a2cf8df68178ee2e5dbdf00bff';
|
|
165
|
+
const meta = await this.#chain.head.meta;
|
|
166
|
+
const keys = await this.#chain.head.getKeysPaged({
|
|
167
|
+
prefix,
|
|
168
|
+
pageSize: 1000
|
|
169
|
+
});
|
|
170
|
+
for (const key of keys){
|
|
171
|
+
const rawValue = await this.#chain.head.get(key);
|
|
172
|
+
if (!rawValue) continue;
|
|
173
|
+
const message = meta.registry.createType('PalletMessageQueueBookState', hexToU8a(rawValue)).toJSON();
|
|
174
|
+
if (message.size > 0) {
|
|
175
|
+
logger.info('Queued messages detected, building a new block');
|
|
176
|
+
// build a new block to process the queued messages
|
|
177
|
+
await this.#chain.newBlock();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
158
181
|
} catch (err) {
|
|
159
182
|
logger.error({
|
|
160
183
|
err
|
package/dist/esm/setup.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type SetupOptions = {
|
|
|
17
17
|
registeredTypes?: RegisteredTypes;
|
|
18
18
|
offchainWorker?: boolean;
|
|
19
19
|
maxMemoryBlockCount?: number;
|
|
20
|
+
processQueuedMessages?: boolean;
|
|
20
21
|
};
|
|
21
22
|
export declare const genesisSetup: (chain: Blockchain, genesis: GenesisProvider) => Promise<void>;
|
|
22
23
|
export declare const processOptions: (options: SetupOptions) => Promise<{
|
|
@@ -33,5 +34,6 @@ export declare const processOptions: (options: SetupOptions) => Promise<{
|
|
|
33
34
|
registeredTypes?: RegisteredTypes | undefined;
|
|
34
35
|
offchainWorker?: boolean | undefined;
|
|
35
36
|
maxMemoryBlockCount?: number | undefined;
|
|
37
|
+
processQueuedMessages?: boolean | undefined;
|
|
36
38
|
}>;
|
|
37
39
|
export declare const setup: (options: SetupOptions) => Promise<Blockchain>;
|
package/dist/esm/setup.js
CHANGED
|
@@ -108,7 +108,8 @@ export const setup = async (options)=>{
|
|
|
108
108
|
runtimeLogLevel: opts.runtimeLogLevel,
|
|
109
109
|
registeredTypes: opts.registeredTypes || {},
|
|
110
110
|
offchainWorker: opts.offchainWorker,
|
|
111
|
-
maxMemoryBlockCount: opts.maxMemoryBlockCount
|
|
111
|
+
maxMemoryBlockCount: opts.maxMemoryBlockCount,
|
|
112
|
+
processQueuedMessages: opts.processQueuedMessages
|
|
112
113
|
});
|
|
113
114
|
if (opts.genesis) {
|
|
114
115
|
await genesisSetup(chain, opts.genesis);
|
package/dist/esm/utils/index.js
CHANGED
|
@@ -106,5 +106,5 @@ export const getCurrentTimestamp = async (chain)=>{
|
|
|
106
106
|
};
|
|
107
107
|
export const getSlotDuration = async (chain)=>{
|
|
108
108
|
const meta = await chain.head.meta;
|
|
109
|
-
return meta.consts.babe ? meta.consts.babe.expectedBlockTime.toNumber() : meta.query.aura ? getAuraSlotDuration(await chain.head.wasm) : 12_000;
|
|
109
|
+
return meta.consts.babe ? meta.consts.babe.expectedBlockTime.toNumber() : meta.query.aura ? getAuraSlotDuration(await chain.head.wasm) : meta.consts.asyncBacking ? meta.consts.asyncBacking.expectedBlockTime.toNumber() : 12_000;
|
|
110
110
|
};
|
|
@@ -27,5 +27,14 @@ export const connectHorizontal = async (parachains)=>{
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
|
+
const hrmpHeads = await chain.head.read('BTreeMap<u32, H256>', meta.query.parachainSystem.lastHrmpMqcHeads);
|
|
31
|
+
if (hrmpHeads && !process.env.DISABLE_AUTO_HRMP) {
|
|
32
|
+
const existingChannels = Array.from(hrmpHeads.keys()).map((x)=>x.toNumber());
|
|
33
|
+
for (const paraId of Object.keys(parachains).filter((x)=>x !== id)){
|
|
34
|
+
if (!existingChannels.includes(Number(paraId))) {
|
|
35
|
+
chain.submitHorizontalMessages(Number(paraId), []);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acala-network/chopsticks-core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11-2",
|
|
4
4
|
"author": "Acala Developers <hello@acala.network>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"docs:prep": "typedoc"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@acala-network/chopsticks-executor": "0.9.
|
|
15
|
+
"@acala-network/chopsticks-executor": "0.9.11-2",
|
|
16
16
|
"@polkadot/rpc-provider": "^10.11.2",
|
|
17
17
|
"@polkadot/types": "^10.11.2",
|
|
18
18
|
"@polkadot/types-codec": "^10.11.2",
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
"comlink": "^4.4.1",
|
|
23
23
|
"eventemitter3": "^5.0.1",
|
|
24
24
|
"lodash": "^4.17.21",
|
|
25
|
-
"lru-cache": "^10.
|
|
26
|
-
"pino": "^8.
|
|
27
|
-
"pino-pretty": "^
|
|
25
|
+
"lru-cache": "^10.2.0",
|
|
26
|
+
"pino": "^8.19.0",
|
|
27
|
+
"pino-pretty": "^11.0.0",
|
|
28
28
|
"rxjs": "^7.8.1",
|
|
29
29
|
"zod": "^3.22.4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@swc/cli": "0.1.65",
|
|
33
|
-
"@swc/core": "^1.
|
|
34
|
-
"@types/lodash": "^4.
|
|
33
|
+
"@swc/core": "^1.4.8",
|
|
34
|
+
"@types/lodash": "^4.17.0",
|
|
35
35
|
"typescript": "^5.3.3",
|
|
36
|
-
"vitest": "^1.
|
|
36
|
+
"vitest": "^1.4.0"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"dist/esm/**",
|