@leofcoin/launch-chain 0.4.20 → 0.4.22

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,6 +17,74 @@ 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
+
38
+ ## run a validator
39
+
40
+ No monorepo checkout is required. Run the validator directly from the published package:
41
+
42
+ ```sh
43
+ npx --package @leofcoin/launch-chain leofcoin validator
44
+ ```
45
+
46
+ The first run uses the normal interactive identity-password prompt. A new account must receive
47
+ the validator contract's minimum balance before it can register. An existing validator must be
48
+ online to finalize that registration transaction.
49
+
50
+ For an unattended server, keep the password outside the repository in a mode-`0600` file:
51
+
52
+ ```sh
53
+ npx --package @leofcoin/launch-chain leofcoin-validator \
54
+ --password-file /secure/leofcoin/password \
55
+ --root /var/lib/leofcoin/peach \
56
+ --interval 5
57
+ ```
58
+
59
+ The validator also exposes the HTTP API on port `8080` and WebSocket API on port `4040`, so one
60
+ process can replace a separate launch-chain server. Change them with `--http-port` and `--ws-port`,
61
+ or use `--no-endpoints` for a validator that should not expose an API.
62
+
63
+ Never put the password or an identity backup in a command-line argument, service file, or Git.
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.
87
+
20
88
  ## options
21
89
 
22
90
  ### default
@@ -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 };
@@ -26,6 +26,7 @@ type launchOptions = {
26
26
  stars?: string[];
27
27
  forceRemote?: boolean;
28
28
  mode?: launchMode;
29
+ root?: string;
29
30
  ws?: endpointOptions[] | undefined;
30
31
  http?: endpointOptions[] | undefined;
31
32
  };
