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 CHANGED
@@ -30,7 +30,8 @@ In practice:
30
30
  ## Requirements
31
31
 
32
32
  - Node.js + npm (recommended: a modern Node LTS).
33
- - `pear` is optional (you can run in pure Node). If you use Pear, add it to PATH as Pear suggests.
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 is similar, but uses Pear runtime (like MSB does).
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, you can also start Peer 2 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` using the Pear runtime (similar to `trac-msb`). You can control stores via flags (recommended); an optional first positional arg can be used as a “store label” fallback if `--peer-store-name` is omitted.
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.5",
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": "pear run -d scripts/run-peer.mjs",
26
- "peer:pear-rpc": "pear run -d scripts/run-peer.mjs --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.4.3",
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.3",
103
- "trac-msb": "^0.2.9",
104
- "trac-wallet": "^1.0.1",
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();
@@ -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 createMsb = (options) => {
27
- const config = createMsbConfig(MSB_ENV.MAINNET, options)
28
- return new MainSettlementBus(config);
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 ensureKeypairFile = async (keyPairPath) => {
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 msb = createMsb({ bootstrap: msbBootstrap, channel: msbChannel, storeName: effectiveMsbStoreName, storesDirectory: msbStoresDirectory})
212
- await msb.ready();
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
- // DevProtocol and DevContract moved to shared src files
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
- process.on("SIGINT", async () => {
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 PartialTransaction from 'trac-msb/src/core/network/protocols/shared/validators/PartialTransaction.js';
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 PartialTransaction(this.#msb.state, null, this.#msb.config)
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
+ };
@@ -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
- typeof process !== "undefined" ? process.exit(0) : Pear.exit(0);
490
+ process.exit(0);
490
491
  return { exit: true };
491
492
  }
492
493
  }
@@ -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 (global.Pear !== undefined && global.Pear.config?.options?.type === 'desktop') return;
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 = readline.createInterface({
60
- input: new tty.ReadStream(0),
61
- output: new tty.WriteStream(1),
62
- });
67
+ rl = createTerminalReadline();
63
68
  } catch (_e) {
64
- try {
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
@@ -2,10 +2,6 @@ import PeerWallet from "trac-wallet"
2
2
  import b4a from "b4a";
3
3
 
4
4
  class Wallet extends PeerWallet {
5
- constructor() {
6
- super();
7
- }
8
-
9
5
  get publicKey() {
10
6
  const pk = super.publicKey;
11
7
  if (!pk) return null;
@@ -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
+ });
@@ -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
+ });