@chainsafe/lodestar 1.35.0-dev.8644a83c62 → 1.35.0-dev.8689cc3545
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/.git-data.json +1 -1
- package/bin/lodestar.js +3 -0
- package/bin/lodestar.ts +3 -0
- package/lib/applyPreset.d.ts.map +1 -0
- package/lib/cli.d.ts +3 -3
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +1 -1
- package/lib/cli.js.map +1 -1
- package/lib/cmds/beacon/handler.d.ts.map +1 -0
- package/lib/cmds/beacon/handler.js.map +1 -1
- package/lib/cmds/beacon/index.d.ts.map +1 -0
- package/lib/cmds/beacon/initBeaconState.d.ts.map +1 -0
- package/lib/cmds/beacon/initBeaconState.js.map +1 -1
- package/lib/cmds/beacon/initPeerIdAndEnr.d.ts +2 -2
- package/lib/cmds/beacon/initPeerIdAndEnr.d.ts.map +1 -0
- package/lib/cmds/beacon/initPeerIdAndEnr.js +1 -1
- package/lib/cmds/beacon/initPeerIdAndEnr.js.map +1 -1
- package/lib/cmds/beacon/options.d.ts.map +1 -0
- package/lib/cmds/beacon/paths.d.ts.map +1 -0
- package/lib/cmds/bootnode/handler.d.ts +13 -8
- package/lib/cmds/bootnode/handler.d.ts.map +1 -0
- package/lib/cmds/bootnode/handler.js +2 -2
- package/lib/cmds/bootnode/handler.js.map +1 -1
- package/lib/cmds/bootnode/index.d.ts.map +1 -0
- package/lib/cmds/bootnode/options.d.ts.map +1 -0
- package/lib/cmds/bootnode/options.js +2 -1
- package/lib/cmds/bootnode/options.js.map +1 -1
- package/lib/cmds/dev/files.d.ts.map +1 -0
- package/lib/cmds/dev/handler.d.ts.map +1 -0
- package/lib/cmds/dev/handler.js +1 -1
- package/lib/cmds/dev/handler.js.map +1 -1
- package/lib/cmds/dev/index.d.ts.map +1 -0
- package/lib/cmds/dev/options.d.ts.map +1 -0
- package/lib/cmds/index.d.ts.map +1 -0
- package/lib/cmds/lightclient/handler.d.ts.map +1 -0
- package/lib/cmds/lightclient/index.d.ts.map +1 -0
- package/lib/cmds/lightclient/options.d.ts.map +1 -0
- package/lib/cmds/validator/blsToExecutionChange.d.ts.map +1 -0
- package/lib/cmds/validator/blsToExecutionChange.js.map +1 -1
- package/lib/cmds/validator/handler.d.ts.map +1 -0
- package/lib/cmds/validator/handler.js +2 -4
- package/lib/cmds/validator/handler.js.map +1 -1
- package/lib/cmds/validator/import.d.ts.map +1 -0
- package/lib/cmds/validator/index.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/decryptKeystoreDefinitions.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/decryptKeystores/index.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/decryptKeystores/poolSize.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/decryptKeystores/threadPool.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/decryptKeystores/threadPool.js +4 -1
- package/lib/cmds/validator/keymanager/decryptKeystores/threadPool.js.map +1 -1
- package/lib/cmds/validator/keymanager/decryptKeystores/types.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/decryptKeystores/worker.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/impl.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/impl.js +4 -0
- package/lib/cmds/validator/keymanager/impl.js.map +1 -1
- package/lib/cmds/validator/keymanager/interface.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/keystoreCache.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/persistedKeys.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/persistedKeys.js +1 -0
- package/lib/cmds/validator/keymanager/persistedKeys.js.map +1 -1
- package/lib/cmds/validator/keymanager/server.d.ts.map +1 -0
- package/lib/cmds/validator/keymanager/server.js +2 -0
- package/lib/cmds/validator/keymanager/server.js.map +1 -1
- package/lib/cmds/validator/list.d.ts.map +1 -0
- package/lib/cmds/validator/options.d.ts.map +1 -0
- package/lib/cmds/validator/paths.d.ts.map +1 -0
- package/lib/cmds/validator/signers/importExternalKeystores.d.ts.map +1 -0
- package/lib/cmds/validator/signers/index.d.ts.map +1 -0
- package/lib/cmds/validator/signers/logSigners.d.ts.map +1 -0
- package/lib/cmds/validator/slashingProtection/export.d.ts.map +1 -0
- package/lib/cmds/validator/slashingProtection/import.d.ts.map +1 -0
- package/lib/cmds/validator/slashingProtection/index.d.ts.map +1 -0
- package/lib/cmds/validator/slashingProtection/options.d.ts.map +1 -0
- package/lib/cmds/validator/slashingProtection/utils.d.ts.map +1 -0
- package/lib/cmds/validator/voluntaryExit.d.ts.map +1 -0
- package/lib/cmds/validator/voluntaryExit.js +1 -1
- package/lib/cmds/validator/voluntaryExit.js.map +1 -1
- package/lib/config/beaconNodeOptions.d.ts.map +1 -0
- package/lib/config/beaconNodeOptions.js +2 -1
- package/lib/config/beaconNodeOptions.js.map +1 -1
- package/lib/config/beaconParams.d.ts.map +1 -0
- package/lib/config/index.d.ts.map +1 -0
- package/lib/config/peerId.d.ts.map +1 -0
- package/lib/config/types.d.ts.map +1 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js.map +1 -1
- package/lib/migrations/index.d.ts +1 -0
- package/lib/migrations/index.d.ts.map +1 -0
- package/lib/migrations/index.js +1 -1
- package/lib/networks/chiado.d.ts.map +1 -0
- package/lib/networks/dev.d.ts.map +1 -0
- package/lib/networks/ephemery.d.ts.map +1 -0
- package/lib/networks/gnosis.d.ts.map +1 -0
- package/lib/networks/holesky.d.ts.map +1 -0
- package/lib/networks/hoodi.d.ts.map +1 -0
- package/lib/networks/index.d.ts.map +1 -0
- package/lib/networks/index.js +1 -2
- package/lib/networks/index.js.map +1 -1
- package/lib/networks/mainnet.d.ts.map +1 -0
- package/lib/networks/sepolia.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/api.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/builder.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/chain.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/eth1.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/execution.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/index.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/metrics.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/monitoring.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/network.d.ts +1 -0
- package/lib/options/beaconNodeOptions/network.d.ts.map +1 -0
- package/lib/options/beaconNodeOptions/network.js +6 -3
- package/lib/options/beaconNodeOptions/network.js.map +1 -1
- package/lib/options/beaconNodeOptions/sync.d.ts.map +1 -0
- package/lib/options/globalOptions.d.ts.map +1 -0
- package/lib/options/index.d.ts.map +1 -0
- package/lib/options/logOptions.d.ts.map +1 -0
- package/lib/options/paramsOptions.d.ts.map +1 -0
- package/lib/paths/global.d.ts.map +1 -0
- package/lib/paths/rootDir.d.ts.map +1 -0
- package/lib/util/errors.d.ts.map +1 -0
- package/lib/util/ethers.d.ts.map +1 -0
- package/lib/util/feeRecipient.d.ts.map +1 -0
- package/lib/util/file.d.ts.map +1 -0
- package/lib/util/file.js +1 -1
- package/lib/util/file.js.map +1 -1
- package/lib/util/format.d.ts.map +1 -0
- package/lib/util/fs.d.ts.map +1 -0
- package/lib/util/gitData/gitDataPath.d.ts.map +1 -0
- package/lib/util/gitData/index.d.ts.map +1 -0
- package/lib/util/gitData/index.js.map +1 -1
- package/lib/util/gitData/writeGitData.d.ts.map +1 -0
- package/lib/util/index.d.ts +3 -3
- package/lib/util/index.d.ts.map +1 -0
- package/lib/util/index.js +3 -3
- package/lib/util/index.js.map +1 -1
- package/lib/util/jwt.d.ts.map +1 -0
- package/lib/util/lockfile.d.ts.map +1 -0
- package/lib/util/logger.d.ts.map +1 -0
- package/lib/util/object.d.ts.map +1 -0
- package/lib/util/object.js.map +1 -1
- package/lib/util/passphrase.d.ts.map +1 -0
- package/lib/util/process.d.ts.map +1 -0
- package/lib/util/progress.d.ts.map +1 -0
- package/lib/util/proposerConfig.d.ts.map +1 -0
- package/lib/util/proposerConfig.js.map +1 -1
- package/lib/util/pruneOldFilesInDir.d.ts.map +1 -0
- package/lib/util/sleep.d.ts.map +1 -0
- package/lib/util/stripOffNewlines.d.ts.map +1 -0
- package/lib/util/types.d.ts.map +1 -0
- package/lib/util/version.d.ts.map +1 -0
- package/package.json +20 -19
- package/src/applyPreset.ts +91 -0
- package/src/cli.ts +56 -0
- package/src/cmds/beacon/handler.ts +267 -0
- package/src/cmds/beacon/index.ts +18 -0
- package/src/cmds/beacon/initBeaconState.ts +275 -0
- package/src/cmds/beacon/initPeerIdAndEnr.ts +199 -0
- package/src/cmds/beacon/options.ts +214 -0
- package/src/cmds/beacon/paths.ts +62 -0
- package/src/cmds/bootnode/handler.ts +203 -0
- package/src/cmds/bootnode/index.ts +13 -0
- package/src/cmds/bootnode/options.ts +109 -0
- package/src/cmds/dev/files.ts +52 -0
- package/src/cmds/dev/handler.ts +86 -0
- package/src/cmds/dev/index.ts +18 -0
- package/src/cmds/dev/options.ts +110 -0
- package/src/cmds/index.ts +15 -0
- package/src/cmds/lightclient/handler.ts +36 -0
- package/src/cmds/lightclient/index.ts +18 -0
- package/src/cmds/lightclient/options.ts +21 -0
- package/src/cmds/validator/blsToExecutionChange.ts +91 -0
- package/src/cmds/validator/handler.ts +300 -0
- package/src/cmds/validator/import.ts +111 -0
- package/src/cmds/validator/index.ts +28 -0
- package/src/cmds/validator/keymanager/decryptKeystoreDefinitions.ts +189 -0
- package/src/cmds/validator/keymanager/decryptKeystores/index.ts +1 -0
- package/src/cmds/validator/keymanager/decryptKeystores/poolSize.ts +16 -0
- package/src/cmds/validator/keymanager/decryptKeystores/threadPool.ts +75 -0
- package/src/cmds/validator/keymanager/decryptKeystores/types.ts +12 -0
- package/src/cmds/validator/keymanager/decryptKeystores/worker.ts +24 -0
- package/src/cmds/validator/keymanager/impl.ts +425 -0
- package/src/cmds/validator/keymanager/interface.ts +35 -0
- package/src/cmds/validator/keymanager/keystoreCache.ts +91 -0
- package/src/cmds/validator/keymanager/persistedKeys.ts +268 -0
- package/src/cmds/validator/keymanager/server.ts +86 -0
- package/src/cmds/validator/list.ts +35 -0
- package/src/cmds/validator/options.ts +461 -0
- package/src/cmds/validator/paths.ts +95 -0
- package/src/cmds/validator/signers/importExternalKeystores.ts +69 -0
- package/src/cmds/validator/signers/index.ts +176 -0
- package/src/cmds/validator/signers/logSigners.ts +81 -0
- package/src/cmds/validator/slashingProtection/export.ts +110 -0
- package/src/cmds/validator/slashingProtection/import.ts +70 -0
- package/src/cmds/validator/slashingProtection/index.ts +12 -0
- package/src/cmds/validator/slashingProtection/options.ts +15 -0
- package/src/cmds/validator/slashingProtection/utils.ts +56 -0
- package/src/cmds/validator/voluntaryExit.ts +232 -0
- package/src/config/beaconNodeOptions.ts +68 -0
- package/src/config/beaconParams.ts +87 -0
- package/src/config/index.ts +3 -0
- package/src/config/peerId.ts +50 -0
- package/src/config/types.ts +3 -0
- package/src/index.ts +28 -0
- package/src/migrations/index.ts +0 -0
- package/src/networks/chiado.ts +20 -0
- package/src/networks/dev.ts +27 -0
- package/src/networks/ephemery.ts +9 -0
- package/src/networks/gnosis.ts +18 -0
- package/src/networks/holesky.ts +17 -0
- package/src/networks/hoodi.ts +16 -0
- package/src/networks/index.ts +236 -0
- package/src/networks/mainnet.ts +34 -0
- package/src/networks/sepolia.ts +17 -0
- package/src/options/beaconNodeOptions/api.ts +110 -0
- package/src/options/beaconNodeOptions/builder.ts +63 -0
- package/src/options/beaconNodeOptions/chain.ts +326 -0
- package/src/options/beaconNodeOptions/eth1.ts +95 -0
- package/src/options/beaconNodeOptions/execution.ts +92 -0
- package/src/options/beaconNodeOptions/index.ts +50 -0
- package/src/options/beaconNodeOptions/metrics.ts +39 -0
- package/src/options/beaconNodeOptions/monitoring.ts +61 -0
- package/src/options/beaconNodeOptions/network.ts +401 -0
- package/src/options/beaconNodeOptions/sync.ts +65 -0
- package/src/options/globalOptions.ts +72 -0
- package/src/options/index.ts +3 -0
- package/src/options/logOptions.ts +70 -0
- package/src/options/paramsOptions.ts +72 -0
- package/src/paths/global.ts +24 -0
- package/src/paths/rootDir.ts +11 -0
- package/src/util/errors.ts +20 -0
- package/src/util/ethers.ts +44 -0
- package/src/util/feeRecipient.ts +6 -0
- package/src/util/file.ts +167 -0
- package/src/util/format.ts +76 -0
- package/src/util/fs.ts +59 -0
- package/src/util/gitData/gitDataPath.ts +48 -0
- package/src/util/gitData/index.ts +70 -0
- package/src/util/gitData/writeGitData.ts +10 -0
- package/src/util/index.ts +17 -0
- package/src/util/jwt.ts +10 -0
- package/src/util/lockfile.ts +45 -0
- package/src/util/logger.ts +105 -0
- package/src/util/object.ts +15 -0
- package/src/util/passphrase.ts +25 -0
- package/src/util/process.ts +25 -0
- package/src/util/progress.ts +58 -0
- package/src/util/proposerConfig.ts +136 -0
- package/src/util/pruneOldFilesInDir.ts +27 -0
- package/src/util/sleep.ts +3 -0
- package/src/util/stripOffNewlines.ts +6 -0
- package/src/util/types.ts +8 -0
- package/src/util/version.ts +74 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {GlobalArgs} from "../options/index.js";
|
|
2
|
+
import {getDefaultDataDir} from "./rootDir.js";
|
|
3
|
+
|
|
4
|
+
export type GlobalPaths = {
|
|
5
|
+
dataDir: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Defines the path structure of the globally used files
|
|
10
|
+
*
|
|
11
|
+
* ```bash
|
|
12
|
+
* $dataDir
|
|
13
|
+
* └── $paramsFile
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function getGlobalPaths(args: Partial<GlobalArgs>, network: string): GlobalPaths {
|
|
17
|
+
// Set dataDir to network name iff dataDir is not set explicitly
|
|
18
|
+
const dataDir = args.dataDir || getDefaultDataDir(network);
|
|
19
|
+
return {
|
|
20
|
+
dataDir,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const defaultGlobalPaths = getGlobalPaths({dataDir: "$dataDir"}, "$network");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Follows XDG Base Directory Specification
|
|
6
|
+
* https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#basics
|
|
7
|
+
*/
|
|
8
|
+
export function getDefaultDataDir(network: string): string {
|
|
9
|
+
const dataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
|
|
10
|
+
return path.join(dataHome, "lodestar", network);
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expected error that shouldn't print a stack trace
|
|
3
|
+
*/
|
|
4
|
+
export class YargsError extends Error {}
|
|
5
|
+
|
|
6
|
+
export type Result<T> = {err: null; result: T} | {err: Error};
|
|
7
|
+
export async function wrapError<T>(promise: Promise<T>): Promise<Result<T>> {
|
|
8
|
+
try {
|
|
9
|
+
return {err: null, result: await promise};
|
|
10
|
+
} catch (err) {
|
|
11
|
+
return {err: err as Error};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function wrapFnError<T>(fn: () => T): Result<T> {
|
|
15
|
+
try {
|
|
16
|
+
return {err: null, result: fn()};
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return {err: err as Error};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import {ethers} from "ethers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a connected ethers signer from a variety of provider options
|
|
6
|
+
*/
|
|
7
|
+
export async function getEthersSigner({
|
|
8
|
+
keystorePath,
|
|
9
|
+
keystorePassword,
|
|
10
|
+
rpcUrl,
|
|
11
|
+
rpcPassword,
|
|
12
|
+
ipcPath,
|
|
13
|
+
chainId,
|
|
14
|
+
}: {
|
|
15
|
+
keystorePath?: string;
|
|
16
|
+
keystorePassword?: string;
|
|
17
|
+
rpcUrl?: string;
|
|
18
|
+
rpcPassword?: string;
|
|
19
|
+
ipcPath?: string;
|
|
20
|
+
chainId: number;
|
|
21
|
+
}): Promise<ethers.Signer> {
|
|
22
|
+
if (keystorePath && keystorePassword) {
|
|
23
|
+
const keystoreJson = fs.readFileSync(keystorePath, "utf8");
|
|
24
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, keystorePassword);
|
|
25
|
+
const eth1Provider = rpcUrl
|
|
26
|
+
? new ethers.JsonRpcProvider(rpcUrl)
|
|
27
|
+
: new ethers.InfuraProvider({name: "deposit", chainId});
|
|
28
|
+
return wallet.connect(eth1Provider);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (rpcUrl) {
|
|
32
|
+
const signer = await new ethers.JsonRpcProvider(rpcUrl).getSigner();
|
|
33
|
+
if (rpcPassword) {
|
|
34
|
+
await signer.unlock(rpcPassword);
|
|
35
|
+
}
|
|
36
|
+
return signer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (ipcPath) {
|
|
40
|
+
return new ethers.IpcSocketProvider(ipcPath).getSigner();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw Error("Must supply either keystorePath, rpcUrl, or ipcPath");
|
|
44
|
+
}
|
package/src/util/file.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {Readable} from "node:stream";
|
|
4
|
+
import stream from "node:stream/promises";
|
|
5
|
+
import {ReadableStream as NodeReadableStream} from "node:stream/web";
|
|
6
|
+
import yaml from "js-yaml";
|
|
7
|
+
import {fetch} from "@lodestar/utils";
|
|
8
|
+
|
|
9
|
+
const {load, dump, FAILSAFE_SCHEMA, Type} = yaml;
|
|
10
|
+
|
|
11
|
+
import {mkdir} from "./fs.js";
|
|
12
|
+
|
|
13
|
+
export const yamlSchema = FAILSAFE_SCHEMA.extend({
|
|
14
|
+
implicit: [
|
|
15
|
+
new Type("tag:yaml.org,2002:str", {
|
|
16
|
+
kind: "scalar",
|
|
17
|
+
construct: function construct(data) {
|
|
18
|
+
return data !== null ? data : "";
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export enum FileFormat {
|
|
25
|
+
json = "json",
|
|
26
|
+
yaml = "yaml",
|
|
27
|
+
yml = "yml",
|
|
28
|
+
toml = "toml",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse file contents as Json.
|
|
33
|
+
*/
|
|
34
|
+
export function parse<T>(contents: string, fileFormat: FileFormat): T {
|
|
35
|
+
switch (fileFormat) {
|
|
36
|
+
case FileFormat.json:
|
|
37
|
+
return JSON.parse(contents) as T;
|
|
38
|
+
case FileFormat.yaml:
|
|
39
|
+
case FileFormat.yml:
|
|
40
|
+
return load(contents, {schema: yamlSchema}) as T;
|
|
41
|
+
default:
|
|
42
|
+
return contents as unknown as T;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Stringify file contents.
|
|
48
|
+
*/
|
|
49
|
+
export function stringify(obj: unknown, fileFormat: FileFormat): string {
|
|
50
|
+
let contents: string;
|
|
51
|
+
switch (fileFormat) {
|
|
52
|
+
case FileFormat.json:
|
|
53
|
+
contents = JSON.stringify(obj, null, 2);
|
|
54
|
+
break;
|
|
55
|
+
case FileFormat.yaml:
|
|
56
|
+
case FileFormat.yml:
|
|
57
|
+
contents = dump(obj, {schema: yamlSchema});
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
contents = obj as string;
|
|
61
|
+
}
|
|
62
|
+
return contents;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Write a JSON serializable object to a file
|
|
67
|
+
*
|
|
68
|
+
* Serialize either to json, yaml, or toml
|
|
69
|
+
*/
|
|
70
|
+
export function writeFile(filepath: string, obj: unknown, options: fs.WriteFileOptions = "utf-8"): void {
|
|
71
|
+
mkdir(path.dirname(filepath));
|
|
72
|
+
const fileFormat = path.extname(filepath).substr(1);
|
|
73
|
+
fs.writeFileSync(filepath, typeof obj === "string" ? obj : stringify(obj, fileFormat as FileFormat), options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a file with `600 (-rw-------)` permissions
|
|
78
|
+
* *Note*: 600: Owner has full read and write access to the file,
|
|
79
|
+
* while no other user can access the file
|
|
80
|
+
*/
|
|
81
|
+
export function writeFile600Perm(filepath: string, obj: unknown, options?: fs.WriteFileOptions): void {
|
|
82
|
+
writeFile(filepath, obj, options);
|
|
83
|
+
fs.chmodSync(filepath, "0600");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Read a JSON serializable object from a file
|
|
88
|
+
*
|
|
89
|
+
* Parse either from json, yaml, or toml
|
|
90
|
+
* Optional acceptedFormats object can be passed which can be an array of accepted formats, in future can be extended to include parseFn for the accepted formats
|
|
91
|
+
*/
|
|
92
|
+
export function readFile<T>(filepath: string, acceptedFormats?: string[]): T {
|
|
93
|
+
const fileFormat = path.extname(filepath).substr(1);
|
|
94
|
+
if (acceptedFormats && !acceptedFormats.includes(fileFormat)) throw new Error(`UnsupportedFileFormat: ${filepath}`);
|
|
95
|
+
const contents = fs.readFileSync(filepath, "utf-8");
|
|
96
|
+
return parse(contents, fileFormat as FileFormat);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @see readFile
|
|
101
|
+
* If `filepath` does not exist returns null
|
|
102
|
+
*/
|
|
103
|
+
export function readFileIfExists<T>(filepath: string, acceptedFormats?: string[]): T | null {
|
|
104
|
+
try {
|
|
105
|
+
return readFile(filepath, acceptedFormats);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
if ((e as {code: string}).code === "ENOENT") {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Download from URL or copy from local filesystem
|
|
116
|
+
* @param urlOrPathSrc "/path/to/file.szz" | "https://url.to/file.szz"
|
|
117
|
+
*/
|
|
118
|
+
export async function downloadOrCopyFile(pathDest: string, urlOrPathSrc: string): Promise<void> {
|
|
119
|
+
if (isUrl(urlOrPathSrc)) {
|
|
120
|
+
await downloadFile(pathDest, urlOrPathSrc);
|
|
121
|
+
} else {
|
|
122
|
+
mkdir(path.dirname(pathDest));
|
|
123
|
+
await fs.promises.copyFile(urlOrPathSrc, pathDest);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Downloads a genesis file per network if it does not exist
|
|
129
|
+
*/
|
|
130
|
+
export async function downloadFile(pathDest: string, url: string): Promise<void> {
|
|
131
|
+
if (!fs.existsSync(pathDest)) {
|
|
132
|
+
mkdir(path.dirname(pathDest));
|
|
133
|
+
const res = await fetch(url);
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
throw new Error(`Failed to download file from ${url}: ${res.status} ${res.statusText}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!res.body) {
|
|
139
|
+
throw new Error("Response body is null");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await stream.pipeline(Readable.fromWeb(res.body as NodeReadableStream), fs.createWriteStream(pathDest));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Download from URL to memory or load from local filesystem
|
|
148
|
+
* @param urlOrPathSrc "/path/to/file.szz" | "https://url.to/file.szz"
|
|
149
|
+
*/
|
|
150
|
+
export async function downloadOrLoadFile(pathOrUrl: string): Promise<Uint8Array> {
|
|
151
|
+
if (isUrl(pathOrUrl)) {
|
|
152
|
+
const res = await fetch(pathOrUrl);
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
throw new Error(`Failed to download file from ${pathOrUrl}: ${res.status} ${res.statusText}`);
|
|
155
|
+
}
|
|
156
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
157
|
+
return new Uint8Array(arrayBuffer);
|
|
158
|
+
}
|
|
159
|
+
return fs.promises.readFile(pathOrUrl);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Returns boolean for whether the string is a URL.
|
|
164
|
+
*/
|
|
165
|
+
function isUrl(pathOrUrl: string): boolean {
|
|
166
|
+
return pathOrUrl.startsWith("http");
|
|
167
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {PublicKey} from "@chainsafe/blst";
|
|
2
|
+
import {fromHex} from "@lodestar/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 0x prefix a string if not prefixed already
|
|
6
|
+
*/
|
|
7
|
+
export function ensure0xPrefix(hex: string): string {
|
|
8
|
+
if (!hex.startsWith("0x")) hex = `0x${hex}`;
|
|
9
|
+
return hex;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isValidatePubkeyHex(pubkeyHex: string): boolean {
|
|
13
|
+
return /^0x[0-9a-fA-F]{96}$/.test(pubkeyHex);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getPubkeyHexFromKeystore(keystore: {pubkey?: string}): string {
|
|
17
|
+
if (!keystore.pubkey) {
|
|
18
|
+
throw Error("Invalid keystore, must contain .pubkey property");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pubkeyHex = ensure0xPrefix(keystore.pubkey);
|
|
22
|
+
if (!isValidatePubkeyHex(pubkeyHex)) {
|
|
23
|
+
throw Error(`Invalid keystore pubkey format ${pubkeyHex}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return pubkeyHex;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse string inclusive range: `0..32`, into an array of all values in that range
|
|
31
|
+
*/
|
|
32
|
+
export function parseRange(range: string): number[] {
|
|
33
|
+
if (!range.includes("..")) {
|
|
34
|
+
throw Error(`Invalid range '${range}', must include '..'`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const [from, to] = range.split("..").map((n) => parseInt(n));
|
|
38
|
+
|
|
39
|
+
if (Number.isNaN(from)) throw Error(`Invalid range from isNaN '${range}'`);
|
|
40
|
+
if (Number.isNaN(to)) throw Error(`Invalid range to isNaN '${range}'`);
|
|
41
|
+
if (from > to) throw Error(`Invalid range from > to '${range}'`);
|
|
42
|
+
|
|
43
|
+
const arr: number[] = [];
|
|
44
|
+
for (let i = from; i <= to; i++) {
|
|
45
|
+
arr.push(i);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return arr;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function assertValidPubkeysHex(pubkeysHex: string[]): void {
|
|
52
|
+
for (const pubkeyHex of pubkeysHex) {
|
|
53
|
+
const pubkeyBytes = fromHex(pubkeyHex);
|
|
54
|
+
PublicKey.fromBytes(pubkeyBytes, true);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parses a file to get a list of bootnodes for a network.
|
|
60
|
+
* Bootnodes file is expected to contain bootnode ENR's concatenated by newlines, or commas for
|
|
61
|
+
* parsing plaintext, YAML, JSON and/or env files.
|
|
62
|
+
*/
|
|
63
|
+
export function parseBootnodesFile(bootnodesFile: string): string[] {
|
|
64
|
+
const enrs = [];
|
|
65
|
+
for (const line of bootnodesFile.trim().split(/\r?\n/)) {
|
|
66
|
+
for (const entry of line.split(",")) {
|
|
67
|
+
const sanitizedEntry = entry.replace(/['",[\]{}.]+/g, "").trim();
|
|
68
|
+
|
|
69
|
+
if (sanitizedEntry.includes("enr:-")) {
|
|
70
|
+
const parsedEnr = `enr:-${sanitizedEntry.split("enr:-")[1].split("#")[0].trim()}`;
|
|
71
|
+
enrs.push(parsedEnr);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return enrs;
|
|
76
|
+
}
|
package/src/util/fs.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maybe create a directory
|
|
6
|
+
*/
|
|
7
|
+
export function mkdir(dirname: string): void {
|
|
8
|
+
if (!fs.existsSync(dirname)) fs.mkdirSync(dirname, {recursive: true});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** NodeJS POSIX errors subset */
|
|
12
|
+
type ErrorFs = Error & {code: "ENOENT" | "ENOTDIR"};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Attempts to unlink a file, return true if it is deleted, false if not found
|
|
16
|
+
*/
|
|
17
|
+
export function unlinkSyncMaybe(filepath: string): boolean {
|
|
18
|
+
try {
|
|
19
|
+
fs.unlinkSync(filepath);
|
|
20
|
+
return true;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
const {code} = e as ErrorFs;
|
|
23
|
+
if (code === "ENOENT") return false;
|
|
24
|
+
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Attempts rm a dir, return true if it is deleted, false if not found
|
|
31
|
+
*/
|
|
32
|
+
export function rmdirSyncMaybe(dirpath: string): boolean {
|
|
33
|
+
try {
|
|
34
|
+
fs.rmSync(dirpath, {recursive: true});
|
|
35
|
+
return true;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
const {code} = e as ErrorFs;
|
|
38
|
+
// about error codes https://nodejs.org/api/fs.html#fspromisesrmdirpath-options
|
|
39
|
+
// ENOENT error on Windows and an ENOTDIR
|
|
40
|
+
if (code === "ENOENT" || code === "ENOTDIR") return false;
|
|
41
|
+
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find all files recursively in `dirPath`
|
|
48
|
+
*/
|
|
49
|
+
export function recursiveLookup(dirPath: string, filepaths: string[] = []): string[] {
|
|
50
|
+
if (fs.statSync(dirPath).isDirectory()) {
|
|
51
|
+
for (const filename of fs.readdirSync(dirPath)) {
|
|
52
|
+
recursiveLookup(path.join(dirPath, filename), filepaths);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
filepaths.push(dirPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return filepaths;
|
|
59
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {fileURLToPath} from "node:url";
|
|
4
|
+
|
|
5
|
+
// Global variable __dirname no longer available in ES6 modules.
|
|
6
|
+
// Solutions: https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-js-when-using-es6-modules
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
// Persist git data and distribute through NPM so CLI consumers can know exactly
|
|
10
|
+
// at what commit was this src build. This is used in the metrics and to log initially.
|
|
11
|
+
//
|
|
12
|
+
// - For NPM release (stable): Only the version is persisted. Once must then track the version's tag
|
|
13
|
+
// in Github to resolve that version to a specific commit. While this is okay, git-data.json gives
|
|
14
|
+
// a gurantee of the exact commit at build time.
|
|
15
|
+
//
|
|
16
|
+
// - For NPM release (dev): canary commits include the commit, so this feature is not really
|
|
17
|
+
// necessary. However, it's more cumbersome to have conditional logic on stable / dev.
|
|
18
|
+
//
|
|
19
|
+
// - For build from source: .git folder is available in the context of the built code, so it can extract
|
|
20
|
+
// branch and commit directly without the need for .git-data.json.
|
|
21
|
+
//
|
|
22
|
+
// - For build from source dockerized: This feature is required to know the branch and commit, since
|
|
23
|
+
// git data is not persisted past the build. However, .dockerignore prevents .git folder from being
|
|
24
|
+
// copied into the container's context, so .git-data.json can't be generated.
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* WARNING!! If you change this path make sure to update:
|
|
28
|
+
* - 'packages/cli/package.json' -> .files -> `".git-data.json"`
|
|
29
|
+
*/
|
|
30
|
+
export const gitDataPath = path.resolve(__dirname, "../../../.git-data.json");
|
|
31
|
+
|
|
32
|
+
/** Git data type used to construct version information string and persistence. */
|
|
33
|
+
export type GitData = {
|
|
34
|
+
/** "developer-feature" */
|
|
35
|
+
branch: string;
|
|
36
|
+
/** "80c248bb392f512cc115d95059e22239a17bbd7d" */
|
|
37
|
+
commit: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Writes a persistent git data file. */
|
|
41
|
+
export function writeGitDataFile(gitData: GitData): void {
|
|
42
|
+
fs.writeFileSync(gitDataPath, JSON.stringify(gitData, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Reads the persistent git data file. */
|
|
46
|
+
export function readGitDataFile(): GitData {
|
|
47
|
+
return JSON.parse(fs.readFileSync(gitDataPath, "utf8")) as GitData;
|
|
48
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {execSync} from "node:child_process";
|
|
2
|
+
// This file is created in the build step and is distributed through NPM
|
|
3
|
+
// MUST be in sync with `-/gitDataPath.ts` and `package.json` files.
|
|
4
|
+
import {GitData, readGitDataFile} from "./gitDataPath.js";
|
|
5
|
+
|
|
6
|
+
/** Reads git data from a persisted file or local git data at build time. */
|
|
7
|
+
export function readAndGetGitData(): GitData {
|
|
8
|
+
try {
|
|
9
|
+
// Gets git data containing current branch and commit info from persistent file.
|
|
10
|
+
let persistedGitData: Partial<GitData>;
|
|
11
|
+
try {
|
|
12
|
+
persistedGitData = readGitDataFile();
|
|
13
|
+
} catch (_e) {
|
|
14
|
+
persistedGitData = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const currentGitData = getGitData();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
// If the CLI is run from source, prioritze current git data
|
|
21
|
+
// over `.git-data.json` file, which might be stale here.
|
|
22
|
+
branch:
|
|
23
|
+
currentGitData.branch && currentGitData.branch.length > 0
|
|
24
|
+
? currentGitData.branch
|
|
25
|
+
: (persistedGitData.branch ?? ""),
|
|
26
|
+
commit:
|
|
27
|
+
currentGitData.commit && currentGitData.commit.length > 0
|
|
28
|
+
? currentGitData.commit
|
|
29
|
+
: (persistedGitData.commit ?? ""),
|
|
30
|
+
};
|
|
31
|
+
} catch (_e) {
|
|
32
|
+
return {
|
|
33
|
+
branch: "",
|
|
34
|
+
commit: "",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Gets git data containing current branch and commit info from CLI. */
|
|
40
|
+
export function getGitData(): GitData {
|
|
41
|
+
return {
|
|
42
|
+
branch: process.env.GIT_BRANCH ?? getBranch(),
|
|
43
|
+
commit: process.env.GIT_COMMIT ?? getCommit(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Tries to get branch from git CLI. */
|
|
48
|
+
function getBranch(): string {
|
|
49
|
+
try {
|
|
50
|
+
return shellSilent("git rev-parse --abbrev-ref HEAD");
|
|
51
|
+
} catch (_e) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Tries to get commit from git from git CLI. */
|
|
57
|
+
function getCommit(): string {
|
|
58
|
+
try {
|
|
59
|
+
return shellSilent("git rev-parse --verify HEAD");
|
|
60
|
+
} catch (_e) {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Silent shell that won't pollute stdout, or stderr */
|
|
66
|
+
function shellSilent(cmd: string): string {
|
|
67
|
+
return execSync(cmd, {stdio: ["ignore", "pipe", "ignore"]})
|
|
68
|
+
.toString()
|
|
69
|
+
.trim();
|
|
70
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// For RATIONALE of this file, check packages/cli/src/util/gitData/gitDataPath.ts
|
|
4
|
+
// Persist exact commit in NPM distributions for easier tracking of the build
|
|
5
|
+
|
|
6
|
+
import {writeGitDataFile} from "./gitDataPath.js";
|
|
7
|
+
import {getGitData} from "./index.js";
|
|
8
|
+
|
|
9
|
+
// Script to write the git data file (json) used by the build procedures to persist git data.
|
|
10
|
+
writeGitDataFile(getGitData());
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./errors.js";
|
|
2
|
+
export * from "./ethers.js";
|
|
3
|
+
export * from "./feeRecipient.js";
|
|
4
|
+
export * from "./file.js";
|
|
5
|
+
export * from "./format.js";
|
|
6
|
+
export * from "./fs.js";
|
|
7
|
+
export * from "./gitData/index.js";
|
|
8
|
+
export * from "./jwt.js";
|
|
9
|
+
export * from "./logger.js";
|
|
10
|
+
export * from "./object.js";
|
|
11
|
+
export * from "./passphrase.js";
|
|
12
|
+
export * from "./process.js";
|
|
13
|
+
export * from "./proposerConfig.js";
|
|
14
|
+
export * from "./pruneOldFilesInDir.js";
|
|
15
|
+
export * from "./sleep.js";
|
|
16
|
+
export * from "./stripOffNewlines.js";
|
|
17
|
+
export * from "./types.js";
|
package/src/util/jwt.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function extractJwtHexSecret(jwtSecretContents: string): string {
|
|
2
|
+
const hexPattern = new RegExp(/^(0x|0X)?(?<jwtSecret>[a-fA-F0-9]+)$/, "g");
|
|
3
|
+
const jwtSecretHexMatch = hexPattern.exec(jwtSecretContents);
|
|
4
|
+
const jwtSecret = jwtSecretHexMatch?.groups?.jwtSecret;
|
|
5
|
+
if (!jwtSecret || jwtSecret.length !== 64) {
|
|
6
|
+
throw Error(`Need a valid 256 bit hex encoded secret ${jwtSecret} ${jwtSecretContents}`);
|
|
7
|
+
}
|
|
8
|
+
// Return the secret in proper hex format
|
|
9
|
+
return `0x${jwtSecret}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {lockSync, unlockSync} from "proper-lockfile";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a .lock file for `filepath`, argument passed must not be the lock path
|
|
5
|
+
* @param filepath File to lock, i.e. `keystore_0001.json`
|
|
6
|
+
*/
|
|
7
|
+
export function lockFilepath(filepath: string): void {
|
|
8
|
+
try {
|
|
9
|
+
lockSync(filepath, {
|
|
10
|
+
// Allows to lock files that do not exist
|
|
11
|
+
realpath: false,
|
|
12
|
+
});
|
|
13
|
+
} catch (e) {
|
|
14
|
+
if (isLockfileError(e) && (e.code === "ELOCKED" || e.code === "ENOTDIR")) {
|
|
15
|
+
e.message = `'${filepath}' already in use by another process`;
|
|
16
|
+
}
|
|
17
|
+
throw e;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Deletes a .lock file for `filepath`, argument passed must not be the lock path
|
|
23
|
+
* @param filepath File to unlock, i.e. `keystore_0001.json`
|
|
24
|
+
*/
|
|
25
|
+
export function unlockFilepath(filepath: string): void {
|
|
26
|
+
try {
|
|
27
|
+
unlockSync(filepath, {
|
|
28
|
+
// Allows to unlock files that do not exist
|
|
29
|
+
realpath: false,
|
|
30
|
+
});
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (isLockfileError(e) && e.code === "ENOTACQUIRED") {
|
|
33
|
+
// Do not throw if the lock file is already deleted
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
throw e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// https://github.com/moxystudio/node-proper-lockfile/blob/9f8c303c91998e8404a911dc11c54029812bca69/lib/lockfile.js#L53
|
|
41
|
+
export type LockfileError = Error & {code: "ELOCKED" | "ENOTACQUIRED" | "ENOTDIR"};
|
|
42
|
+
|
|
43
|
+
function isLockfileError(e: unknown): e is LockfileError {
|
|
44
|
+
return e instanceof Error && (e as LockfileError).code !== undefined;
|
|
45
|
+
}
|