@leofcoin/launch-chain 0.4.19 → 0.4.21

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
@@ -1,4 +1,4 @@
1
- # lauch-chain
1
+ # launch-chain
2
2
 
3
3
  > launch chain & peernet (reuses exposed transports when already running)
4
4
 
@@ -7,13 +7,44 @@
7
7
  ```js
8
8
  import launch from '@leofcoin/launch-chain'
9
9
 
10
- const { chain, endpoints, mode } = await launch()
10
+ const { chain, endpoints, mode } = await launch(
11
+ { mode: 'direct' },
12
+ process.env.LEOFCOIN_PASSWORD
13
+ )
11
14
  // chain is undefined when mode is remote
12
15
  // endpoints contain urls to connect to the desired remote
13
16
  // when mode is remote means an instance is already running
14
17
  // when mode is direct means chain is directly available and no endpoint is needed to interact with it
15
18
  ```
16
19
 
20
+ ## run a validator
21
+
22
+ No monorepo checkout is required. Run the validator directly from the published package:
23
+
24
+ ```sh
25
+ npx --package @leofcoin/launch-chain leofcoin-validator
26
+ ```
27
+
28
+ The first run uses the normal interactive identity-password prompt. A new account must receive
29
+ the validator contract's minimum balance before it can register. An existing validator must be
30
+ online to finalize that registration transaction.
31
+
32
+ For an unattended server, keep the password outside the repository in a mode-`0600` file:
33
+
34
+ ```sh
35
+ npx --package @leofcoin/launch-chain leofcoin-validator \
36
+ --password-file /secure/leofcoin/password \
37
+ --root /var/lib/leofcoin/peach \
38
+ --interval 5
39
+ ```
40
+
41
+ The validator also exposes the HTTP API on port `8080` and WebSocket API on port `4040`, so one
42
+ process can replace a separate launch-chain server. Change them with `--http-port` and `--ws-port`,
43
+ or use `--no-endpoints` for a validator that should not expose an API.
44
+
45
+ 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.
47
+
17
48
  ## options
18
49
 
19
50
  ### default
@@ -21,7 +52,7 @@ const { chain, endpoints, mode } = await launch()
21
52
  ```js
22
53
  {
23
54
  network: 'leofcoin:peach',
24
- stars: ['wss://peach.leofcoin.org'],
55
+ stars: ['wss://star.leofcoin.org'],
25
56
  forceRemote: false, // when set to true only tries to connect to an external/local exposed node
26
57
  ws: {
27
58
  port: 4040,
@@ -3,15 +3,15 @@ import WSClient from '@leofcoin/endpoint-clients/ws';
3
3
  import HttpClient from '@leofcoin/endpoint-clients/http';
4
4
  type launchMode = 'direct' | 'remote' | 'server';
5
5
  type endpointReturns = {
6
- http?: string[];
7
- ws?: string[];
6
+ http: string[];
7
+ ws: string[];
8
8
  };
9
9
  type clientReturns = {
10
- http?: HttpClient[];
11
- ws?: WSClient[];
10
+ http: HttpClient[];
11
+ ws: WSClient[];
12
12
  };
13
13
  type launchReturn = {
14
- chain: Chain;
14
+ chain?: Chain;
15
15
  mode: launchMode;
16
16
  endpoints: endpointReturns;
17
17
  clients: clientReturns;
@@ -23,9 +23,10 @@ type endpointOptions = {
23
23
  type launchOptions = {
24
24
  network?: string;
25
25
  networkVersion?: string;
26
- stars: string[];
27
- forceRemote: boolean;
26
+ stars?: string[];
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: (options: launchOptions, 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
@@ -72,11 +73,8 @@ const getWS = async (url, networkVersion) => {
72
73
  * @param {object} options { ws: boolean || {url: string, port: number}, http: boolean || {url: string, port: number}, network}
73
74
  * @returns '{ mode: string, endpoints: object, chain}'
74
75
  */
75
- const launch = async (options, password) => {
76
- if (!options)
77
- options = defaultOptions;
78
- else
79
- options = { ...defaultOptions, ...options };
76
+ const launch = async (input = {}, password) => {
77
+ const options = { ...defaultOptions, ...input };
80
78
  const clients = {
81
79
  http: [],
82
80
  ws: []
@@ -89,26 +87,29 @@ const launch = async (options, password) => {
89
87
  if (options.mode === 'remote') {
90
88
  if (options.http) {
91
89
  for (const endpoint of options.http) {
92
- if (endpoint.port && !endpoint.url)
93
- endpoint.url = `http://localhost:${endpoint.port}`;
94
- const client = await getHttp(endpoint.url, options.networkVersion);
95
- if (client)
96
- endpoints.http.push(endpoint.url) && clients.http.push(client);
90
+ const url = endpoint.url ?? `http://localhost:${endpoint.port}`;
91
+ const client = await getHttp(url, options.networkVersion);
92
+ if (client) {
93
+ endpoints.http.push(url);
94
+ clients.http.push(client);
95
+ }
97
96
  }
