@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 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-validator
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
- Use `leofcoin-validator --help` for all options.
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 };
@@ -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>;
@@ -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 };
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { readFile } from 'node:fs/promises';
3
- import { resolve } from 'node:path';
4
- import { startValidator } from './validator.js';
5
- import '@leofcoin/addresses';
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
- const args = process.argv.slice(2);
16
- const valueAfter = (name) => {
17
- const index = args.indexOf(name);
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'));
@@ -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;
@@ -1,7 +1,8 @@
1
1
  import addresses from '@leofcoin/addresses';
2
- import { signTransaction } from '@leofcoin/lib';
3
- import networks from '@leofcoin/networks';
4
- import launch from './index.js';
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 launched = await launch({
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
- http: options.httpPort === false ? [] : [{ port: options.httpPort ?? 8080 }],
45
- ws: options.wsPort === false ? [] : [{ port: options.wsPort ?? 4040 }]
46
- }, options.password);
47
- if (!launched.chain)
48
- throw new Error('validator launch did not create a direct chain runtime');
49
- const chain = launched.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 raw = await chainRuntime.createTransaction({
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
- await heartbeat();
112
- const interval = setInterval(heartbeat, intervalMinutes * 60_000);
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
- clearInterval(interval);
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.21",
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",