trac-peer 0.4.5 → 0.4.6
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/DOCS.md +9 -3
- package/README.md +7 -1
- package/package.json +8 -10
- package/scripts/pear-runner.mjs +163 -0
- package/scripts/run-peer.mjs +58 -34
- package/src/config/env.js +25 -1
- package/src/msbClient.js +6 -2
- package/src/pearCompat.js +49 -0
- package/src/terminal/handlers.js +2 -1
- package/src/terminal/index.js +13 -14
- package/src/wallet.js +0 -4
- package/tests/unit/pearCompat.test.js +46 -0
- package/tests/unit/terminalRuntime.test.js +25 -0
- package/tests/unit/unit.test.js +3 -0
- package/tests/unit/walletNetworkConfig.test.js +42 -0
package/DOCS.md
CHANGED
|
@@ -30,7 +30,8 @@ In practice:
|
|
|
30
30
|
## Requirements
|
|
31
31
|
|
|
32
32
|
- Node.js + npm (recommended: a modern Node LTS).
|
|
33
|
-
-
|
|
33
|
+
- The Pear CLI is optional. `peer:pear` automatically uses legacy `pear run` when it detects Pear v2,
|
|
34
|
+
and uses the embedded `pear-runtime` worker for Pear v3 (or when Pear is not installed).
|
|
34
35
|
- You need your **MSB bootstrap** (32‑byte hex / 64 hex characters) and **MSB channel** (string).
|
|
35
36
|
|
|
36
37
|
---
|
|
@@ -112,7 +113,10 @@ Notes:
|
|
|
112
113
|
|
|
113
114
|
## Start a peer (Pear runner)
|
|
114
115
|
|
|
115
|
-
Pear runner
|
|
116
|
+
The Pear compatibility runner uses the same command on Pear v2 and v3. It delegates to legacy
|
|
117
|
+
`pear run` on v2; on v3, where that command no longer exists, it launches the peer as a Bare worker
|
|
118
|
+
through the embedded `pear-runtime` module. If no Pear platform version can be detected, it safely
|
|
119
|
+
uses the module path.
|
|
116
120
|
|
|
117
121
|
Recommended: set store names explicitly (this avoids confusion when running multiple nodes on one machine).
|
|
118
122
|
|
|
@@ -323,7 +327,7 @@ npm run peer:run -- \
|
|
|
323
327
|
--subnet-channel=trac-peer-subnet
|
|
324
328
|
```
|
|
325
329
|
|
|
326
|
-
If you started Peer 1 with Pear,
|
|
330
|
+
If you started Peer 1 with the Pear compatibility runner, use the same runner for Peer 2:
|
|
327
331
|
|
|
328
332
|
```sh
|
|
329
333
|
npm run peer:pear -- \
|
|
@@ -394,6 +398,8 @@ npm run peer:run-rpc -- \
|
|
|
394
398
|
|
|
395
399
|
### Start with RPC enabled (Pear)
|
|
396
400
|
|
|
401
|
+
This command uses the same automatic Pear v2/v3 selection described above.
|
|
402
|
+
|
|
397
403
|
```sh
|
|
398
404
|
npm run peer:pear-rpc -- \
|
|
399
405
|
--msb-bootstrap=<hex32> \
|
package/README.md
CHANGED
|
@@ -35,7 +35,13 @@ npm run peer:run -- --msb-bootstrap=<32-byte-hex> --msb-channel=<channel-string>
|
|
|
35
35
|
|
|
36
36
|
### Pear runner (interactive)
|
|
37
37
|
|
|
38
|
-
Runs `trac-peer`
|
|
38
|
+
Runs `trac-peer` through the Pear v2/v3 compatibility runner. With Pear v2 it delegates to the legacy
|
|
39
|
+
`pear run` command; with Pear v3 (where `pear run` was removed), or when no Pear platform version can be
|
|
40
|
+
detected, it starts the same entrypoint with the embedded `pear-runtime` module. The npm command and all
|
|
41
|
+
application flags are identical in both modes.
|
|
42
|
+
|
|
43
|
+
You can control stores via flags (recommended); an optional first positional arg can be used as a “store
|
|
44
|
+
label” fallback if `--peer-store-name` is omitted.
|
|
39
45
|
|
|
40
46
|
```sh
|
|
41
47
|
npm run peer:pear -- \
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trac-peer",
|
|
3
3
|
"main": "src/index.js",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"pear": {
|
|
7
7
|
"name": "trac-peer",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"peer:run": "node scripts/run-peer.mjs",
|
|
23
23
|
"peer:run-rpc": "node scripts/run-peer.mjs --rpc",
|
|
24
24
|
"peer:smoke": "node scripts/smoke-msb-sync.mjs",
|
|
25
|
-
"peer:pear": "
|
|
26
|
-
"peer:pear-rpc": "
|
|
25
|
+
"peer:pear": "node scripts/pear-runner.mjs",
|
|
26
|
+
"peer:pear-rpc": "node scripts/pear-runner.mjs --rpc",
|
|
27
27
|
"test:unit:node": "brittle-node -t 60000 tests/unit/unit.test.js",
|
|
28
28
|
"test:unit:bare": "brittle-bare -t 60000 tests/unit/unit.test.js",
|
|
29
29
|
"test:unit:all": "(npm run test:unit:node && npm run test:unit:bare) || exit 1",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"bare-assert": "1.0.2",
|
|
41
41
|
"bare-buffer": "3.1.2",
|
|
42
42
|
"bare-console": "6.0.1",
|
|
43
|
-
"bare-crypto": "1.
|
|
43
|
+
"bare-crypto": "1.12.0",
|
|
44
44
|
"bare-events": "2.5.4",
|
|
45
45
|
"bare-fetch": "2.2.2",
|
|
46
46
|
"bare-fs": "^4.1.5",
|
|
@@ -57,7 +57,6 @@
|
|
|
57
57
|
"bare-subprocess": "5.0.4",
|
|
58
58
|
"bare-timers": "3.0.1",
|
|
59
59
|
"bare-tls": "2.0.4",
|
|
60
|
-
"bare-tty": "5.0.2",
|
|
61
60
|
"bare-url": "2.1.5",
|
|
62
61
|
"bare-utils": "1.5.1",
|
|
63
62
|
"bare-worker": "3.0.0",
|
|
@@ -88,6 +87,7 @@
|
|
|
88
87
|
"os": "npm:bare-node-os",
|
|
89
88
|
"path": "npm:bare-node-path",
|
|
90
89
|
"pear-interface": "1.1.0",
|
|
90
|
+
"pear-runtime": "1.3.1",
|
|
91
91
|
"process": "npm:bare-node-process",
|
|
92
92
|
"protomux": "^3.10.1",
|
|
93
93
|
"protomux-wakeup": "^2.4.0",
|
|
@@ -95,14 +95,12 @@
|
|
|
95
95
|
"ready-resource": "1.1.2",
|
|
96
96
|
"repl": "npm:bare-node-repl",
|
|
97
97
|
"safety-catch": "1.0.2",
|
|
98
|
-
"sodium-native": "5.0.1",
|
|
99
98
|
"stream": "npm:bare-node-stream",
|
|
100
99
|
"timers": "npm:bare-node-timers",
|
|
101
100
|
"tls": "npm:bare-node-tls",
|
|
102
|
-
"trac-crypto-api": "^0.1.
|
|
103
|
-
"trac-msb": "^0.2.
|
|
104
|
-
"trac-wallet": "^1.0.
|
|
105
|
-
"tty": "npm:bare-node-tty",
|
|
101
|
+
"trac-crypto-api": "^0.1.5",
|
|
102
|
+
"trac-msb": "^0.2.19",
|
|
103
|
+
"trac-wallet": "^1.0.4",
|
|
106
104
|
"url": "npm:bare-node-url",
|
|
107
105
|
"util": "npm:bare-node-util",
|
|
108
106
|
"worker_threads": "npm:bare-node-worker-threads",
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { constants as osConstants } from "node:os";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { parsePearMajor, selectPearRunnerMode } from "../src/pearCompat.js";
|
|
7
|
+
|
|
8
|
+
const projectDirectory = fileURLToPath(new URL("../", import.meta.url));
|
|
9
|
+
const entrypoint = fileURLToPath(new URL("./run-peer.mjs", import.meta.url));
|
|
10
|
+
const appArgs = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
const exitCodeFor = (code, signal) => {
|
|
13
|
+
if (code !== null && code !== undefined) return code;
|
|
14
|
+
const signalNumber = osConstants.signals?.[signal];
|
|
15
|
+
return Number.isInteger(signalNumber) ? 128 + signalNumber : 1;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const detectPear = () => {
|
|
19
|
+
for (const args of [
|
|
20
|
+
["-v"],
|
|
21
|
+
["-v", "--json"],
|
|
22
|
+
]) {
|
|
23
|
+
const result = spawnSync("pear", args, {
|
|
24
|
+
cwd: projectDirectory,
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
windowsHide: true,
|
|
27
|
+
});
|
|
28
|
+
if (result.error || result.status !== 0) continue;
|
|
29
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
30
|
+
if (parsePearMajor(output) !== null) return output;
|
|
31
|
+
}
|
|
32
|
+
return "";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const forwardedSignals =
|
|
36
|
+
process.platform === "win32"
|
|
37
|
+
? ["SIGINT", "SIGBREAK", "SIGTERM"]
|
|
38
|
+
: ["SIGHUP", "SIGINT", "SIGQUIT", "SIGTERM"];
|
|
39
|
+
|
|
40
|
+
const supervise = (child, { stop, forceStop = null }) => {
|
|
41
|
+
let exited = false;
|
|
42
|
+
let receivedSignal = null;
|
|
43
|
+
let forceTimer = null;
|
|
44
|
+
|
|
45
|
+
const signalHandlers = new Map();
|
|
46
|
+
const stopOnHostExit = () => {
|
|
47
|
+
if (!exited) stop("SIGTERM");
|
|
48
|
+
};
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
51
|
+
for (const [signal, handler] of signalHandlers) {
|
|
52
|
+
process.removeListener(signal, handler);
|
|
53
|
+
}
|
|
54
|
+
process.removeListener("exit", stopOnHostExit);
|
|
55
|
+
};
|
|
56
|
+
const requestStop = (signal) => {
|
|
57
|
+
if (receivedSignal !== null) {
|
|
58
|
+
(forceStop ?? stop)(signal);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
receivedSignal = signal;
|
|
62
|
+
stop(signal);
|
|
63
|
+
if (!forceStop) return;
|
|
64
|
+
forceTimer = setTimeout(() => {
|
|
65
|
+
if (!exited) forceStop();
|
|
66
|
+
}, 5000);
|
|
67
|
+
forceTimer.unref?.();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const signal of forwardedSignals) {
|
|
71
|
+
const handler = () => requestStop(signal);
|
|
72
|
+
signalHandlers.set(signal, handler);
|
|
73
|
+
process.on(signal, handler);
|
|
74
|
+
}
|
|
75
|
+
process.once("exit", stopOnHostExit);
|
|
76
|
+
|
|
77
|
+
child.once("error", (error) => {
|
|
78
|
+
exited = true;
|
|
79
|
+
cleanup();
|
|
80
|
+
console.error("Pear runner failed:", error?.message ?? error);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
});
|
|
83
|
+
child.once("exit", (code, signal) => {
|
|
84
|
+
exited = true;
|
|
85
|
+
cleanup();
|
|
86
|
+
if (receivedSignal === null && (signal || (code !== null && code !== 0))) {
|
|
87
|
+
const reason = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
88
|
+
console.error(`Pear runner exited unexpectedly (${reason}).`);
|
|
89
|
+
}
|
|
90
|
+
process.exitCode = receivedSignal
|
|
91
|
+
? exitCodeFor(null, receivedSignal)
|
|
92
|
+
: exitCodeFor(code, signal);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const runLegacy = () => {
|
|
97
|
+
const child = spawn(
|
|
98
|
+
"pear",
|
|
99
|
+
["run", "-d", "scripts/run-peer.mjs", ...appArgs],
|
|
100
|
+
{
|
|
101
|
+
cwd: projectDirectory,
|
|
102
|
+
stdio: "inherit",
|
|
103
|
+
windowsHide: true,
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
supervise(child, {
|
|
108
|
+
stop: (signal) => {
|
|
109
|
+
try {
|
|
110
|
+
child.kill(signal);
|
|
111
|
+
} catch (_e) {}
|
|
112
|
+
},
|
|
113
|
+
forceStop: () => {
|
|
114
|
+
try {
|
|
115
|
+
child.kill("SIGKILL");
|
|
116
|
+
} catch (_e) {}
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const runModule = async () => {
|
|
122
|
+
const imported = await import("pear-runtime");
|
|
123
|
+
const PearRuntime = imported.default ?? imported;
|
|
124
|
+
const worker = PearRuntime.run(entrypoint, appArgs);
|
|
125
|
+
|
|
126
|
+
const onWorkerStdinError = (error) => {
|
|
127
|
+
if (error?.code === "EPIPE" || error?.code === "ERR_STREAM_DESTROYED") return;
|
|
128
|
+
console.error("Pear worker stdin failed:", error?.message ?? error);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (worker.stdin) worker.stdin.on("error", onWorkerStdinError);
|
|
132
|
+
if (worker.stdin) process.stdin.pipe(worker.stdin);
|
|
133
|
+
if (worker.stdout) worker.stdout.pipe(process.stdout, { end: false });
|
|
134
|
+
if (worker.stderr) worker.stderr.pipe(process.stderr, { end: false });
|
|
135
|
+
|
|
136
|
+
const unpipe = () => {
|
|
137
|
+
if (worker.stdin) process.stdin.unpipe(worker.stdin);
|
|
138
|
+
if (worker.stdin) worker.stdin.removeListener("error", onWorkerStdinError);
|
|
139
|
+
if (worker.stdout) worker.stdout.unpipe(process.stdout);
|
|
140
|
+
if (worker.stderr) worker.stderr.unpipe(process.stderr);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
worker.once("close", unpipe);
|
|
144
|
+
supervise(worker, {
|
|
145
|
+
stop: () => {
|
|
146
|
+
if (!worker.destroyed) worker.destroy();
|
|
147
|
+
},
|
|
148
|
+
forceStop: () => {
|
|
149
|
+
// Sidecar only exposes graceful destroy; use its child handle as a guarded fallback.
|
|
150
|
+
try {
|
|
151
|
+
if (typeof worker._process?.kill === "function") {
|
|
152
|
+
worker._process.kill("SIGKILL");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
} catch (_e) {}
|
|
156
|
+
if (!worker.destroyed) worker.destroy();
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const mode = selectPearRunnerMode(detectPear());
|
|
162
|
+
if (mode === "legacy") runLegacy();
|
|
163
|
+
else await runModule();
|
package/scripts/run-peer.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import b4a from "b4a";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs";
|
|
4
|
+
import process from "process";
|
|
4
5
|
import PeerWallet from "trac-wallet";
|
|
5
6
|
import { Peer, Wallet, createConfig as createPeerConfig, ENV as PEER_ENV } from "../src/index.js";
|
|
6
7
|
import { MainSettlementBus } from "trac-msb/src/index.js";
|
|
@@ -12,21 +13,20 @@ import { ensureTextCodecs } from "../src/textCodec.js";
|
|
|
12
13
|
import TuxemonProtocol from "../dev/tuxemonProtocol.js";
|
|
13
14
|
import TuxemonContract from "../dev/tuxemonContract.js";
|
|
14
15
|
|
|
15
|
-
let process = globalThis.process;
|
|
16
|
-
if (globalThis.Pear !== undefined) {
|
|
17
|
-
const { default: bareProcess } = await import("bare-process");
|
|
18
|
-
process = bareProcess;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
const pearApp = typeof Pear !== "undefined" ? (Pear.app ?? Pear.config) : undefined;
|
|
22
17
|
const runtimeArgs = typeof process !== "undefined" ? process.argv.slice(2) : [];
|
|
23
18
|
const argv = pearApp?.args ?? runtimeArgs;
|
|
24
19
|
const positionalStoreName = argv.find((a) => a !== undefined && !String(a).startsWith("--")) ?? null;
|
|
25
20
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
21
|
+
const resolveEnvironment = (network) => {
|
|
22
|
+
if (network === PEER_ENV.DEVELOPMENT) {
|
|
23
|
+
return { peer: PEER_ENV.DEVELOPMENT, msb: MSB_ENV.DEVELOPMENT };
|
|
24
|
+
}
|
|
25
|
+
if (network === PEER_ENV.TESTNET1 || network === "testnet") {
|
|
26
|
+
return { peer: PEER_ENV.TESTNET1, msb: MSB_ENV.TESTNET1 };
|
|
27
|
+
}
|
|
28
|
+
return { peer: PEER_ENV.MAINNET, msb: MSB_ENV.MAINNET };
|
|
29
|
+
};
|
|
30
30
|
|
|
31
31
|
const toArgMap = (argv) => {
|
|
32
32
|
const out = {};
|
|
@@ -54,17 +54,26 @@ const toArgMap = (argv) => {
|
|
|
54
54
|
|
|
55
55
|
const ensureTrailingSlash = (value) => (value.endsWith("/") ? value : `${value}/`);
|
|
56
56
|
|
|
57
|
-
const
|
|
58
|
-
if (fs.existsSync(keyPairPath)) return;
|
|
57
|
+
const loadOrCreateWallet = async (keyPairPath, walletOptions) => {
|
|
59
58
|
fs.mkdirSync(path.dirname(keyPairPath), { recursive: true });
|
|
60
59
|
await ensureTextCodecs();
|
|
61
|
-
const wallet = new PeerWallet();
|
|
60
|
+
const wallet = new PeerWallet(walletOptions);
|
|
62
61
|
await wallet.ready;
|
|
62
|
+
if (fs.existsSync(keyPairPath)) {
|
|
63
|
+
wallet.importFromFile(keyPairPath, b4a.alloc(0));
|
|
64
|
+
return wallet;
|
|
65
|
+
}
|
|
63
66
|
if (!wallet.secretKey) {
|
|
64
|
-
await wallet.generateKeyPair();
|
|
67
|
+
await wallet.generateKeyPair(null, walletOptions?.derivationPath ?? null);
|
|
65
68
|
await wallet.ready;
|
|
66
69
|
}
|
|
67
70
|
wallet.exportToFile(keyPairPath, b4a.alloc(0));
|
|
71
|
+
return wallet;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const ensureKeypairFile = async (keyPairPath, walletOptions) => {
|
|
75
|
+
if (fs.existsSync(keyPairPath)) return;
|
|
76
|
+
await loadOrCreateWallet(keyPairPath, walletOptions);
|
|
68
77
|
};
|
|
69
78
|
|
|
70
79
|
const readHexFile = (filePath, byteLength) => {
|
|
@@ -78,6 +87,11 @@ const readHexFile = (filePath, byteLength) => {
|
|
|
78
87
|
};
|
|
79
88
|
|
|
80
89
|
const args = toArgMap(argv);
|
|
90
|
+
const selectedEnvironment = resolveEnvironment(
|
|
91
|
+
(args["network"] && String(args["network"]).trim().toLowerCase()) ||
|
|
92
|
+
(process.env.NETWORK && String(process.env.NETWORK).trim().toLowerCase()) ||
|
|
93
|
+
PEER_ENV.MAINNET
|
|
94
|
+
);
|
|
81
95
|
|
|
82
96
|
const rpcEnabled =
|
|
83
97
|
args["rpc"] === true || args["rpc"] === "true" || process.env.PEER_RPC === "true" || process.env.PEER_RPC === "1";
|
|
@@ -172,18 +186,6 @@ if (!msbBootstrapHex || !msbChannel) {
|
|
|
172
186
|
// trac-peer currently requires an MSB instance to broadcast and to observe confirmed state.
|
|
173
187
|
|
|
174
188
|
const effectiveMsbStoreName = msbStoreName ?? `${peerStoreNameRaw}-msb`;
|
|
175
|
-
const msbStoresFullPath = path.join(ensureTrailingSlash(msbStoresDirectory), effectiveMsbStoreName);
|
|
176
|
-
const msbKeyPairPath = path.join(msbStoresFullPath, "db", "keypair.json");
|
|
177
|
-
await ensureKeypairFile(msbKeyPairPath);
|
|
178
|
-
|
|
179
|
-
const peerKeyPairPath = path.join(
|
|
180
|
-
peerStoresDirectory,
|
|
181
|
-
peerStoreNameRaw,
|
|
182
|
-
"db",
|
|
183
|
-
"keypair.json"
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
await ensureKeypairFile(peerKeyPairPath);
|
|
187
189
|
|
|
188
190
|
const subnetBootstrapFile = path.join(
|
|
189
191
|
peerStoresDirectory,
|
|
@@ -208,11 +210,20 @@ if (subnetBootstrap) {
|
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
+
const msbConfig = createMsbConfig(selectedEnvironment.msb, {
|
|
214
|
+
bootstrap: msbBootstrap,
|
|
215
|
+
channel: msbChannel,
|
|
216
|
+
storeName: effectiveMsbStoreName,
|
|
217
|
+
storesDirectory: msbStoresDirectory,
|
|
218
|
+
});
|
|
219
|
+
const walletOptions = {
|
|
220
|
+
networkPrefix: msbConfig.addressPrefix,
|
|
221
|
+
derivationPath: msbConfig.derivationPath
|
|
222
|
+
};
|
|
223
|
+
const msbWallet = await loadOrCreateWallet(msbConfig.keyPairPath, walletOptions);
|
|
224
|
+
const msb = new MainSettlementBus(msbConfig, msbWallet);
|
|
213
225
|
|
|
214
|
-
|
|
215
|
-
const peerConfig = createPeerConfig(PEER_ENV.MAINNET, {
|
|
226
|
+
const peerConfig = createPeerConfig(selectedEnvironment.peer, {
|
|
216
227
|
storesDirectory: ensureTrailingSlash(peerStoresDirectory),
|
|
217
228
|
storeName: peerStoreNameRaw,
|
|
218
229
|
bootstrap: subnetBootstrap ? b4a.from(subnetBootstrap, "hex") : null,
|
|
@@ -221,10 +232,15 @@ const peerConfig = createPeerConfig(PEER_ENV.MAINNET, {
|
|
|
221
232
|
apiTxExposed: apiTxExposedEffective,
|
|
222
233
|
});
|
|
223
234
|
|
|
235
|
+
await ensureKeypairFile(peerConfig.keyPairPath, walletOptions);
|
|
236
|
+
await msb.ready();
|
|
237
|
+
|
|
238
|
+
// DevProtocol and DevContract moved to shared src files
|
|
239
|
+
|
|
224
240
|
const peer = new Peer({
|
|
225
241
|
config: peerConfig,
|
|
226
242
|
msb,
|
|
227
|
-
wallet: new Wallet(),
|
|
243
|
+
wallet: new Wallet(walletOptions),
|
|
228
244
|
protocol: TuxemonProtocol,
|
|
229
245
|
contract: TuxemonContract,
|
|
230
246
|
});
|
|
@@ -277,8 +293,16 @@ if (peer.config.enableInteractiveMode) {
|
|
|
277
293
|
console.log("Interactive CLI disabled.");
|
|
278
294
|
}
|
|
279
295
|
|
|
280
|
-
|
|
296
|
+
let shuttingDown = false;
|
|
297
|
+
|
|
298
|
+
const shutdown = async (signal) => {
|
|
299
|
+
if (shuttingDown) return;
|
|
300
|
+
shuttingDown = true;
|
|
301
|
+
|
|
281
302
|
if (rpcServer) await new Promise((resolve) => rpcServer.close(resolve));
|
|
282
303
|
await Promise.allSettled([peer.close(), msb.close()]);
|
|
283
|
-
process.exit(130);
|
|
284
|
-
}
|
|
304
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
308
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
package/src/config/env.js
CHANGED
|
@@ -3,9 +3,9 @@ import { Config } from "./config.js";
|
|
|
3
3
|
export const ENV = {
|
|
4
4
|
MAINNET: "mainnet",
|
|
5
5
|
DEVELOPMENT: "development",
|
|
6
|
+
TESTNET1: "testnet1",
|
|
6
7
|
};
|
|
7
8
|
// TODO: CREATE TEST ENV CONFIG SIMILAR TO MAINNET AND USE IT IN TESTS.
|
|
8
|
-
// TODO: CREATE TESTNET1 ENV CONFIG and update npm scripts to run node witn mainnet or testnet1.
|
|
9
9
|
|
|
10
10
|
const configData = {
|
|
11
11
|
[ENV.MAINNET]: {
|
|
@@ -32,6 +32,30 @@ const configData = {
|
|
|
32
32
|
apiMsgExposed: false,
|
|
33
33
|
bootstrap: null,
|
|
34
34
|
},
|
|
35
|
+
[ENV.TESTNET1]: {
|
|
36
|
+
channel: "1111trac1network1peer1testnet1111",
|
|
37
|
+
storesDirectory: "stores/",
|
|
38
|
+
storeName: "testnet",
|
|
39
|
+
txPoolMaxSize: 1_000,
|
|
40
|
+
maxTxDelay: 60,
|
|
41
|
+
maxMsbSignedLength: 1_000_000_000,
|
|
42
|
+
maxMsbApplyOperationBytes: 1024 * 1024,
|
|
43
|
+
enableInteractiveMode: true,
|
|
44
|
+
enableBackgroundTasks: true,
|
|
45
|
+
enableUpdater: true,
|
|
46
|
+
replicate: true,
|
|
47
|
+
dhtBootstrap: [
|
|
48
|
+
"116.202.214.149:10001",
|
|
49
|
+
"157.180.12.214:10001",
|
|
50
|
+
"node1.hyperdht.org:49737",
|
|
51
|
+
"node2.hyperdht.org:49737",
|
|
52
|
+
"node3.hyperdht.org:49737",
|
|
53
|
+
],
|
|
54
|
+
enableTxlogs: false,
|
|
55
|
+
apiTxExposed: false,
|
|
56
|
+
apiMsgExposed: false,
|
|
57
|
+
bootstrap: null,
|
|
58
|
+
},
|
|
35
59
|
[ENV.DEVELOPMENT]: {
|
|
36
60
|
channel: "unit-test",
|
|
37
61
|
storesDirectory: "stores/",
|
package/src/msbClient.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import b4a from 'b4a';
|
|
2
2
|
import PeerWallet from 'trac-wallet';
|
|
3
3
|
import ReadyResource from 'ready-resource';
|
|
4
|
-
import
|
|
4
|
+
import PartialTransactionValidator from 'trac-msb/src/core/network/protocols/shared/validators/PartialTransactionValidator.js';
|
|
5
5
|
import { normalizeTransactionOperation } from 'trac-msb/src/utils/normalizers.js';
|
|
6
6
|
|
|
7
7
|
export const MSB_OPERATION_TYPE = Object.freeze({
|
|
@@ -21,7 +21,7 @@ export class MsbClient extends ReadyResource {
|
|
|
21
21
|
|
|
22
22
|
async _open() {
|
|
23
23
|
await this.#msb.ready()
|
|
24
|
-
this.#partialTransactionValidator = new
|
|
24
|
+
this.#partialTransactionValidator = new PartialTransactionValidator(this.#msb.state, null, this.#msb.config)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
#orchestratorCompatiblePayload(payload) {
|
|
@@ -41,6 +41,10 @@ export class MsbClient extends ReadyResource {
|
|
|
41
41
|
return this.#msb.config.addressPrefix
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
get derivationPath() {
|
|
45
|
+
return this.#msb.config.derivationPath
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
get networkId() {
|
|
45
49
|
return this.#msb.config.networkId
|
|
46
50
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const parseSemverMajor = (value) => {
|
|
2
|
+
const match = /^v?(\d+)\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
3
|
+
String(value ?? "").trim()
|
|
4
|
+
);
|
|
5
|
+
return match ? Number(match[1]) : null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const parseJsonMajor = (value) => {
|
|
9
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
10
|
+
|
|
11
|
+
for (const key of ["semver", "SemVer", "version", "pear"]) {
|
|
12
|
+
const major = parseSemverMajor(value[key]);
|
|
13
|
+
if (major !== null) return major;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const parsePearMajor = (output) => {
|
|
20
|
+
const text = String(output ?? "").trim();
|
|
21
|
+
if (!text) return null;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const major = parseJsonMajor(JSON.parse(text));
|
|
25
|
+
if (major !== null) return major;
|
|
26
|
+
} catch (_e) {}
|
|
27
|
+
|
|
28
|
+
const jsonStart = text.indexOf("{");
|
|
29
|
+
const jsonEnd = text.lastIndexOf("}");
|
|
30
|
+
if (jsonStart !== -1 && jsonEnd > jsonStart) {
|
|
31
|
+
try {
|
|
32
|
+
const major = parseJsonMajor(JSON.parse(text.slice(jsonStart, jsonEnd + 1)));
|
|
33
|
+
if (major !== null) return major;
|
|
34
|
+
} catch (_e) {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const labelled = /(?:^|\s)SemVer\s*[:=]\s*v?(\d+)\.\d+\.\d+(?=$|\s)/im.exec(text);
|
|
38
|
+
if (labelled) return Number(labelled[1]);
|
|
39
|
+
|
|
40
|
+
const versions = [
|
|
41
|
+
...text.matchAll(/(?:^|[\s/])v?(\d+)\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?=$|\s)/g),
|
|
42
|
+
];
|
|
43
|
+
return versions.length > 0 ? Number(versions[versions.length - 1][1]) : null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const selectPearRunnerMode = (output) => {
|
|
47
|
+
const major = parsePearMajor(output);
|
|
48
|
+
return major !== null && major < 3 ? "legacy" : "module";
|
|
49
|
+
};
|
package/src/terminal/handlers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import b4a from "b4a";
|
|
2
|
+
import process from "process";
|
|
2
3
|
import { createMessage } from 'trac-msb/src/utils/buffer.js';
|
|
3
4
|
import { blake3 } from '@tracsystems/blake3';
|
|
4
5
|
import { MSB_OPERATION_TYPE } from '../msbClient.js';
|
|
@@ -486,7 +487,7 @@ class TerminalHandlers {
|
|
|
486
487
|
console.log('Exiting...');
|
|
487
488
|
if(rl) rl.close();
|
|
488
489
|
await this.#peer.close();
|
|
489
|
-
|
|
490
|
+
process.exit(0);
|
|
490
491
|
return { exit: true };
|
|
491
492
|
}
|
|
492
493
|
}
|
package/src/terminal/index.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/** @typedef {import('pear-interface')} */ /* global Pear */
|
|
2
|
+
import process from 'process';
|
|
2
3
|
import readline from 'readline';
|
|
3
|
-
import tty from 'tty';
|
|
4
4
|
import { TerminalHandlers } from './handlers.js';
|
|
5
5
|
|
|
6
|
+
const createTerminalReadline = ({
|
|
7
|
+
readlineModule = readline,
|
|
8
|
+
runtimeProcess = process,
|
|
9
|
+
} = {}) => readlineModule.createInterface({
|
|
10
|
+
input: runtimeProcess.stdin,
|
|
11
|
+
output: runtimeProcess.stdout,
|
|
12
|
+
});
|
|
13
|
+
|
|
6
14
|
class Terminal {
|
|
7
15
|
#peer
|
|
8
16
|
#handlers
|
|
@@ -51,21 +59,14 @@ class Terminal {
|
|
|
51
59
|
async start({ readlineInstance = null } = {}) {
|
|
52
60
|
const peer = this.#peer;
|
|
53
61
|
if (!peer) return;
|
|
54
|
-
if (
|
|
62
|
+
if (globalThis.Pear !== undefined && globalThis.Pear.config?.options?.type === 'desktop') return;
|
|
55
63
|
|
|
56
64
|
let rl = readlineInstance;
|
|
57
65
|
if (!rl) {
|
|
58
66
|
try {
|
|
59
|
-
rl =
|
|
60
|
-
input: new tty.ReadStream(0),
|
|
61
|
-
output: new tty.WriteStream(1),
|
|
62
|
-
});
|
|
67
|
+
rl = createTerminalReadline();
|
|
63
68
|
} catch (_e) {
|
|
64
|
-
|
|
65
|
-
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
66
|
-
} catch (_e2) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
+
return;
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -118,6 +119,4 @@ class Terminal {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
export { Terminal };
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
export { Terminal, createTerminalReadline };
|
package/src/wallet.js
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import test from "brittle";
|
|
2
|
+
|
|
3
|
+
import { parsePearMajor, selectPearRunnerMode } from "../../src/pearCompat.js";
|
|
4
|
+
|
|
5
|
+
test("pear compat: Pear v2 JSON selects the legacy runner", (t) => {
|
|
6
|
+
const output = JSON.stringify({
|
|
7
|
+
key: "example",
|
|
8
|
+
fork: 0,
|
|
9
|
+
length: 123,
|
|
10
|
+
semver: "2.6.5",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
t.is(parsePearMajor(output), 2);
|
|
14
|
+
t.is(selectPearRunnerMode(output), "legacy");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("pear compat: Pear v3 JSON selects the module runner", (t) => {
|
|
18
|
+
const output = JSON.stringify({
|
|
19
|
+
key: "example",
|
|
20
|
+
fork: 0,
|
|
21
|
+
length: 456,
|
|
22
|
+
semver: "3.0.0",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
t.is(parsePearMajor(output), 3);
|
|
26
|
+
t.is(selectPearRunnerMode(output), "module");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("pear compat: legacy SemVer output selects the legacy runner", (t) => {
|
|
30
|
+
const output = [
|
|
31
|
+
"v0.9609.example / v1.18.0",
|
|
32
|
+
"Key=example",
|
|
33
|
+
"Fork=0",
|
|
34
|
+
"Length=9609",
|
|
35
|
+
"SemVer=1.18.0",
|
|
36
|
+
].join("\n");
|
|
37
|
+
|
|
38
|
+
t.is(parsePearMajor(output), 1);
|
|
39
|
+
t.is(selectPearRunnerMode(output), "legacy");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("pear compat: unknown output safely selects the module runner", (t) => {
|
|
43
|
+
t.is(parsePearMajor("Pear development checkout"), null);
|
|
44
|
+
t.is(selectPearRunnerMode("Pear development checkout"), "module");
|
|
45
|
+
t.is(selectPearRunnerMode(""), "module");
|
|
46
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import test from "brittle";
|
|
2
|
+
|
|
3
|
+
import { createTerminalReadline } from "../../src/terminal/index.js";
|
|
4
|
+
|
|
5
|
+
test("terminal runtime: readline uses process streams", (t) => {
|
|
6
|
+
const runtimeProcess = { stdin: {}, stdout: {} };
|
|
7
|
+
const expected = {};
|
|
8
|
+
let options = null;
|
|
9
|
+
|
|
10
|
+
const readlineModule = {
|
|
11
|
+
createInterface(value) {
|
|
12
|
+
options = value;
|
|
13
|
+
return expected;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const actual = createTerminalReadline({
|
|
18
|
+
readlineModule,
|
|
19
|
+
runtimeProcess,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
t.is(actual, expected);
|
|
23
|
+
t.is(options.input, runtimeProcess.stdin);
|
|
24
|
+
t.is(options.output, runtimeProcess.stdout);
|
|
25
|
+
});
|
package/tests/unit/unit.test.js
CHANGED
|
@@ -8,4 +8,7 @@ await import('./cliTx.test.js');
|
|
|
8
8
|
await import('./operations.test.js');
|
|
9
9
|
await import('./simFunds.test.js');
|
|
10
10
|
await import('./msbTxValidation.test.js');
|
|
11
|
+
await import('./walletNetworkConfig.test.js');
|
|
12
|
+
await import('./pearCompat.test.js');
|
|
13
|
+
await import('./terminalRuntime.test.js');
|
|
11
14
|
test.resume();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import test from "brittle";
|
|
2
|
+
import b4a from "b4a";
|
|
3
|
+
import PeerWallet from "trac-wallet";
|
|
4
|
+
import { TRAC_NETWORK_MSB_TESTNET1_PREFIX } from "trac-wallet/constants.js";
|
|
5
|
+
import { TRAC_NETWORK_TESTNET_ID } from "trac-crypto-api/constants.js";
|
|
6
|
+
|
|
7
|
+
import Wallet from "../../src/wallet.js";
|
|
8
|
+
import { createConfig, ENV } from "../../src/index.js";
|
|
9
|
+
|
|
10
|
+
const MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
11
|
+
const TESTNET_DERIVATION_PATH = `m/${TRAC_NETWORK_TESTNET_ID}'/0'/0'/0'`;
|
|
12
|
+
|
|
13
|
+
test("wallet: constructor options are forwarded to trac-wallet", async (t) => {
|
|
14
|
+
const wallet = new Wallet({
|
|
15
|
+
networkPrefix: TRAC_NETWORK_MSB_TESTNET1_PREFIX,
|
|
16
|
+
derivationPath: TESTNET_DERIVATION_PATH,
|
|
17
|
+
mnemonic: MNEMONIC,
|
|
18
|
+
});
|
|
19
|
+
await wallet.ready;
|
|
20
|
+
|
|
21
|
+
const expected = new PeerWallet({
|
|
22
|
+
networkPrefix: TRAC_NETWORK_MSB_TESTNET1_PREFIX,
|
|
23
|
+
mnemonic: MNEMONIC,
|
|
24
|
+
derivationPath: TESTNET_DERIVATION_PATH,
|
|
25
|
+
});
|
|
26
|
+
await expected.ready;
|
|
27
|
+
|
|
28
|
+
t.is(wallet.address, expected.address);
|
|
29
|
+
t.is(wallet.publicKey, b4a.toString(expected.publicKey, "hex"));
|
|
30
|
+
t.is(wallet.secretKey, b4a.toString(expected.secretKey, "hex"));
|
|
31
|
+
t.is(wallet.derivationPath, TESTNET_DERIVATION_PATH);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("config: TESTNET1 preset is exported and can be instantiated", async (t) => {
|
|
35
|
+
t.is(ENV.TESTNET1, "testnet1");
|
|
36
|
+
|
|
37
|
+
const config = createConfig(ENV.TESTNET1, {});
|
|
38
|
+
|
|
39
|
+
t.is(config.storeName, "testnet");
|
|
40
|
+
t.ok(b4a.isBuffer(config.channel));
|
|
41
|
+
t.is(config.bootstrap, null);
|
|
42
|
+
});
|