98
97
  }
99
98
  if (options.ws) {
100
99
  for (const endpoint of options.ws) {
101
- if (endpoint.port && !endpoint.url)
102
- endpoint.url = `ws://localhost:${endpoint.port}`;
103
- const client = await getWS(endpoint.url, options.networkVersion);
104
- client && endpoints.ws.push(endpoint.url) && clients.ws.push(client);
100
+ const url = endpoint.url ?? `ws://localhost:${endpoint.port}`;
101
+ const client = await getWS(url, options.networkVersion);
102
+ if (client) {
103
+ endpoints.ws.push(url);
104
+ clients.ws.push(client);
105
+ }
105
106
  }
106
107
  }
107
108
  if (endpoints.http.length === 0 && endpoints.ws.length === 0)
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 });
@@ -117,11 +118,10 @@ const launch = async (options, password) => {
117
118
  const importee = await import('@leofcoin/endpoints/ws');
118
119
  const wsServer = importee.default;
119
120
  for (const endpoint of options.ws) {
120
- if (endpoint.port && !endpoint.url)
121
- endpoint.url = `ws://localhost:${endpoint.port}`;
121
+ const url = endpoint.url ?? `ws://localhost:${endpoint.port}`;
122
122
  await wsServer(chain, endpoint.port, options.networkVersion);
123
- endpoints.ws.push(endpoint.url);
124
- const client = await getWS(endpoint.url, options.networkVersion);
123
+ endpoints.ws.push(url);
124
+ const client = await getWS(url, options.networkVersion);
125
125
  client && clients.ws.push(client);
126
126
  }
127
127
  }
@@ -129,17 +129,16 @@ const launch = async (options, password) => {
129
129
  const importee = await import('@leofcoin/endpoints/http');
130
130
  const httpServer = importee.default;
131
131
  for (const endpoint of options.http) {
132
- if (endpoint.port && !endpoint.url)
133
- endpoint.url = `http://localhost:${endpoint.port}`;
132
+ const url = endpoint.url ?? `http://localhost:${endpoint.port}`;
134
133
  await httpServer(chain, endpoint.port, options.networkVersion);
135
- endpoints.http.push(endpoint.url);
136
- const client = await getHttp(endpoint.url, options.networkVersion);
134
+ endpoints.http.push(url);
135
+ const client = await getHttp(url, options.networkVersion);
137
136
  client && clients.http.push(client);
138
137
  }
139
138
  }
140
139
  }
141
140
  else {
142
- 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);
143
142
  await node.ready;
144
143
  await nodeConfig({ network: options.network, networkVersion: options.networkVersion });
145
144
  chain = new Chain({ network: options.network, stars: options.stars, networkVersion: options.networkVersion });
@@ -148,20 +147,18 @@ const launch = async (options, password) => {
148
147
  const importee = await import('@leofcoin/endpoints/ws');
149
148
  const wsServer = importee.default;
150
149
  for (const endpoint of options.ws) {
151
- if (endpoint.port && !endpoint.url)
152
- endpoint.url = `ws://localhost:${endpoint.port}`;
150
+ const url = endpoint.url ?? `ws://localhost:${endpoint.port}`;
153
151
  await wsServer(chain, endpoint.port, options.networkVersion);
154
- endpoints.ws.push(endpoint.url);
152
+ endpoints.ws.push(url);
155
153
  }
156
154
  }
157
155
  if (options.http) {
158
156
  const importee = await import('@leofcoin/endpoints/http');
159
157
  const httpServer = importee.default;
160
158
  for (const endpoint of options.http) {
161
- if (endpoint.port && !endpoint.url)
162
- endpoint.url = `http://localhost:${endpoint.port}`;
159
+ const url = endpoint.url ?? `http://localhost:${endpoint.port}`;
163
160
  await httpServer(chain, endpoint.port, options.networkVersion);
164
- endpoints.http.push(endpoint.url);
161
+ endpoints.http.push(url);
165
162
  }
166
163
  }
