@aztec/cli 0.0.1-commit.96bb3f7 → 0.0.1-commit.993d240
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/dest/cmds/aztec_node/block_number.js +1 -1
- package/dest/cmds/aztec_node/get_logs.d.ts +30 -4
- package/dest/cmds/aztec_node/get_logs.d.ts.map +1 -1
- package/dest/cmds/aztec_node/get_logs.js +39 -29
- package/dest/cmds/aztec_node/get_node_info.d.ts +1 -1
- package/dest/cmds/aztec_node/get_node_info.d.ts.map +1 -1
- package/dest/cmds/aztec_node/get_node_info.js +0 -2
- package/dest/cmds/aztec_node/index.d.ts +1 -1
- package/dest/cmds/aztec_node/index.d.ts.map +1 -1
- package/dest/cmds/aztec_node/index.js +13 -3
- package/dest/cmds/infrastructure/setup_l2_contract.d.ts +1 -1
- package/dest/cmds/infrastructure/setup_l2_contract.d.ts.map +1 -1
- package/dest/cmds/infrastructure/setup_l2_contract.js +4 -3
- package/dest/cmds/l1/assume_proven_through.d.ts +3 -2
- package/dest/cmds/l1/assume_proven_through.d.ts.map +1 -1
- package/dest/cmds/l1/assume_proven_through.js +3 -5
- package/dest/cmds/l1/compute_genesis_values.d.ts +4 -0
- package/dest/cmds/l1/compute_genesis_values.d.ts.map +1 -0
- package/dest/cmds/l1/compute_genesis_values.js +17 -0
- package/dest/cmds/l1/deploy_l1_contracts_cmd.d.ts +1 -1
- package/dest/cmds/l1/deploy_l1_contracts_cmd.d.ts.map +1 -1
- package/dest/cmds/l1/deploy_l1_contracts_cmd.js +0 -1
- package/dest/cmds/l1/deploy_new_rollup.d.ts +1 -1
- package/dest/cmds/l1/deploy_new_rollup.d.ts.map +1 -1
- package/dest/cmds/l1/deploy_new_rollup.js +2 -4
- package/dest/cmds/l1/index.d.ts +1 -1
- package/dest/cmds/l1/index.d.ts.map +1 -1
- package/dest/cmds/l1/index.js +7 -2
- package/dest/cmds/l1/update_l1_validators.d.ts +3 -3
- package/dest/cmds/l1/update_l1_validators.d.ts.map +1 -1
- package/dest/cmds/l1/update_l1_validators.js +52 -18
- package/dest/config/cached_fetch.d.ts +19 -10
- package/dest/config/cached_fetch.d.ts.map +1 -1
- package/dest/config/cached_fetch.js +110 -32
- package/dest/config/chain_l2_config.d.ts +12 -39
- package/dest/config/chain_l2_config.d.ts.map +1 -1
- package/dest/config/chain_l2_config.js +35 -512
- package/dest/config/generated/networks.d.ts +221 -0
- package/dest/config/generated/networks.d.ts.map +1 -0
- package/dest/config/generated/networks.js +222 -0
- package/dest/config/network_config.d.ts +2 -2
- package/dest/config/network_config.d.ts.map +1 -1
- package/dest/config/network_config.js +7 -3
- package/dest/utils/aztec.d.ts +1 -2
- package/dest/utils/aztec.d.ts.map +1 -1
- package/dest/utils/aztec.js +2 -3
- package/dest/utils/commands.d.ts +14 -6
- package/dest/utils/commands.d.ts.map +1 -1
- package/dest/utils/commands.js +19 -9
- package/dest/utils/inspect.d.ts +1 -1
- package/dest/utils/inspect.d.ts.map +1 -1
- package/dest/utils/inspect.js +10 -6
- package/package.json +35 -30
- package/src/cmds/aztec_node/block_number.ts +1 -1
- package/src/cmds/aztec_node/get_logs.ts +70 -38
- package/src/cmds/aztec_node/get_node_info.ts +0 -2
- package/src/cmds/aztec_node/index.ts +13 -8
- package/src/cmds/infrastructure/setup_l2_contract.ts +5 -4
- package/src/cmds/l1/assume_proven_through.ts +4 -7
- package/src/cmds/l1/compute_genesis_values.ts +29 -0
- package/src/cmds/l1/deploy_l1_contracts_cmd.ts +0 -1
- package/src/cmds/l1/deploy_new_rollup.ts +1 -3
- package/src/cmds/l1/index.ts +23 -4
- package/src/cmds/l1/update_l1_validators.ts +46 -21
- package/src/config/cached_fetch.ts +119 -31
- package/src/config/chain_l2_config.ts +35 -670
- package/src/config/generated/networks.ts +226 -0
- package/src/config/network_config.ts +7 -3
- package/src/utils/aztec.ts +12 -18
- package/src/utils/commands.ts +22 -9
- package/src/utils/inspect.ts +8 -6
|
@@ -2,19 +2,17 @@ import { createEthereumChain, isAnvilTestChain } from '@aztec/ethereum/chain';
|
|
|
2
2
|
import { createExtendedL1Client, getPublicClient } from '@aztec/ethereum/client';
|
|
3
3
|
import { getL1ContractsConfigEnvVars } from '@aztec/ethereum/config';
|
|
4
4
|
import { GSEContract, RollupContract } from '@aztec/ethereum/contracts';
|
|
5
|
-
import {
|
|
5
|
+
import { createL1TxUtils } from '@aztec/ethereum/l1-tx-utils';
|
|
6
6
|
import { EthCheatCodes } from '@aztec/ethereum/test';
|
|
7
7
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
8
8
|
import type { LogFn, Logger } from '@aztec/foundation/log';
|
|
9
9
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
10
|
-
import { RollupAbi, StakingAssetHandlerAbi } from '@aztec/l1-artifacts';
|
|
10
|
+
import { RollupAbi, StakingAssetHandlerAbi, TestERC20Abi } from '@aztec/l1-artifacts';
|
|
11
11
|
import { ZkPassportProofParams } from '@aztec/stdlib/zkpassport';
|
|
12
12
|
|
|
13
|
-
import { encodeFunctionData, formatEther, getContract } from 'viem';
|
|
13
|
+
import { encodeFunctionData, formatEther, getContract, maxUint256 } from 'viem';
|
|
14
14
|
import { generatePrivateKey, mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
|
|
15
15
|
|
|
16
|
-
import { addLeadingHex } from '../../utils/aztec.js';
|
|
17
|
-
|
|
18
16
|
export interface RollupCommandArgs {
|
|
19
17
|
rpcUrls: string[];
|
|
20
18
|
chainId: number;
|
|
@@ -40,7 +38,6 @@ export interface LoggerArgs {
|
|
|
40
38
|
export function generateL1Account() {
|
|
41
39
|
const privateKey = generatePrivateKey();
|
|
42
40
|
const account = privateKeyToAccount(privateKey);
|
|
43
|
-
account.address;
|
|
44
41
|
return {
|
|
45
42
|
privateKey,
|
|
46
43
|
address: account.address,
|
|
@@ -53,8 +50,8 @@ export async function addL1Validator({
|
|
|
53
50
|
privateKey,
|
|
54
51
|
mnemonic,
|
|
55
52
|
attesterAddress,
|
|
53
|
+
withdrawerAddress,
|
|
56
54
|
stakingAssetHandlerAddress,
|
|
57
|
-
merkleProof,
|
|
58
55
|
proofParams,
|
|
59
56
|
blsSecretKey,
|
|
60
57
|
log,
|
|
@@ -63,8 +60,8 @@ export async function addL1Validator({
|
|
|
63
60
|
LoggerArgs & {
|
|
64
61
|
blsSecretKey: bigint; // scalar field element of BN254
|
|
65
62
|
attesterAddress: EthAddress;
|
|
63
|
+
withdrawerAddress: EthAddress;
|
|
66
64
|
proofParams: Buffer;
|
|
67
|
-
merkleProof: string[];
|
|
68
65
|
}) {
|
|
69
66
|
const dualLog = makeDualLog(log, debugLogger);
|
|
70
67
|
const account = getAccount(privateKey, mnemonic);
|
|
@@ -87,33 +84,61 @@ export async function addL1Validator({
|
|
|
87
84
|
});
|
|
88
85
|
|
|
89
86
|
const gseAddress = await rollup.read.getGSE();
|
|
90
|
-
|
|
91
87
|
const gse = new GSEContract(l1Client, gseAddress);
|
|
92
|
-
|
|
93
88
|
const registrationTuple = await gse.makeRegistrationTuple(blsSecretKey);
|
|
94
89
|
|
|
95
|
-
const l1TxUtils =
|
|
90
|
+
const l1TxUtils = createL1TxUtils(l1Client, { logger: debugLogger });
|
|
96
91
|
const proofParamsObj = ZkPassportProofParams.fromBuffer(proofParams);
|
|
97
|
-
const merkleProofArray = merkleProof.map(proof => addLeadingHex(proof));
|
|
98
92
|
|
|
99
|
-
|
|
93
|
+
// Step 1: Claim STK tokens from the faucet
|
|
94
|
+
dualLog(`Claiming STK tokens from faucet`);
|
|
95
|
+
const { receipt: claimReceipt } = await l1TxUtils.sendAndMonitorTransaction({
|
|
100
96
|
to: stakingAssetHandlerAddress.toString(),
|
|
101
97
|
data: encodeFunctionData({
|
|
102
98
|
abi: StakingAssetHandlerAbi,
|
|
103
|
-
functionName: '
|
|
99
|
+
functionName: 'claim',
|
|
100
|
+
args: [proofParamsObj.toViem()],
|
|
101
|
+
}),
|
|
102
|
+
abi: StakingAssetHandlerAbi,
|
|
103
|
+
});
|
|
104
|
+
dualLog(`Claim transaction hash: ${claimReceipt.transactionHash}`);
|
|
105
|
+
await l1Client.waitForTransactionReceipt({ hash: claimReceipt.transactionHash });
|
|
106
|
+
|
|
107
|
+
// Step 2: Approve the rollup to spend STK tokens
|
|
108
|
+
const stakingAssetAddress = await stakingAssetHandler.read.STAKING_ASSET();
|
|
109
|
+
dualLog(`Approving rollup to spend STK tokens`);
|
|
110
|
+
const { receipt: approveReceipt } = await l1TxUtils.sendAndMonitorTransaction({
|
|
111
|
+
to: stakingAssetAddress,
|
|
112
|
+
data: encodeFunctionData({
|
|
113
|
+
abi: TestERC20Abi,
|
|
114
|
+
functionName: 'approve',
|
|
115
|
+
args: [rollupAddress, maxUint256],
|
|
116
|
+
}),
|
|
117
|
+
abi: TestERC20Abi,
|
|
118
|
+
});
|
|
119
|
+
await l1Client.waitForTransactionReceipt({ hash: approveReceipt.transactionHash });
|
|
120
|
+
|
|
121
|
+
// Step 3: Deposit into the rollup to register as a validator
|
|
122
|
+
dualLog(`Depositing into rollup to register validator`);
|
|
123
|
+
const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
|
|
124
|
+
to: rollupAddress,
|
|
125
|
+
data: encodeFunctionData({
|
|
126
|
+
abi: RollupAbi,
|
|
127
|
+
functionName: 'deposit',
|
|
104
128
|
args: [
|
|
105
129
|
attesterAddress.toString(),
|
|
106
|
-
|
|
107
|
-
proofParamsObj.toViem(),
|
|
130
|
+
withdrawerAddress.toString(),
|
|
108
131
|
registrationTuple.publicKeyInG1,
|
|
109
132
|
registrationTuple.publicKeyInG2,
|
|
110
133
|
registrationTuple.proofOfPossession,
|
|
134
|
+
false, // moveWithLatestRollup
|
|
111
135
|
],
|
|
112
136
|
}),
|
|
113
|
-
abi:
|
|
137
|
+
abi: RollupAbi,
|
|
114
138
|
});
|
|
115
|
-
dualLog(`
|
|
139
|
+
dualLog(`Deposit transaction hash: ${receipt.transactionHash}`);
|
|
116
140
|
await l1Client.waitForTransactionReceipt({ hash: receipt.transactionHash });
|
|
141
|
+
|
|
117
142
|
if (isAnvilTestChain(chainId)) {
|
|
118
143
|
dualLog(`Funding validator on L1`);
|
|
119
144
|
const cheatCodes = new EthCheatCodes(rpcUrls, new DateProvider(), debugLogger);
|
|
@@ -168,7 +193,7 @@ export async function addL1ValidatorViaRollup({
|
|
|
168
193
|
|
|
169
194
|
const registrationTuple = await gse.makeRegistrationTuple(blsSecretKey);
|
|
170
195
|
|
|
171
|
-
const l1TxUtils =
|
|
196
|
+
const l1TxUtils = createL1TxUtils(l1Client, { logger: debugLogger });
|
|
172
197
|
|
|
173
198
|
const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
|
|
174
199
|
to: rollupAddress.toString(),
|
|
@@ -215,7 +240,7 @@ export async function removeL1Validator({
|
|
|
215
240
|
const account = getAccount(privateKey, mnemonic);
|
|
216
241
|
const chain = createEthereumChain(rpcUrls, chainId);
|
|
217
242
|
const l1Client = createExtendedL1Client(rpcUrls, account, chain.chainInfo);
|
|
218
|
-
const l1TxUtils =
|
|
243
|
+
const l1TxUtils = createL1TxUtils(l1Client, { logger: debugLogger });
|
|
219
244
|
|
|
220
245
|
dualLog(`Removing validator ${validatorAddress.toString()} from rollup ${rollupAddress.toString()}`);
|
|
221
246
|
const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
|
|
@@ -242,7 +267,7 @@ export async function pruneRollup({
|
|
|
242
267
|
const account = getAccount(privateKey, mnemonic);
|
|
243
268
|
const chain = createEthereumChain(rpcUrls, chainId);
|
|
244
269
|
const l1Client = createExtendedL1Client(rpcUrls, account, chain.chainInfo);
|
|
245
|
-
const l1TxUtils =
|
|
270
|
+
const l1TxUtils = createL1TxUtils(l1Client, { logger: debugLogger });
|
|
246
271
|
|
|
247
272
|
dualLog(`Trying prune`);
|
|
248
273
|
const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
|
|
@@ -1,24 +1,48 @@
|
|
|
1
1
|
import { createLogger } from '@aztec/aztec.js/log';
|
|
2
2
|
|
|
3
|
-
import { mkdir, readFile,
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
4
|
import { dirname } from 'path';
|
|
5
5
|
|
|
6
6
|
export interface CachedFetchOptions {
|
|
7
|
-
/**
|
|
8
|
-
cacheDurationMs: number;
|
|
9
|
-
/** The cache file */
|
|
7
|
+
/** The cache file path for storing data. If not provided, no caching is performed. */
|
|
10
8
|
cacheFile?: string;
|
|
9
|
+
/** Fallback max-age in milliseconds when server sends no Cache-Control header. Defaults to 5 minutes. */
|
|
10
|
+
defaultMaxAgeMs?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Cache metadata stored in a sidecar .meta file alongside the data file. */
|
|
14
|
+
interface CacheMeta {
|
|
15
|
+
etag?: string;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
|
|
20
|
+
|
|
21
|
+
/** Extracts max-age value in milliseconds from a Response's Cache-Control header. Returns undefined if not present. */
|
|
22
|
+
export function parseMaxAge(response: { headers: { get(name: string): string | null } }): number | undefined {
|
|
23
|
+
const cacheControl = response.headers.get('cache-control');
|
|
24
|
+
if (!cacheControl) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const match = cacheControl.match(/max-age=(\d+)/);
|
|
28
|
+
if (!match) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return parseInt(match[1], 10) * 1000;
|
|
11
32
|
}
|
|
12
33
|
|
|
13
34
|
/**
|
|
14
|
-
* Fetches data from a URL with file-based caching
|
|
15
|
-
*
|
|
35
|
+
* Fetches data from a URL with file-based HTTP conditional caching.
|
|
36
|
+
*
|
|
37
|
+
* Data is stored as raw JSON in the cache file (same format as the server returns).
|
|
38
|
+
* Caching metadata (ETag, expiry) is stored in a separate sidecar `.meta` file.
|
|
39
|
+
* This keeps the data file human-readable and backward-compatible with older code.
|
|
16
40
|
*
|
|
17
41
|
* @param url - The URL to fetch from
|
|
18
|
-
* @param
|
|
19
|
-
* @param
|
|
20
|
-
* @param
|
|
21
|
-
* @returns The fetched and parsed JSON data, or undefined if fetch fails
|
|
42
|
+
* @param options - Caching options
|
|
43
|
+
* @param fetch - Fetch implementation (defaults to globalThis.fetch)
|
|
44
|
+
* @param log - Logger instance
|
|
45
|
+
* @returns The fetched and parsed JSON data, or undefined if fetch fails
|
|
22
46
|
*/
|
|
23
47
|
export async function cachedFetch<T = any>(
|
|
24
48
|
url: string,
|
|
@@ -26,42 +50,106 @@ export async function cachedFetch<T = any>(
|
|
|
26
50
|
fetch = globalThis.fetch,
|
|
27
51
|
log = createLogger('cached_fetch'),
|
|
28
52
|
): Promise<T | undefined> {
|
|
29
|
-
const {
|
|
53
|
+
const { cacheFile, defaultMaxAgeMs = DEFAULT_MAX_AGE_MS } = options;
|
|
54
|
+
|
|
55
|
+
// If no cacheFile, just fetch normally without caching
|
|
56
|
+
if (!cacheFile) {
|
|
57
|
+
return fetchAndParse<T>(url, fetch, log);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const metaFile = cacheFile + '.meta';
|
|
30
61
|
|
|
31
|
-
// Try to read
|
|
62
|
+
// Try to read metadata
|
|
63
|
+
let meta: CacheMeta | undefined;
|
|
32
64
|
try {
|
|
33
|
-
|
|
34
|
-
const info = await stat(cacheFile);
|
|
35
|
-
if (info.mtimeMs + cacheDurationMs > Date.now()) {
|
|
36
|
-
const cachedData = JSON.parse(await readFile(cacheFile, 'utf-8'));
|
|
37
|
-
return cachedData;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
65
|
+
meta = JSON.parse(await readFile(metaFile, 'utf-8'));
|
|
40
66
|
} catch {
|
|
41
|
-
log.trace('
|
|
67
|
+
log.trace('No usable cache metadata found');
|
|
42
68
|
}
|
|
43
69
|
|
|
70
|
+
// Try to read cached data
|
|
71
|
+
let cachedData: T | undefined;
|
|
44
72
|
try {
|
|
45
|
-
|
|
73
|
+
cachedData = JSON.parse(await readFile(cacheFile, 'utf-8'));
|
|
74
|
+
} catch {
|
|
75
|
+
log.trace('No usable cached data found');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// If metadata and data exist and cache is fresh, return directly
|
|
79
|
+
if (meta && cachedData !== undefined && meta.expiresAt > Date.now()) {
|
|
80
|
+
return cachedData;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Cache is stale or missing — make a (possibly conditional) request
|
|
84
|
+
try {
|
|
85
|
+
const headers: Record<string, string> = {};
|
|
86
|
+
if (meta?.etag && cachedData !== undefined) {
|
|
87
|
+
headers['If-None-Match'] = meta.etag;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const response = await fetch(url, { headers });
|
|
91
|
+
|
|
92
|
+
if (response.status === 304 && cachedData !== undefined) {
|
|
93
|
+
// Not modified — recompute expiry from new response headers and return cached data
|
|
94
|
+
const maxAgeMs = parseMaxAge(response) ?? defaultMaxAgeMs;
|
|
95
|
+
await writeMetaFile(metaFile, { etag: meta?.etag, expiresAt: Date.now() + maxAgeMs }, log);
|
|
96
|
+
return cachedData;
|
|
97
|
+
}
|
|
98
|
+
|
|
46
99
|
if (!response.ok) {
|
|
47
100
|
log.warn(`Failed to fetch from ${url}: ${response.status} ${response.statusText}`);
|
|
48
|
-
return
|
|
101
|
+
return cachedData;
|
|
49
102
|
}
|
|
50
103
|
|
|
51
|
-
|
|
104
|
+
// 200 — parse new data and cache it
|
|
105
|
+
const data = (await response.json()) as T;
|
|
106
|
+
const maxAgeMs = parseMaxAge(response) ?? defaultMaxAgeMs;
|
|
107
|
+
const etag = response.headers.get('etag') ?? undefined;
|
|
52
108
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} catch (err) {
|
|
59
|
-
log.warn('Failed to cache data on disk: ' + cacheFile, { cacheFile, err });
|
|
60
|
-
}
|
|
109
|
+
await ensureDir(cacheFile, log);
|
|
110
|
+
await Promise.all([
|
|
111
|
+
writeFile(cacheFile, JSON.stringify(data), 'utf-8'),
|
|
112
|
+
writeFile(metaFile, JSON.stringify({ etag, expiresAt: Date.now() + maxAgeMs }), 'utf-8'),
|
|
113
|
+
]);
|
|
61
114
|
|
|
62
115
|
return data;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log.warn(`Failed to fetch from ${url}`, { err });
|
|
118
|
+
return cachedData;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function fetchAndParse<T>(
|
|
123
|
+
url: string,
|
|
124
|
+
fetch: typeof globalThis.fetch,
|
|
125
|
+
log: ReturnType<typeof createLogger>,
|
|
126
|
+
): Promise<T | undefined> {
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(url);
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
log.warn(`Failed to fetch from ${url}: ${response.status} ${response.statusText}`);
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return (await response.json()) as T;
|
|
63
134
|
} catch (err) {
|
|
64
135
|
log.warn(`Failed to fetch from ${url}`, { err });
|
|
65
136
|
return undefined;
|
|
66
137
|
}
|
|
67
138
|
}
|
|
139
|
+
|
|
140
|
+
async function ensureDir(filePath: string, log: ReturnType<typeof createLogger>) {
|
|
141
|
+
try {
|
|
142
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
143
|
+
} catch (err) {
|
|
144
|
+
log.warn('Failed to create cache directory for: ' + filePath, { err });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function writeMetaFile(metaFile: string, meta: CacheMeta, log: ReturnType<typeof createLogger>) {
|
|
149
|
+
try {
|
|
150
|
+
await mkdir(dirname(metaFile), { recursive: true });
|
|
151
|
+
await writeFile(metaFile, JSON.stringify(meta), 'utf-8');
|
|
152
|
+
} catch (err) {
|
|
153
|
+
log.warn('Failed to write cache metadata: ' + metaFile, { err });
|
|
154
|
+
}
|
|
155
|
+
}
|