@@ -34,5 +35,5 @@ type launchOptions = {
34
35
  * @param {object} options { ws: boolean || {url: string, port: number}, http: boolean || {url: string, port: number}, network}
35
36
  * @returns '{ mode: string, endpoints: object, chain}'
36
37
  */
37
- declare const launch: (input: launchOptions | undefined, password: string) => Promise<launchReturn>;
38
+ declare const launch: (input?: launchOptions, password?: string) => Promise<launchReturn>;
38
39
  export { launch as default };
package/exports/index.js CHANGED
@@ -11,6 +11,7 @@ const defaultOptions = {
11
11
  stars: networks.leofcoin.peach.stars,
12
12
  forceRemote: false,
13
13
  mode: 'direct',
14
+ root: undefined,
14
15
  ws: [
15
16
  {
16
17
  port: 4040
@@ -108,7 +109,7 @@ const launch = async (input = {}, password) => {
108
109
  throw new Error(`no remotes connected`);
109
110
  }
110
111
  else if (options.mode === 'direct') {
111
- const node = new Node({ network: options.network, stars: options.stars, networkVersion: options.networkVersion }, password);
112
+ const node = new Node({ network: options.network, stars: options.stars, networkVersion: options.networkVersion, root: options.root }, password);
112
113
  await node.ready;
113
114
  await nodeConfig({ network: options.network, networkVersion: options.networkVersion });
114
115
  chain = new Chain({ network: options.network, stars: options.stars, networkVersion: options.networkVersion });
@@ -137,7 +138,7 @@ const launch = async (input = {}, password) => {
137
138
  }
138
139
  }
139
140
  else {
140
- const node = new Node({ network: options.network, stars: options.stars, networkVersion: options.networkVersion }, password);
141
+ const node = new Node({ network: options.network, stars: options.stars, networkVersion: options.networkVersion, root: options.root }, password);
141
142
  await node.ready;
142
143
  await nodeConfig({ network: options.network, networkVersion: options.networkVersion });
143
144
  chain = new Chain({ network: options.network, stars: options.stars, networkVersion: options.networkVersion });
@@ -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 };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
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';
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 './validator.js';
19
+ import '@leofcoin/addresses';
20
+
21
+ runCli(['validator', ...process.argv.slice(2)]).catch((error) => {
22
+ process.stderr.write(`leofcoin-validator: ${error?.message ?? error}\n`);
23
+ process.exitCode = 1;
24
+ });
@@ -0,0 +1,27 @@
1
+ import Chain from '@leofcoin/chain/chain';
2
+ export type ValidatorOptions = {
3
+ network?: string;
4
+ networkVersion?: string;
5
+ stars?: string[];
6
+ root?: string;
7
+ password?: string;
8
+ intervalMinutes?: number;
9
+ registrationTimeoutMs?: number;
10
+ httpPort?: number | false;
11
+ wsPort?: number | false;
12
+ heartbeat?: boolean;
13
+ signal?: AbortSignal;
14
+ logger?: Pick<Console, 'log' | 'warn' | 'error'>;
15
+ };
16
+ export type ValidatorHandle = {
17
+ account: string;
18
+ chain: Chain;
19
+ endpoints: {
20
+ http: string[];
21
+ ws: string[];
22
+ };
23
+ transfer: (to: string, amount: bigint) => Promise<string>;
24
+ stop: () => void;
25
+ };
26
+ export declare const validateIntervalMinutes: (value: number) => number;
27
+ export declare const startValidator: (options?: ValidatorOptions) => Promise<ValidatorHandle>;
@@ -0,0 +1,121 @@
1
+ import addresses from '@leofcoin/addresses';
2
+ import { startNode } from './node.js';
3
+ import '@leofcoin/lib';
4
+ import '@leofcoin/networks';
5
+ import './index.js';
6
+ import '@leofcoin/chain/node';
7
+ import '@leofcoin/chain/chain';
8
+ import '@leofcoin/lib/node-config';
9
+ import '@leofcoin/endpoint-clients/ws';
10
+ import '@leofcoin/endpoint-clients/http';
11
+
12
+ const delay = (milliseconds, signal) => new Promise((resolve, reject) => {
13
+ if (signal?.aborted)
14
+ return reject(signal.reason ?? new Error('validator stopped'));
15
+ const onAbort = () => {
16
+ clearTimeout(timeout);
17
+ reject(signal?.reason ?? new Error('validator stopped'));
18
+ };
19
+ const timeout = setTimeout(() => {
20
+ signal?.removeEventListener('abort', onAbort);
21
+ resolve();
22
+ }, milliseconds);
23
+ signal?.addEventListener('abort', onAbort, { once: true });
24
+ });
25
+ const validateIntervalMinutes = (value) => {
26
+ if (!Number.isFinite(value) || value < 1)
27
+ throw new Error('validator interval must be at least one minute');
28
+ return value;
29
+ };
30
+ const startValidator = async (options = {}) => {
31
+ const network = options.network ?? 'leofcoin:peach';
32
+ const networkVersion = options.networkVersion ?? network.split(':')[1] ?? 'peach';
33
+ const intervalMinutes = validateIntervalMinutes(options.intervalMinutes ?? 5);
34
+ const registrationTimeoutMs = options.registrationTimeoutMs ?? 5 * 60_000;
35
+ const logger = options.logger ?? console;
36
+ const controller = new AbortController();
37
+ options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason), { once: true });
38
+ const node = await startNode({
39
+ network,
40
+ networkVersion,
41
+ stars: options.stars,
42
+ root: options.root,
43
+ password: options.password,
44
+ httpPort: options.httpPort,
45
+ wsPort: options.wsPort,
46
+ signal: controller.signal
47
+ });
48
+ const chain = node.chain;
49
+ const chainRuntime = chain;
50
+ const peernet = globalThis.peernet;
51
+ const account = peernet?.selectedAccount;
52
+ if (!account)
53
+ throw new Error('validator identity has no selected account');
54
+ logger.log(`validator account: ${account}`);
55
+ const isRegistered = () => chainRuntime.staticCall(addresses.validators, 'has', [account]);
56
+ if (!(await isRegistered())) {
57
+ const [balanceValue, minimumValue] = await Promise.all([
58
+ chainRuntime.balanceOf(account),
59
+ chainRuntime.staticCall(addresses.validators, 'minimumBalance')
60
+ ]);
61
+ const balance = BigInt(balanceValue);
62
+ const minimumBalance = BigInt(minimumValue);
63
+ if (balance < minimumBalance) {
64
+ throw new Error(`validator account ${account} has balance ${balance}; registration requires at least ${minimumBalance}`);
65
+ }
66
+ const submitted = await chainRuntime.participate(account);
67
+ if (submitted)
68
+ throw new Error('validator registration state changed unexpectedly');
69
+ logger.log('validator registration submitted; waiting for finalization...');
70
+ const deadline = Date.now() + registrationTimeoutMs;
71
+ while (Date.now() < deadline && !(await isRegistered()))
72
+ await delay(5_000, controller.signal);
73
+ if (!(await isRegistered())) {
74
+ throw new Error('validator registration was not finalized; at least one existing validator must be online to produce it');
75
+ }
76
+ }
77
+ if (!(await chainRuntime.participate(account))) {
78
+ throw new Error('validator is registered but participation did not activate');
79
+ }
80
+ logger.log('validator participation active');
81
+ const transfer = node.transfer;
82
+ let sending = false;
83
+ const heartbeat = async () => {
84
+ if (controller.signal.aborted || sending)
85
+ return;
86
+ sending = true;
87
+ try {
88
+ const balance = BigInt(await chainRuntime.balanceOf(account));
89
+ if (balance <= 1n) {
90
+ logger.warn(`heartbeat skipped: balance ${balance} is too low`);
91
+ return;
92
+ }
93
+ const hash = await transfer(account, 1n);
94
+ logger.log(`heartbeat transaction finalized: ${hash}`);
95
+ }
96
+ catch (error) {
97
+ logger.error(`heartbeat transaction failed: ${error?.message ?? error}`);
98
+ }
99
+ finally {
100
+ sending = false;
101
+ }
102
+ };
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);
109
+ const stop = () => {
110
+ if (controller.signal.aborted)
111
+ return;
112
+ controller.abort(new Error('validator stopped'));
113
+ node.stop();
114
+ if (interval)
115
+ clearInterval(interval);
116
+ };
117
+ controller.signal.addEventListener('abort', () => interval && clearInterval(interval), { once: true });
118
+ return { account, chain, endpoints: node.endpoints, transfer, stop };
119
+ };
120
+
121
+ export { startValidator, validateIntervalMinutes };
package/package.json CHANGED
@@ -1,13 +1,25 @@
1
1
  {
2
2
  "name": "@leofcoin/launch-chain",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
7
7
  "import": "./exports/index.js",
8
8
  "types": "./exports/index.d.ts"
9
+ },
10
+ "./validator": {
11
+ "import": "./exports/validator.js",
12
+ "types": "./exports/validator.d.ts"
13
+ },
14
+ "./node": {
15
+ "import": "./exports/node.js",
16
+ "types": "./exports/node.d.ts"
9
17
  }
10
18
  },
19
+ "bin": {
20
+ "leofcoin": "./exports/cli.js",
21
+ "leofcoin-validator": "./exports/validator-cli.js"
22
+ },
11
23
  "files": [
12
24
  "exports"
13
25
  ],
@@ -29,9 +41,11 @@
29
41
  },
30
42
  "homepage": "https://github.com/leofcoin/monorepo/tree/main/packages/launch-chain#readme",
31
43
  "dependencies": {
32
- "@leofcoin/chain": "^1.10.11",
33
- "@leofcoin/endpoint-clients": "^0.3.15",
34
- "@leofcoin/endpoints": "^0.4.8",
44
+ "@leofcoin/addresses": "^1.0.62",
45
+ "@leofcoin/chain": "^1.10.12",
46
+ "@leofcoin/endpoint-clients": "^0.3.16",
47
+ "@leofcoin/endpoints": "^0.4.9",
48
+ "@leofcoin/lib": "^1.3.10",
35
49
  "@leofcoin/networks": "^1.1.30"
36
50
  },
37
51
  "devDependencies": {