167
164
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,72 @@
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';
6
+ import '@leofcoin/lib';
7
+ import '@leofcoin/networks';
8
+ import './index.js';
9
+ import '@leofcoin/chain/node';
10
+ import '@leofcoin/chain/chain';
11
+ import '@leofcoin/lib/node-config';
12
+ import '@leofcoin/endpoint-clients/ws';
13
+ import '@leofcoin/endpoint-clients/http';
14
+
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)
65
+ });
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'));
@@ -0,0 +1,21 @@
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
+ signal?: AbortSignal;
13
+ logger?: Pick<Console, 'log' | 'warn' | 'error'>;
14
+ };
15
+ export type ValidatorHandle = {
16
+ account: string;
17
+ chain: Chain;
18
+ stop: () => void;
19
+ };
20
+ export declare const validateIntervalMinutes: (value: number) => number;
21
+ export declare const startValidator: (options?: ValidatorOptions) => Promise<ValidatorHandle>;
@@ -0,0 +1,123 @@
1
+ import addresses from '@leofcoin/addresses';
2
+ import { signTransaction } from '@leofcoin/lib';
3
+ import networks from '@leofcoin/networks';
4
+ import launch from './index.js';
5
+ import '@leofcoin/chain/node';
6
+ import '@leofcoin/chain/chain';
7
+ import '@leofcoin/lib/node-config';
8
+ import '@leofcoin/endpoint-clients/ws';
9
+ import '@leofcoin/endpoint-clients/http';
10
+
11
+ const delay = (milliseconds, signal) => new Promise((resolve, reject) => {
12
+ if (signal?.aborted)
13
+ return reject(signal.reason ?? new Error('validator stopped'));
14
+ const onAbort = () => {
15
+ clearTimeout(timeout);
16
+ reject(signal?.reason ?? new Error('validator stopped'));
17
+ };
18
+ const timeout = setTimeout(() => {
19
+ signal?.removeEventListener('abort', onAbort);
20
+ resolve();
21
+ }, milliseconds);
22
+ signal?.addEventListener('abort', onAbort, { once: true });
23
+ });
24
+ const validateIntervalMinutes = (value) => {
25
+ if (!Number.isFinite(value) || value < 1)
26
+ throw new Error('validator interval must be at least one minute');
27
+ return value;
28
+ };
29
+ const startValidator = async (options = {}) => {
30
+ const network = options.network ?? 'leofcoin:peach';
31
+ const networkVersion = options.networkVersion ?? network.split(':')[1] ?? 'peach';
32
+ const stars = options.stars ?? networks.leofcoin.peach.stars;
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 launched = await launch({
39
+ mode: 'direct',
40
+ network,
41
+ networkVersion,
42
+ stars,
43
+ 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;
50
+ const chainRuntime = chain;
51
+ const peernet = globalThis.peernet;
52
+ const account = peernet?.selectedAccount;
53
+ if (!account)
54
+ throw new Error('validator identity has no selected account');
55
+ logger.log(`validator account: ${account}`);
56
+ const isRegistered = () => chainRuntime.staticCall(addresses.validators, 'has', [account]);
57
+ if (!(await isRegistered())) {
58
+ const [balanceValue, minimumValue] = await Promise.all([
59
+ chainRuntime.balanceOf(account),
60
+ chainRuntime.staticCall(addresses.validators, 'minimumBalance')
61
+ ]);
62
+ const balance = BigInt(balanceValue);
63
+ const minimumBalance = BigInt(minimumValue);
64
+ if (balance < minimumBalance) {
65
+ throw new Error(`validator account ${account} has balance ${balance}; registration requires at least ${minimumBalance}`);
66
+ }
67
+ const submitted = await chainRuntime.participate(account);
68
+ if (submitted)
69
+ throw new Error('validator registration state changed unexpectedly');
70
+ logger.log('validator registration submitted; waiting for finalization...');
71
+ const deadline = Date.now() + registrationTimeoutMs;
72
+ while (Date.now() < deadline && !(await isRegistered()))
73
+ await delay(5_000, controller.signal);
74
+ if (!(await isRegistered())) {
75
+ throw new Error('validator registration was not finalized; at least one existing validator must be online to produce it');
76
+ }
77
+ }
78
+ if (!(await chainRuntime.participate(account))) {
79
+ throw new Error('validator is registered but participation did not activate');
80
+ }
81
+ logger.log('validator participation active');
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 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;
102
+ logger.log(`heartbeat transaction finalized: ${hash}`);
103
+ }
104
+ catch (error) {
105
+ logger.error(`heartbeat transaction failed: ${error?.message ?? error}`);
106
+ }
107
+ finally {
108
+ sending = false;
109
+ }
110
+ };
111
+ await heartbeat();
112
+ const interval = setInterval(heartbeat, intervalMinutes * 60_000);
113
+ const stop = () => {
114
+ if (controller.signal.aborted)
115
+ return;
116
+ controller.abort(new Error('validator stopped'));
117
+ clearInterval(interval);
118
+ };
119
+ controller.signal.addEventListener('abort', () => clearInterval(interval), { once: true });
120
+ return { account, chain, stop };
121
+ };
122
+
123
+ export { startValidator, validateIntervalMinutes };
package/package.json CHANGED
@@ -1,24 +1,32 @@
1
1
  {
2
2
  "name": "@leofcoin/launch-chain",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
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"
9
13
  }
