@leofcoin/launch-chain 0.4.20 → 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 +28 -0
- package/exports/index.d.ts +2 -1
- package/exports/index.js +3 -2
- package/exports/validator-cli.d.ts +2 -0
- package/exports/validator-cli.js +72 -0
- package/exports/validator.d.ts +21 -0
- package/exports/validator.js +123 -0
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -17,6 +17,34 @@ 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
|
+
## 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
|
+
|
|
20
48
|
## options
|
|
21
49
|
|
|
22
50
|
### default
|
package/exports/index.d.ts
CHANGED
|
@@ -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
|
|
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,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,13 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/launch-chain",
|
|
3
|
-
"version": "0.4.
|
|
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
|
],
|
|
@@ -29,9 +36,11 @@
|
|
|
29
36
|
},
|
|
30
37
|
"homepage": "https://github.com/leofcoin/monorepo/tree/main/packages/launch-chain#readme",
|
|
31
38
|
"dependencies": {
|
|
32
|
-
"@leofcoin/
|
|
33
|
-
"@leofcoin/
|
|
34
|
-
"@leofcoin/
|
|
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",
|
|
35
44
|
"@leofcoin/networks": "^1.1.30"
|
|
36
45
|
},
|
|
37
46
|
"devDependencies": {
|