@leofcoin/launch-chain 0.4.21 → 0.4.23
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/README.md +42 -2
- package/exports/cli-options.d.ts +14 -0
- package/exports/cli-options.js +66 -0
- package/exports/cli.d.ts +3 -0
- package/exports/cli.js +187 -0
- package/exports/node.d.ts +22 -0
- package/exports/node.js +53 -0
- package/exports/validator-cli.js +13 -61
- package/exports/validator.d.ts +6 -0
- package/exports/validator.js +25 -27
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -17,12 +17,30 @@ const { chain, endpoints, mode } = await launch(
|
|
|
17
17
|
// when mode is direct means chain is directly available and no endpoint is needed to interact with it
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
## command line
|
|
21
|
+
|
|
22
|
+
No monorepo checkout or global install is required:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npx --package @leofcoin/launch-chain leofcoin --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run a normal syncing full node with HTTP and WebSocket endpoints:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npx --package @leofcoin/launch-chain leofcoin node
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The same CLI exposes `node`, `validator`, `account`, `balance`, `status`, and
|
|
35
|
+
`transfer` commands. `node` never registers as a validator. Only the explicit
|
|
36
|
+
`validator` command checks validator eligibility and starts participation.
|
|
37
|
+
|
|
20
38
|
## run a validator
|
|
21
39
|
|
|
22
40
|
No monorepo checkout is required. Run the validator directly from the published package:
|
|
23
41
|
|
|
24
42
|
```sh
|
|
25
|
-
npx --package @leofcoin/launch-chain leofcoin
|
|
43
|
+
npx --package @leofcoin/launch-chain leofcoin validator
|
|
26
44
|
```
|
|
27
45
|
|
|
28
46
|
The first run uses the normal interactive identity-password prompt. A new account must receive
|
|
@@ -43,7 +61,29 @@ process can replace a separate launch-chain server. Change them with `--http-por
|
|
|
43
61
|
or use `--no-endpoints` for a validator that should not expose an API.
|
|
44
62
|
|
|
45
63
|
Never put the password or an identity backup in a command-line argument, service file, or Git.
|
|
46
|
-
|
|
64
|
+
The legacy `leofcoin-validator` command remains an alias for `leofcoin validator`.
|
|
65
|
+
Use `leofcoin --help` for all commands and options.
|
|
66
|
+
|
|
67
|
+
### send LFC from the local validator
|
|
68
|
+
|
|
69
|
+
When the validator runs in a terminal it exposes a local wallet shell. Private keys never leave
|
|
70
|
+
the process:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
leofcoin> account
|
|
74
|
+
leofcoin> balance
|
|
75
|
+
leofcoin> transfer YTq... 1000
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
When the node is stopped, a one-shot transfer can start the same local identity,
|
|
79
|
+
finalize the transfer, and exit. It does not register or participate as a validator:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npx --package @leofcoin/launch-chain leofcoin transfer YTq... 1000 \
|
|
83
|
+
--password-file /secure/leofcoin/password
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Do not run the daemon and one-shot command simultaneously against the same data root.
|
|
47
87
|
|
|
48
88
|
## options
|
|
49
89
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CliCommand = 'node' | 'validator' | 'account' | 'balance' | 'status' | 'transfer' | 'help' | 'version';
|
|
2
|
+
export type CliOptions = {
|
|
3
|
+
command: CliCommand;
|
|
4
|
+
positionals: string[];
|
|
5
|
+
network?: string;
|
|
6
|
+
root?: string;
|
|
7
|
+
passwordFile?: string;
|
|
8
|
+
intervalMinutes?: number;
|
|
9
|
+
httpPort: number | false;
|
|
10
|
+
wsPort: number | false;
|
|
11
|
+
shell: boolean;
|
|
12
|
+
stars?: string[];
|
|
13
|
+
};
|
|
14
|
+
export declare const parseCliOptions: (args: string[]) => CliOptions;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const commands = new Set(['node', 'validator', 'account', 'balance', 'status', 'transfer', 'help', 'version']);
|
|
2
|
+
const port = (name, value) => {
|
|
3
|
+
const parsed = Number(value);
|
|
4
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65_535)
|
|
5
|
+
throw new Error(`${name} must be a valid port`);
|
|
6
|
+
return parsed;
|
|
7
|
+
};
|
|
8
|
+
const parseCliOptions = (args) => {
|
|
9
|
+
let command;
|
|
10
|
+
const positionals = [];
|
|
11
|
+
const stars = [];
|
|
12
|
+
const options = { command: 'help', positionals, httpPort: 8080, wsPort: 4040, shell: true };
|
|
13
|
+
const value = (index, name) => {
|
|
14
|
+
const result = args[index + 1];
|
|
15
|
+
if (!result || result.startsWith('-'))
|
|
16
|
+
throw new Error(`${name} requires a value`);
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
+
const argument = args[index];
|
|
21
|
+
if (argument === '-h' || argument === '--help')
|
|
22
|
+
command = 'help';
|
|
23
|
+
else if (argument === '-v' || argument === '--version')
|
|
24
|
+
command = 'version';
|
|
25
|
+
else if (argument === '--network')
|
|
26
|
+
options.network = value(index++, argument);
|
|
27
|
+
else if (argument === '--root')
|
|
28
|
+
options.root = value(index++, argument);
|
|
29
|
+
else if (argument === '--password-file')
|
|
30
|
+
options.passwordFile = value(index++, argument);
|
|
31
|
+
else if (argument === '--interval') {
|
|
32
|
+
options.intervalMinutes = Number(value(index++, argument));
|
|
33
|
+
if (!Number.isFinite(options.intervalMinutes) || options.intervalMinutes < 1)
|
|
34
|
+
throw new Error('--interval must be at least one minute');
|
|
35
|
+
}
|
|
36
|
+
else if (argument === '--http-port')
|
|
37
|
+
options.httpPort = port(argument, value(index++, argument));
|
|
38
|
+
else if (argument === '--ws-port')
|
|
39
|
+
options.wsPort = port(argument, value(index++, argument));
|
|
40
|
+
else if (argument === '--no-endpoints') {
|
|
41
|
+
options.httpPort = false;
|
|
42
|
+
options.wsPort = false;
|
|
43
|
+
}
|
|
44
|
+
else if (argument === '--no-shell')
|
|
45
|
+
options.shell = false;
|
|
46
|
+
else if (argument === '--star')
|
|
47
|
+
stars.push(value(index++, argument));
|
|
48
|
+
else if (argument === '--no-stars')
|
|
49
|
+
options.stars = [];
|
|
50
|
+
else if (argument.startsWith('-'))
|
|
51
|
+
throw new Error(`unknown option: ${argument}`);
|
|
52
|
+
else if (!command) {
|
|
53
|
+
if (!commands.has(argument))
|
|
54
|
+
throw new Error(`unknown command: ${argument}`);
|
|
55
|
+
command = argument;
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
positionals.push(argument);
|
|
59
|
+
}
|
|
60
|
+
options.command = command ?? 'help';
|
|
61
|
+
if (stars.length > 0)
|
|
62
|
+
options.stars = stars;
|
|
63
|
+
return options;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export { parseCliOptions };
|
package/exports/cli.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export declare const help = "Leofcoin command line\n\nUsage:\n leofcoin node [options] Run a normal full node\n leofcoin validator [options] Run a validator node\n leofcoin account [options] Show the local account\n leofcoin balance [address] [options] Show an LFC balance\n leofcoin status [options] Show local chain status\n leofcoin transfer <address> <LFC> [options] Send LFC and await finalization\n\nOptions:\n --network <name> Network (default: leofcoin:peach)\n --root <path> Persistent data directory\n --password-file <path> Read the identity password from a private file\n --star <url> Discovery star; repeat for multiple stars\n --no-stars Disable discovery stars\n --http-port <port> HTTP API port (default: 8080)\n --ws-port <port> WebSocket API port (default: 4040)\n --no-endpoints Disable HTTP and WebSocket endpoints\n --no-shell Disable the interactive wallet shell\n --interval <minutes> Validator heartbeat interval (default: 5)\n -h, --help Show help\n -v, --version Show version\n\nPasswords are never accepted as command-line values. Use --password-file,\nLEOFCOIN_PASSWORD_FILE, LEOFCOIN_PASSWORD, or the interactive password prompt.\n";
|
|
3
|
+
export declare const runCli: (args?: string[]) => Promise<undefined>;
|
package/exports/cli.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { resolve, isAbsolute, relative } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
import { parseCliOptions } from './cli-options.js';
|
|
8
|
+
import { startNode } from './node.js';
|
|
9
|
+
import { startValidator } from './validator.js';
|
|
10
|
+
import '@leofcoin/lib';
|
|
11
|
+
import '@leofcoin/networks';
|
|
12
|
+
import './index.js';
|
|
13
|
+
import '@leofcoin/chain/node';
|
|
14
|
+
import '@leofcoin/chain/chain';
|
|
15
|
+
import '@leofcoin/lib/node-config';
|
|
16
|
+
import '@leofcoin/endpoint-clients/ws';
|
|
17
|
+
import '@leofcoin/endpoint-clients/http';
|
|
18
|
+
import '@leofcoin/addresses';
|
|
19
|
+
|
|
20
|
+
const help = `Leofcoin command line
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
leofcoin node [options] Run a normal full node
|
|
24
|
+
leofcoin validator [options] Run a validator node
|
|
25
|
+
leofcoin account [options] Show the local account
|
|
26
|
+
leofcoin balance [address] [options] Show an LFC balance
|
|
27
|
+
leofcoin status [options] Show local chain status
|
|
28
|
+
leofcoin transfer <address> <LFC> [options] Send LFC and await finalization
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--network <name> Network (default: leofcoin:peach)
|
|
32
|
+
--root <path> Persistent data directory
|
|
33
|
+
--password-file <path> Read the identity password from a private file
|
|
34
|
+
--star <url> Discovery star; repeat for multiple stars
|
|
35
|
+
--no-stars Disable discovery stars
|
|
36
|
+
--http-port <port> HTTP API port (default: 8080)
|
|
37
|
+
--ws-port <port> WebSocket API port (default: 4040)
|
|
38
|
+
--no-endpoints Disable HTTP and WebSocket endpoints
|
|
39
|
+
--no-shell Disable the interactive wallet shell
|
|
40
|
+
--interval <minutes> Validator heartbeat interval (default: 5)
|
|
41
|
+
-h, --help Show help
|
|
42
|
+
-v, --version Show version
|
|
43
|
+
|
|
44
|
+
Passwords are never accepted as command-line values. Use --password-file,
|
|
45
|
+
LEOFCOIN_PASSWORD_FILE, LEOFCOIN_PASSWORD, or the interactive password prompt.
|
|
46
|
+
`;
|
|
47
|
+
const packageVersion = async () => {
|
|
48
|
+
const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
49
|
+
return manifest.version;
|
|
50
|
+
};
|
|
51
|
+
const passwordFor = async (passwordFile) => {
|
|
52
|
+
const file = passwordFile ?? process.env.LEOFCOIN_PASSWORD_FILE;
|
|
53
|
+
if (!file)
|
|
54
|
+
return process.env.LEOFCOIN_PASSWORD;
|
|
55
|
+
const password = (await readFile(resolve(file), 'utf8')).trim();
|
|
56
|
+
if (!password)
|
|
57
|
+
throw new Error(`empty identity password file: ${resolve(file)}`);
|
|
58
|
+
return password;
|
|
59
|
+
};
|
|
60
|
+
// @leofcoin/storage resolves its root below the home directory. Convert an
|
|
61
|
+
// absolute CLI path to the equivalent home-relative path before it reaches the
|
|
62
|
+
// storage layer, so `--root /var/lib/leofcoin` remains truly absolute.
|
|
63
|
+
const dataRootFor = (root) => root && isAbsolute(root) ? relative(homedir(), root) : root;
|
|
64
|
+
const amountFor = (chain, value) => {
|
|
65
|
+
if (!value)
|
|
66
|
+
throw new Error('transfer amount is required');
|
|
67
|
+
try {
|
|
68
|
+
const amount = BigInt(chain.utils.parseUnits(value));
|
|
69
|
+
if (amount <= 0n)
|
|
70
|
+
throw new Error();
|
|
71
|
+
return amount;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error('transfer amount must be a positive LFC amount');
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const showStatus = async (handle) => {
|
|
78
|
+
const chain = handle.chain;
|
|
79
|
+
const lastBlock = await chain.lastBlock;
|
|
80
|
+
const balance = await chain.balanceOf(handle.account);
|
|
81
|
+
process.stdout.write(`network: ${chain.network ?? 'leofcoin'}\n`);
|
|
82
|
+
process.stdout.write(`account: ${handle.account}\n`);
|
|
83
|
+
process.stdout.write(`balance: ${chain.utils.formatUnits(balance)} LFC\n`);
|
|
84
|
+
process.stdout.write(`height: ${lastBlock?.index ?? 'unknown'}\n`);
|
|
85
|
+
for (const url of handle.endpoints.http)
|
|
86
|
+
process.stdout.write(`http: ${url}\n`);
|
|
87
|
+
for (const url of handle.endpoints.ws)
|
|
88
|
+
process.stdout.write(`ws: ${url}\n`);
|
|
89
|
+
};
|
|
90
|
+
const runShell = async (handle) => {
|
|
91
|
+
const shell = createInterface({ input: process.stdin, output: process.stdout });
|
|
92
|
+
const chain = handle.chain;
|
|
93
|
+
process.stdout.write('wallet shell ready; type help for commands\n');
|
|
94
|
+
while (true) {
|
|
95
|
+
const [command, ...values] = (await shell.question('leofcoin> ')).trim().split(/\s+/);
|
|
96
|
+
try {
|
|
97
|
+
if (!command)
|
|
98
|
+
continue;
|
|
99
|
+
if (command === 'account')
|
|
100
|
+
process.stdout.write(`${handle.account}\n`);
|
|
101
|
+
else if (command === 'balance') {
|
|
102
|
+
const balance = await chain.balanceOf(values[0] ?? handle.account);
|
|
103
|
+
process.stdout.write(`${chain.utils.formatUnits(balance)} LFC\n`);
|
|
104
|
+
}
|
|
105
|
+
else if (command === 'status')
|
|
106
|
+
await showStatus(handle);
|
|
107
|
+
else if (command === 'transfer') {
|
|
108
|
+
const hash = await handle.transfer(values[0], amountFor(chain, values[1]));
|
|
109
|
+
process.stdout.write(`transfer finalized: ${hash}\n`);
|
|
110
|
+
}
|
|
111
|
+
else if (command === 'help') {
|
|
112
|
+
process.stdout.write('account | balance [address] | status | transfer <address> <LFC> | exit\n');
|
|
113
|
+
}
|
|
114
|
+
else if (command === 'exit' || command === 'quit') {
|
|
115
|
+
shell.close();
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
else
|
|
119
|
+
process.stderr.write(`unknown command: ${command}\n`);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
process.stderr.write(`${error?.message ?? error}\n`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const runCli = async (args = process.argv.slice(2)) => {
|
|
127
|
+
const options = parseCliOptions(args);
|
|
128
|
+
if (options.command === 'help')
|
|
129
|
+
return void process.stdout.write(help);
|
|
130
|
+
if (options.command === 'version')
|
|
131
|
+
return void process.stdout.write(`${await packageVersion()}\n`);
|
|
132
|
+
const common = {
|
|
133
|
+
network: options.network ?? process.env.LEOFCOIN_NETWORK,
|
|
134
|
+
root: dataRootFor(options.root ?? process.env.LEOFCOIN_DATA_ROOT),
|
|
135
|
+
password: await passwordFor(options.passwordFile),
|
|
136
|
+
stars: options.stars,
|
|
137
|
+
httpPort: options.httpPort,
|
|
138
|
+
wsPort: options.wsPort
|
|
139
|
+
};
|
|
140
|
+
const handle = options.command === 'validator'
|
|
141
|
+
? await startValidator({ ...common, intervalMinutes: options.intervalMinutes })
|
|
142
|
+
: await startNode(common);
|
|
143
|
+
const nodeHandle = handle;
|
|
144
|
+
const chain = nodeHandle.chain;
|
|
145
|
+
const shutdown = (signal) => {
|
|
146
|
+
process.stdout.write(`received ${signal}; stopping node\n`);
|
|
147
|
+
nodeHandle.stop();
|
|
148
|
+
setTimeout(() => process.exit(0), 100);
|
|
149
|
+
};
|
|
150
|
+
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
151
|
+
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
152
|
+
if (options.command === 'account')
|
|
153
|
+
process.stdout.write(`${nodeHandle.account}\n`);
|
|
154
|
+
else if (options.command === 'balance') {
|
|
155
|
+
const balance = await chain.balanceOf(options.positionals[0] ?? nodeHandle.account);
|
|
156
|
+
process.stdout.write(`${chain.utils.formatUnits(balance)} LFC\n`);
|
|
157
|
+
}
|
|
158
|
+
else if (options.command === 'status')
|
|
159
|
+
await showStatus(nodeHandle);
|
|
160
|
+
else if (options.command === 'transfer') {
|
|
161
|
+
const to = options.positionals[0];
|
|
162
|
+
if (!to)
|
|
163
|
+
throw new Error('transfer recipient is required');
|
|
164
|
+
const hash = await nodeHandle.transfer(to, amountFor(chain, options.positionals[1]));
|
|
165
|
+
process.stdout.write(`transfer finalized: ${hash}\n`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
process.stdout.write(`${options.command} account: ${nodeHandle.account}\n`);
|
|
169
|
+
if (options.shell && process.stdin.isTTY)
|
|
170
|
+
await runShell(nodeHandle);
|
|
171
|
+
else
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
nodeHandle.stop();
|
|
175
|
+
// Peernet and endpoint servers currently expose no shared async close API.
|
|
176
|
+
// Give pending output a moment to flush, then make one-shot commands and an
|
|
177
|
+
// explicitly closed interactive shell terminate predictably.
|
|
178
|
+
setTimeout(() => process.exit(0), 100);
|
|
179
|
+
};
|
|
180
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
181
|
+
runCli().catch((error) => {
|
|
182
|
+
process.stderr.write(`leofcoin: ${error?.message ?? error}\n`);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { help, runCli };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Chain from '@leofcoin/chain/chain';
|
|
2
|
+
export type LocalNodeOptions = {
|
|
3
|
+
network?: string;
|
|
4
|
+
networkVersion?: string;
|
|
5
|
+
stars?: string[];
|
|
6
|
+
root?: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
httpPort?: number | false;
|
|
9
|
+
wsPort?: number | false;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
};
|
|
12
|
+
export type LocalNodeHandle = {
|
|
13
|
+
account: string;
|
|
14
|
+
chain: Chain;
|
|
15
|
+
endpoints: {
|
|
16
|
+
http: string[];
|
|
17
|
+
ws: string[];
|
|
18
|
+
};
|
|
19
|
+
transfer: (to: string, amount: bigint) => Promise<string>;
|
|
20
|
+
stop: () => void;
|
|
21
|
+
};
|
|
22
|
+
export declare const startNode: (options?: LocalNodeOptions) => Promise<LocalNodeHandle>;
|
package/exports/node.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { signTransaction } from '@leofcoin/lib';
|
|
2
|
+
import networks from '@leofcoin/networks';
|
|
3
|
+
import launch from './index.js';
|
|
4
|
+
import '@leofcoin/chain/node';
|
|
5
|
+
import '@leofcoin/chain/chain';
|
|
6
|
+
import '@leofcoin/lib/node-config';
|
|
7
|
+
import '@leofcoin/endpoint-clients/ws';
|
|
8
|
+
import '@leofcoin/endpoint-clients/http';
|
|
9
|
+
|
|
10
|
+
const startNode = async (options = {}) => {
|
|
11
|
+
const network = options.network ?? 'leofcoin:peach';
|
|
12
|
+
const networkVersion = options.networkVersion ?? network.split(':')[1] ?? 'peach';
|
|
13
|
+
const stars = options.stars ?? networks.leofcoin.peach.stars;
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason), { once: true });
|
|
16
|
+
const launched = await launch({
|
|
17
|
+
mode: 'direct', network, networkVersion, stars, root: options.root,
|
|
18
|
+
http: options.httpPort === false ? [] : [{ port: options.httpPort ?? 8080 }],
|
|
19
|
+
ws: options.wsPort === false ? [] : [{ port: options.wsPort ?? 4040 }]
|
|
20
|
+
}, options.password);
|
|
21
|
+
if (!launched.chain)
|
|
22
|
+
throw new Error('node launch did not create a direct chain runtime');
|
|
23
|
+
const chain = launched.chain;
|
|
24
|
+
const chainRuntime = chain;
|
|
25
|
+
const peernet = globalThis.peernet;
|
|
26
|
+
const account = peernet?.selectedAccount;
|
|
27
|
+
if (!account)
|
|
28
|
+
throw new Error('node identity has no selected account');
|
|
29
|
+
const transfer = async (to, amount) => {
|
|
30
|
+
if (!to)
|
|
31
|
+
throw new Error('transfer recipient is required');
|
|
32
|
+
if (amount <= 0n)
|
|
33
|
+
throw new Error('transfer amount must be greater than zero');
|
|
34
|
+
const balance = BigInt(await chainRuntime.balanceOf(account));
|
|
35
|
+
if (balance < amount)
|
|
36
|
+
throw new Error(`insufficient balance: have ${balance}, need at least ${amount}`);
|
|
37
|
+
const raw = await chainRuntime.createTransaction({
|
|
38
|
+
from: account, to: chain.nativeToken, method: 'transfer', params: [to, amount]
|
|
39
|
+
});
|
|
40
|
+
const signed = await signTransaction(raw, peernet.identity);
|
|
41
|
+
const pending = await chainRuntime.sendTransaction(signed);
|
|
42
|
+
return pending.wait;
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
account,
|
|
46
|
+
chain,
|
|
47
|
+
endpoints: launched.endpoints,
|
|
48
|
+
transfer,
|
|
49
|
+
stop: () => controller.abort(new Error('node stopped'))
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { startNode };
|
package/exports/validator-cli.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import '
|
|
2
|
+
import { runCli } from './cli.js';
|
|
3
|
+
import 'node:fs/promises';
|
|
4
|
+
import 'node:os';
|
|
5
|
+
import 'node:path';
|
|
6
|
+
import 'node:url';
|
|
7
|
+
import 'node:readline/promises';
|
|
8
|
+
import './cli-options.js';
|
|
9
|
+
import './node.js';
|
|
6
10
|
import '@leofcoin/lib';
|
|
7
11
|
import '@leofcoin/networks';
|
|
8
12
|
import './index.js';
|
|
@@ -11,62 +15,10 @@ import '@leofcoin/chain/chain';
|
|
|
11
15
|
import '@leofcoin/lib/node-config';
|
|
12
16
|
import '@leofcoin/endpoint-clients/ws';
|
|
13
17
|
import '@leofcoin/endpoint-clients/http';
|
|
18
|
+
import './validator.js';
|
|
19
|
+
import '@leofcoin/addresses';
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (index === -1)
|
|
19
|
-
return undefined;
|
|
20
|
-
const value = args[index + 1];
|
|
21
|
-
if (value === undefined || value.startsWith('--'))
|
|
22
|
-
throw new Error(`${name} requires a value`);
|
|
23
|
-
return value;
|
|
24
|
-
};
|
|
25
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
26
|
-
process.stdout.write(`Usage: leofcoin-validator [options]
|
|
27
|
-
|
|
28
|
-
Options:
|
|
29
|
-
--network <name> Network (default: leofcoin:peach)
|
|
30
|
-
--root <path> Persistent Leofcoin data directory
|
|
31
|
-
--password-file <path> Read the identity password from a private file
|
|
32
|
-
--interval <minutes> Heartbeat interval (default: 5, minimum: 1)
|
|
33
|
-
--http-port <port> HTTP API port (default: 8080)
|
|
34
|
-
--ws-port <port> WebSocket API port (default: 4040)
|
|
35
|
-
--no-endpoints Disable both API endpoints
|
|
36
|
-
-h, --help Show this help
|
|
37
|
-
|
|
38
|
-
Without --password-file, LEOFCOIN_PASSWORD is used when set. Otherwise the
|
|
39
|
-
standard interactive password prompt is shown. Never pass a password directly
|
|
40
|
-
as a command-line argument.
|
|
41
|
-
`);
|
|
42
|
-
process.exit(0);
|
|
43
|
-
}
|
|
44
|
-
const passwordFile = valueAfter('--password-file') ?? process.env.LEOFCOIN_PASSWORD_FILE;
|
|
45
|
-
const password = passwordFile
|
|
46
|
-
? (await readFile(resolve(passwordFile), 'utf8')).trim()
|
|
47
|
-
: process.env.LEOFCOIN_PASSWORD;
|
|
48
|
-
if (passwordFile && !password)
|
|
49
|
-
throw new Error(`empty validator password file: ${resolve(passwordFile)}`);
|
|
50
|
-
const intervalValue = valueAfter('--interval') ?? process.env.LEOFCOIN_TRANSACTION_INTERVAL_MINUTES;
|
|
51
|
-
const noEndpoints = args.includes('--no-endpoints');
|
|
52
|
-
const port = (name, fallback) => {
|
|
53
|
-
const value = Number(valueAfter(name) ?? fallback);
|
|
54
|
-
if (!Number.isSafeInteger(value) || value < 1 || value > 65_535)
|
|
55
|
-
throw new Error(`${name} must be a valid port`);
|
|
56
|
-
return value;
|
|
57
|
-
};
|
|
58
|
-
const handle = await startValidator({
|
|
59
|
-
network: valueAfter('--network') ?? process.env.LEOFCOIN_NETWORK,
|
|
60
|
-
root: valueAfter('--root') ?? process.env.LEOFCOIN_DATA_ROOT,
|
|
61
|
-
password,
|
|
62
|
-
intervalMinutes: intervalValue === undefined ? undefined : Number(intervalValue),
|
|
63
|
-
httpPort: noEndpoints ? false : port('--http-port', 8080),
|
|
64
|
-
wsPort: noEndpoints ? false : port('--ws-port', 4040)
|
|
21
|
+
runCli(['validator', ...process.argv.slice(2)]).catch((error) => {
|
|
22
|
+
process.stderr.write(`leofcoin-validator: ${error?.message ?? error}\n`);
|
|
23
|
+
process.exitCode = 1;
|
|
65
24
|
});
|
|
66
|
-
const shutdown = (signal) => {
|
|
67
|
-
process.stdout.write(`received ${signal}; stopping validator\n`);
|
|
68
|
-
handle.stop();
|
|
69
|
-
setTimeout(() => process.exit(0), 100);
|
|
70
|
-
};
|
|
71
|
-
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
72
|
-
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
package/exports/validator.d.ts
CHANGED
|
@@ -9,12 +9,18 @@ export type ValidatorOptions = {
|
|
|
9
9
|
registrationTimeoutMs?: number;
|
|
10
10
|
httpPort?: number | false;
|
|
11
11
|
wsPort?: number | false;
|
|
12
|
+
heartbeat?: boolean;
|
|
12
13
|
signal?: AbortSignal;
|
|
13
14
|
logger?: Pick<Console, 'log' | 'warn' | 'error'>;
|
|
14
15
|
};
|
|
15
16
|
export type ValidatorHandle = {
|
|
16
17
|
account: string;
|
|
17
18
|
chain: Chain;
|
|
19
|
+
endpoints: {
|
|
20
|
+
http: string[];
|
|
21
|
+
ws: string[];
|
|
22
|
+
};
|
|
23
|
+
transfer: (to: string, amount: bigint) => Promise<string>;
|
|
18
24
|
stop: () => void;
|
|
19
25
|
};
|
|
20
26
|
export declare const validateIntervalMinutes: (value: number) => number;
|
package/exports/validator.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import addresses from '@leofcoin/addresses';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
2
|
+
import { startNode } from './node.js';
|
|
3
|
+
import '@leofcoin/lib';
|
|
4
|
+
import '@leofcoin/networks';
|
|
5
|
+
import './index.js';
|
|
5
6
|
import '@leofcoin/chain/node';
|
|
6
7
|
import '@leofcoin/chain/chain';
|
|
7
8
|
import '@leofcoin/lib/node-config';
|
|
@@ -29,24 +30,22 @@ const validateIntervalMinutes = (value) => {
|
|
|
29
30
|
const startValidator = async (options = {}) => {
|
|
30
31
|
const network = options.network ?? 'leofcoin:peach';
|
|
31
32
|
const networkVersion = options.networkVersion ?? network.split(':')[1] ?? 'peach';
|
|
32
|
-
const stars = options.stars ?? networks.leofcoin.peach.stars;
|
|
33
33
|
const intervalMinutes = validateIntervalMinutes(options.intervalMinutes ?? 5);
|
|
34
34
|
const registrationTimeoutMs = options.registrationTimeoutMs ?? 5 * 60_000;
|
|
35
35
|
const logger = options.logger ?? console;
|
|
36
36
|
const controller = new AbortController();
|
|
37
37
|
options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason), { once: true });
|
|
38
|
-
const
|
|
39
|
-
mode: 'direct',
|
|
38
|
+
const node = await startNode({
|
|
40
39
|
network,
|
|
41
40
|
networkVersion,
|
|
42
|
-
stars,
|
|
41
|
+
stars: options.stars,
|
|
43
42
|
root: options.root,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const chain =
|
|
43
|
+
password: options.password,
|
|
44
|
+
httpPort: options.httpPort,
|
|
45
|
+
wsPort: options.wsPort,
|
|
46
|
+
signal: controller.signal
|
|
47
|
+
});
|
|
48
|
+
const chain = node.chain;
|
|
50
49
|
const chainRuntime = chain;
|
|
51
50
|
const peernet = globalThis.peernet;
|
|
52
51
|
const account = peernet?.selectedAccount;
|
|
@@ -79,6 +78,7 @@ const startValidator = async (options = {}) => {
|
|
|
79
78
|
throw new Error('validator is registered but participation did not activate');
|
|
80
79
|
}
|
|
81
80
|
logger.log('validator participation active');
|
|
81
|
+
const transfer = node.transfer;
|
|
82
82
|
let sending = false;
|
|
83
83
|
const heartbeat = async () => {
|
|
84
84
|
if (controller.signal.aborted || sending)
|
|
@@ -90,15 +90,7 @@ const startValidator = async (options = {}) => {
|
|
|
90
90
|
logger.warn(`heartbeat skipped: balance ${balance} is too low`);
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
|
-
const
|
|
94
|
-
from: account,
|
|
95
|
-
to: chain.nativeToken,
|
|
96
|
-
method: 'transfer',
|
|
97
|
-
params: [account, account, 1n]
|
|
98
|
-
});
|
|
99
|
-
const signed = await signTransaction(raw, peernet.identity);
|
|
100
|
-
const pending = await chainRuntime.sendTransaction(signed);
|
|
101
|
-
const hash = await pending.wait;
|
|
93
|
+
const hash = await transfer(account, 1n);
|
|
102
94
|
logger.log(`heartbeat transaction finalized: ${hash}`);
|
|
103
95
|
}
|
|
104
96
|
catch (error) {
|
|
@@ -108,16 +100,22 @@ const startValidator = async (options = {}) => {
|
|
|
108
100
|
sending = false;
|
|
109
101
|
}
|
|
110
102
|
};
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
// Startup must not wait for a transaction to finalize. In particular, the
|
|
104
|
+
// interactive wallet shell and API endpoints must remain available while a
|
|
105
|
+
// single-node validator is still trying to produce/finalize its heartbeat.
|
|
106
|
+
if (options.heartbeat !== false)
|
|
107
|
+
void heartbeat();
|
|
108
|
+
const interval = options.heartbeat === false ? undefined : setInterval(heartbeat, intervalMinutes * 60_000);
|
|
113
109
|
const stop = () => {
|
|
114
110
|
if (controller.signal.aborted)
|
|
115
111
|
return;
|
|
116
112
|
controller.abort(new Error('validator stopped'));
|
|
117
|
-
|
|
113
|
+
node.stop();
|
|
114
|
+
if (interval)
|
|
115
|
+
clearInterval(interval);
|
|
118
116
|
};
|
|
119
|
-
controller.signal.addEventListener('abort', () => clearInterval(interval), { once: true });
|
|
120
|
-
return { account, chain, stop };
|
|
117
|
+
controller.signal.addEventListener('abort', () => interval && clearInterval(interval), { once: true });
|
|
118
|
+
return { account, chain, endpoints: node.endpoints, transfer, stop };
|
|
121
119
|
};
|
|
122
120
|
|
|
123
121
|
export { startValidator, validateIntervalMinutes };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/launch-chain",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.23",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
"./validator": {
|
|
11
11
|
"import": "./exports/validator.js",
|
|
12
12
|
"types": "./exports/validator.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./node": {
|
|
15
|
+
"import": "./exports/node.js",
|
|
16
|
+
"types": "./exports/node.d.ts"
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
19
|
"bin": {
|
|
20
|
+
"leofcoin": "./exports/cli.js",
|
|
16
21
|
"leofcoin-validator": "./exports/validator-cli.js"
|
|
17
22
|
},
|
|
18
23
|
"files": [
|
|
@@ -41,7 +46,8 @@
|
|
|
41
46
|
"@leofcoin/endpoint-clients": "^0.3.16",
|
|
42
47
|
"@leofcoin/endpoints": "^0.4.9",
|
|
43
48
|
"@leofcoin/lib": "^1.3.10",
|
|
44
|
-
"@leofcoin/networks": "^1.1.30"
|
|
49
|
+
"@leofcoin/networks": "^1.1.30",
|
|
50
|
+
"@vandeurenglenn/wrtc": "^0.5.5"
|
|
45
51
|
},
|
|
46
52
|
"devDependencies": {
|
|
47
53
|
"@rollup/plugin-typescript": "^12.3.0",
|