10
14
  },
15
+ "bin": {
16
+ "leofcoin-validator": "./exports/validator-cli.js"
17
+ },
11
18
  "files": [
12
19
  "exports"
13
20
  ],
14
21
  "type": "module",
15
22
  "scripts": {
16
23
  "build": "rollup -c",
17
- "test": "echo \"Error: no test specified\" && exit 1"
24
+ "test": "node --test test/*.test.js"
18
25
  },
19
26
  "repository": {
20
27
  "type": "git",
21
- "url": "git+https://github.com/leofcoin/launch-chain.git"
28
+ "url": "https://github.com/leofcoin/monorepo.git",
29
+ "directory": "packages/launch-chain"
22
30
  },
23
31
  "keywords": [],
24
32
  "author": "",
@@ -26,11 +34,13 @@
26
34
  "bugs": {
27
35
  "url": "https://github.com/leofcoin/launch-chain/issues"
28
36
  },
29
- "homepage": "https://github.com/leofcoin/launch-chain#readme",
37
+ "homepage": "https://github.com/leofcoin/monorepo/tree/main/packages/launch-chain#readme",
30
38
  "dependencies": {
31
- "@leofcoin/chain": "^1.10.9",
32
- "@leofcoin/endpoint-clients": "^0.3.15",
33
- "@leofcoin/endpoints": "^0.4.8",
39
+ "@leofcoin/addresses": "^1.0.62",
40
+ "@leofcoin/chain": "^1.10.12",
41
+ "@leofcoin/endpoint-clients": "^0.3.16",
42
+ "@leofcoin/endpoints": "^0.4.9",
43
+ "@leofcoin/lib": "^1.3.10",
34
44
  "@leofcoin/networks": "^1.1.30"
35
45
  },
36
46
  "devDependencies": {
@@ -1,38 +0,0 @@
1
- import Chain from '@leofcoin/chain/chain';
2
- import WSClient from '@leofcoin/endpoint-clients/ws';
3
- import HttpClient from '@leofcoin/endpoint-clients/http';
4
- type launchMode = 'direct' | 'remote' | 'server';
5
- type endpointReturns = {
6
- http?: string[];
7
- ws?: string[];
8
- };
9
- type clientReturns = {
10
- http?: HttpClient[];
11
- ws?: WSClient[];
12
- };
13
- type launchReturn = {
14
- chain: Chain;
15
- mode: launchMode;
16
- endpoints: endpointReturns;
17
- clients: clientReturns;
18
- };
19
- type endpointOptions = {
20
- port: number;
21
- url?: string;
22
- };
23
- type launchOptions = {
24
- network?: string;
25
- networkVersion?: string;
26
- stars: string[];
27
- forceRemote: boolean;
28
- mode?: launchMode;
29
- ws?: endpointOptions[] | undefined;
30
- http?: endpointOptions[] | undefined;
31
- };
32
- /**
33
- *
34
- * @param {object} options { ws: boolean || {url: string, port: number}, http: boolean || {url: string, port: number}, network}
35
- * @returns '{ mode: string, endpoints: object, chain}'
36
- */
37
- declare const launch: (options: launchOptions, password: string) => Promise<launchReturn>;
38
- export { launch as default };