trac-peer 0.4.4 → 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/APP_DEV.md CHANGED
@@ -118,7 +118,7 @@ Endpoints (all JSON, all under `/v1`):
118
118
  - `GET /v1/status`
119
119
  - `GET /v1/contract/schema`
120
120
  - `GET /v1/contract/nonce`
121
- - `POST /v1/contract/tx/prepare`
121
+ - `GET /v1/contract/tx/context` (returns MSB tx context)
122
122
  - `POST /v1/contract/tx`
123
123
  - `GET /v1/state?key=<urlencoded>&confirmed=true|false`
124
124
 
@@ -128,19 +128,20 @@ Important notes:
128
128
 
129
129
  ---
130
130
 
131
- ## 6) Wallet → peer → contract flow (end-to-end)
131
+ ## 6) Client → peer → contract flow (end-to-end)
132
132
 
133
- This is the “Ethereum-style” flow: wallet discovers a peer URL, fetches a schema, prepares a tx, signs locally, then submits it.
133
+ This is the “Ethereum-style” flow: a client (typically a dapp/backend) discovers a peer URL, fetches a schema, prepares a tx, requests a wallet signature, then submits it.
134
134
 
135
- ### Where the dapp fits
135
+ ### Where the dapp fits (dapp constructs, wallet signs)
136
136
 
137
- - A **dapp** (web/mobile UI) talks to a peer’s RPC URL to fetch `GET /v1/contract/schema` and to read state via `GET /v1/state`.
138
- - For writes, the dapp asks the wallet to:
139
- 1) request `nonce` + `prepare` from the peer,
140
- 2) sign the returned `tx` hash locally,
141
- 3) submit `sim: true` then `sim: false` to the peer.
137
+ - A **dapp** (web/mobile UI) can read: `GET /v1/contract/schema` and `GET /v1/state`.
138
+ - For **writes**, the dapp (or a backend the dapp calls) typically:
139
+ 1) fetches `nonce` + `tx/context` from the peer,
140
+ 2) constructs the tx hash (`tx`) locally,
141
+ 3) asks the wallet to **sign** the tx hash,
142
+ 4) submits `sim: true` then `sim: false` to the peer.
142
143
 
143
- In other words: the dapp never needs the private key; it just passes data between the peer RPC and the wallet signer.
144
+ In other words: the wallet only needs to sign; it does not need to talk to the peer RPC.
144
145
 
145
146
  ### Step A — Discover contract schema
146
147
 
@@ -148,42 +149,44 @@ In other words: the dapp never needs the private key; it just passes data betwee
148
149
  curl -s http://127.0.0.1:5001/v1/contract/schema | jq
149
150
  ```
150
151
 
151
- Wallet uses:
152
+ Client uses:
152
153
  - `contract.txTypes` (what tx types exist)
153
154
  - `contract.ops[type]` (input structure for each type, when available)
154
155
  - `api.methods` (optional read/query methods exposed by the protocol api)
155
156
 
156
- ### Step B — Get a nonce
157
+ ### Step B — Get a nonce (client)
157
158
 
158
159
  ```sh
159
160
  curl -s http://127.0.0.1:5001/v1/contract/nonce | jq
160
161
  ```
161
162
 
162
- ### Step C — Prepare a tx hash to sign
163
+ ### Step C — Get tx context + build tx hash (client)
163
164
 
164
- The wallet constructs a typed command (this is app-specific):
165
+ The client constructs a typed command (this is app-specific):
165
166
 
166
167
  ```json
167
168
  { "type": "catch", "value": {} }
168
169
  ```
169
170
 
170
- Then it asks the peer to compute the `tx` hash:
171
+ Then the client asks the peer for the MSB tx context (no computation):
171
172
 
172
173
  ```sh
173
- curl -s -X POST http://127.0.0.1:5001/v1/contract/tx/prepare \
174
- -H 'Content-Type: application/json' \
175
- -d '{
176
- "prepared_command": { "type": "catch", "value": {} },
177
- "address": "<wallet-pubkey-hex32>",
178
- "nonce": "<nonce-hex32>"
179
- }' | jq
174
+ curl -s http://127.0.0.1:5001/v1/contract/tx/context | jq
180
175
  ```
181
176
 
182
- The response contains:
183
- - `tx` (hex32): the exact 32-byte tx hash that must be signed
184
- - `command_hash` (hex32): hash of the prepared command (used by MSB payload)
177
+ The response contains an `msb` object with the fields the client needs to build the tx preimage:
178
+ - `networkId`
179
+ - `txv`
180
+ - `iw` (peer writer key)
181
+ - `bs` (subnet bootstrap)
182
+ - `mbs` (MSB bootstrap)
183
+ - `operationType` (currently `12`)
184
+
185
+ From there, the client computes locally:
186
+ - `command_hash = blake3(JSON.stringify(prepared_command))` (hex32)
187
+ - `tx = blake3(createMessage(networkId, txv, iw, command_hash, bs, mbs, nonce, operationType))` (hex32)
185
188
 
186
- ### Step D — Sign locally in the wallet
189
+ ### Step D — Sign locally with the wallet
187
190
 
188
191
  Wallet signs the **bytes** of `tx` (32 bytes) with its private key to produce:
189
192
  - `signature` (hex64)
@@ -222,10 +225,17 @@ curl -s -X POST http://127.0.0.1:5001/v1/contract/tx \
222
225
 
223
226
  ### Step G — Read app state
224
227
 
225
- Apps typically write under `app/...`. Read via:
228
+ Apps typically write under `app/...` (app-defined). Read via:
226
229
 
227
230
  ```sh
228
- curl -s 'http://127.0.0.1:5001/v1/state?key=app%tuxedex%2F<wallet-pubkey-hex32>&confirmed=false' | jq
231
+ curl -s 'http://127.0.0.1:5001/v1/state?key=<urlencoded-hyperbee-key>&confirmed=false' | jq
232
+ ```
233
+
234
+ Example (Tuxemon demo app):
235
+
236
+ ```sh
237
+ curl -s 'http://127.0.0.1:5001/v1/state?key=app%2Ftuxedex%2F<wallet-pubkey-hex32>&confirmed=false' | jq
238
+ ```
229
239
  ```
230
240
 
231
241
  The `confirmed` flag controls whether you read from:
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> \
@@ -418,9 +424,9 @@ npm run peer:pear-rpc -- \
418
424
  - `GET /v1/contract/schema`
419
425
  - Read state:
420
426
  - `GET /v1/state?key=app%2Fkv%2Ffoo&confirmed=true`
421
- - Wallet/dApp tx flow:
427
+ - Client tx flow (dapp constructs, wallet signs):
422
428
  - `GET /v1/contract/nonce`
423
- - `POST /v1/contract/tx/prepare` body: `{ "prepared_command": { "type": "...", "value": {} }, "address": "<pubkey-hex32>", "nonce": "<hex32>" }`
429
+ - `GET /v1/contract/tx/context` (returns MSB tx context for client-side tx derivation)
424
430
  - `POST /v1/contract/tx` body: `{ "tx": "<hex32>", "prepared_command": { ... }, "address": "<pubkey-hex32>", "signature": "<hex64>", "nonce": "<hex32>", "sim": true|false }`
425
431
 
426
432
  Notes:
@@ -446,7 +452,7 @@ All nodes in the subnet must run the same Protocol/Contract logic for determinis
446
452
 
447
453
  ## How `/tx` works (the lifecycle)
448
454
 
449
- When you run `/tx --command "..."` in the CLI (or a wallet uses the RPC tx flow), the flow is:
455
+ When you run `/tx --command "..."` in the CLI (or a client uses the RPC tx flow), the flow is:
450
456
 
451
457
  1) The command string is mapped into an operation object: `{ type, value }`.
452
458
  2) trac-peer hashes and signs the operation and broadcasts a settlement tx to MSB.
@@ -458,9 +464,11 @@ Where does step (1) happen?
458
464
  - In the demo runner (`scripts/run-peer.mjs`) it’s in the protocol class’s `mapTxCommand(...)` (example: `src/dev/tuxemonProtocol.js`).
459
465
  - The base protocol method is `Protocol.mapTxCommand(...)` in `src/protocol.js`. For your own app you override that function.
460
466
 
461
- dApp tx flow specifics:
462
- - The dApp sends a typed command (`prepared_command`) and asks the peer to compute `tx` via `POST /v1/contract/tx/prepare`.
463
- - The wallet signs `tx` and submits it to `POST /v1/contract/tx` with `sim: true` to simulate (recommended), then `sim: false` to broadcast.
467
+ Client tx flow specifics:
468
+ - The client fetches MSB tx context from `GET /v1/contract/tx/context`.
469
+ - The client computes `command_hash = blake3(JSON.stringify(prepared_command))`, then computes `tx` from the MSB preimage fields + `nonce`.
470
+ - The wallet signs `tx`.
471
+ - The client submits the signed payload to `POST /v1/contract/tx` with `sim: true` to simulate (recommended), then `sim: false` to broadcast.
464
472
 
465
473
  ---
466
474
 
package/PEER_RPC.md ADDED
@@ -0,0 +1,189 @@
1
+ # trac-peer RPC (HTTP) — API Reference
2
+
3
+ This is a **reference** for the public HTTP RPC exposed by `trac-peer`.
4
+
5
+ Base URL example:
6
+ - `http://127.0.0.1:5001`
7
+
8
+ All endpoints below are under the `/v1` prefix.
9
+
10
+ ## Conventions
11
+
12
+ - All responses are JSON.
13
+ - Request bodies (where applicable) are JSON.
14
+ - Hex formats:
15
+ - `hex32`: 32-byte hex string (64 hex chars)
16
+ - `hex64`: 64-byte hex string (128 hex chars)
17
+
18
+ ## Errors
19
+
20
+ Error responses have the shape:
21
+
22
+ ```json
23
+ { "error": "message" }
24
+ ```
25
+
26
+ Common status codes:
27
+ - `200` success
28
+ - `400` bad request (missing/invalid parameters)
29
+ - `404` not found (unknown route)
30
+ - `413` request body too large
31
+ - `500` internal error
32
+
33
+ ---
34
+
35
+ ## `GET /v1/health`
36
+
37
+ Health check.
38
+
39
+ ### Response `200`
40
+
41
+ ```json
42
+ { "ok": true }
43
+ ```
44
+
45
+ ---
46
+
47
+ ## `GET /v1/status`
48
+
49
+ Returns a status summary for the running peer and its MSB client view.
50
+
51
+ ### Query parameters
52
+ None
53
+
54
+ ### Response `200`
55
+
56
+ Object with:
57
+ - `peer`: identifiers + subnet view info (writability, signed length, bootstrap, etc.)
58
+ - `msb`: MSB bootstrap/networkId/signedLength as seen by this peer’s MSB client
59
+
60
+ ---
61
+
62
+ ## `GET /v1/contract/schema`
63
+
64
+ Returns an ABI-like schema describing:
65
+ - which contract tx types exist (`contract.txTypes`)
66
+ - optional per-tx input structure (`contract.ops`)
67
+ - the Protocol API method schema (`api.methods`)
68
+
69
+ ### Query parameters
70
+ None
71
+
72
+ ### Response `200`
73
+
74
+ ```json
75
+ {
76
+ "schemaVersion": 1,
77
+ "schemaFormat": "json-schema",
78
+ "contract": {
79
+ "contractClass": "TuxemonContract",
80
+ "protocolClass": "TuxemonProtocol",
81
+ "txTypes": ["catch"],
82
+ "ops": {
83
+ "catch": { "value": {} }
84
+ }
85
+ },
86
+ "api": { "methods": {} }
87
+ }
88
+ ```
89
+
90
+ ---
91
+
92
+ ## `GET /v1/contract/nonce`
93
+
94
+ Generates a nonce for signing.
95
+
96
+ ### Query parameters
97
+ None
98
+
99
+ ### Response `200`
100
+
101
+ ```json
102
+ { "nonce": "<hex32>" }
103
+ ```
104
+
105
+ ---
106
+
107
+ ## `GET /v1/contract/tx/context`
108
+
109
+ Returns the MSB transaction context needed by a client/dapp to compute the `tx` hash locally.
110
+
111
+ ### Query parameters
112
+ None
113
+
114
+ ### Response `200`
115
+
116
+ ```json
117
+ {
118
+ "msb": {
119
+ "networkId": 918,
120
+ "txv": "<hex32>",
121
+ "iw": "<hex32>",
122
+ "bs": "<hex32>",
123
+ "mbs": "<hex32>",
124
+ "operationType": 12
125
+ }
126
+ }
127
+ ```
128
+
129
+ ---
130
+
131
+ ## `POST /v1/contract/tx`
132
+
133
+ Simulates or broadcasts a signed contract transaction.
134
+
135
+ ### Request body (JSON)
136
+
137
+ Required fields:
138
+ - `tx` (`hex32`): transaction hash computed by the client/dapp
139
+ - `prepared_command` (`object`): `{ "type": "<string>", "value": <any> }`
140
+ - `address` (`hex32`): wallet public key (hex) used for signature verification
141
+ - `signature` (`hex64`): ed25519 signature over `tx` bytes
142
+ - `nonce` (`hex32`)
143
+
144
+ Optional:
145
+ - `sim` (`boolean`, default `false`): when `true`, run MSB preflight + contract simulation; when `false`, broadcast
146
+
147
+ Example:
148
+
149
+ ```json
150
+ {
151
+ "tx": "<hex32>",
152
+ "prepared_command": { "type": "catch", "value": {} },
153
+ "address": "<hex32>",
154
+ "signature": "<hex64>",
155
+ "nonce": "<hex32>",
156
+ "sim": true
157
+ }
158
+ ```
159
+
160
+ ### Response `200`
161
+
162
+ ```json
163
+ { "result": {} }
164
+ ```
165
+
166
+ Result shape is protocol-dependent.
167
+
168
+ ---
169
+
170
+ ## `GET /v1/state`
171
+
172
+ Reads a single key from the subnet state (Hyperbee).
173
+
174
+ ### Query parameters
175
+
176
+ - `key` (required, string): the exact Hyperbee key to read
177
+ - `confirmed` (optional, boolean, default `true`):
178
+ - `true`: read from signed/confirmed view
179
+ - `false`: read from latest local view
180
+
181
+ ### Response `200`
182
+
183
+ ```json
184
+ {
185
+ "key": "app/tuxedex/<pubKeyHex>",
186
+ "confirmed": false,
187
+ "value": {}
188
+ }
189
+ ```
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.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": "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",
package/rpc/handlers.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  getState,
6
6
  getContractSchema,
7
7
  contractGenerateNonce,
8
- contractPrepareTx,
8
+ contractTxContext,
9
9
  contractTx,
10
10
  } from "./services.js";
11
11
 
@@ -36,14 +36,8 @@ export async function handleContractNonce({ respond, peer }) {
36
36
  respond(200, { nonce });
37
37
  }
38
38
 
39
- export async function handleContractPrepareTx({ req, respond, peer, maxBodyBytes }) {
40
- const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
41
- if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
42
- const payload = await contractPrepareTx(peer, {
43
- prepared_command: body.prepared_command,
44
- address: body.address,
45
- nonce: body.nonce,
46
- });
39
+ export async function handleContractTxContext({ req, respond, peer, maxBodyBytes }) {
40
+ const payload = await contractTxContext(peer);
47
41
  respond(200, payload);
48
42
  }
49
43
 
package/rpc/routes/v1.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  handleGetState,
5
5
  handleGetContractSchema,
6
6
  handleContractNonce,
7
- handleContractPrepareTx,
7
+ handleContractTxContext,
8
8
  handleContractTx,
9
9
  } from "../handlers.js";
10
10
 
@@ -13,8 +13,7 @@ export const v1Routes = [
13
13
  { method: "GET", path: "/status", handler: handleStatus },
14
14
  { method: "GET", path: "/state", handler: handleGetState },
15
15
  { method: "GET", path: "/contract/schema", handler: handleGetContractSchema },
16
- // Wallet→peer flow: server-side tx prepare + wallet signature + broadcast.
17
16
  { method: "GET", path: "/contract/nonce", handler: handleContractNonce },
18
- { method: "POST", path: "/contract/tx/prepare", handler: handleContractPrepareTx },
17
+ { method: "GET", path: "/contract/tx/context", handler: handleContractTxContext },
19
18
  { method: "POST", path: "/contract/tx", handler: handleContractTx },
20
19
  ];
package/rpc/services.js CHANGED
@@ -1,12 +1,5 @@
1
1
  import b4a from "b4a";
2
2
  import { fastestToJsonSchema } from "./utils/schemaToJson.js";
3
- import { createHash } from "../src/utils/types.js";
4
-
5
- const asHex32 = (value, field) => {
6
- const hex = String(value ?? "").trim().toLowerCase();
7
- if (!/^[0-9a-f]{64}$/.test(hex)) throw new Error(`Invalid ${field}. Expected 32-byte hex (64 chars).`);
8
- return hex;
9
- };
10
3
 
11
4
  const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
12
5
 
@@ -17,10 +10,10 @@ const requireApi = (peer) => {
17
10
  };
18
11
 
19
12
  export async function getStatus(peer) {
20
- const subnetBootstrapHex = b4a.isBuffer(peer.bootstrap)
21
- ? b4a.toString(peer.bootstrap, "hex")
22
- : peer.bootstrap != null
23
- ? String(peer.bootstrap)
13
+ const subnetBootstrapHex = b4a.isBuffer(peer.config.bootstrap)
14
+ ? b4a.toString(peer.config.bootstrap, "hex")
15
+ : peer.config.bootstrap != null
16
+ ? String(peer.config.bootstrap)
24
17
  : null;
25
18
 
26
19
  const peerMsbAddress = peer.msbClient.pubKeyHexToAddress(peer.wallet.publicKey);
@@ -120,22 +113,34 @@ export async function contractGenerateNonce(peer) {
120
113
  return api.generateNonce();
121
114
  }
122
115
 
123
- export async function contractPrepareTx(peer, { prepared_command, address, nonce } = {}) {
116
+ export async function contractTxContext(peer) {
124
117
  const api = requireApi(peer);
125
- if (!isObject(prepared_command)) throw new Error("prepared_command must be an object.");
126
- const addr = asHex32(address, "address");
127
- const n = asHex32(nonce, "nonce");
128
118
 
129
- if (peer?.protocol.instance?.safeJsonStringify == null) {
130
- throw new Error("safeJsonStringify is not available on protocol instance.");
131
- }
119
+ const networkId = peer.msbClient.networkId;
120
+ const mbs = peer.msbClient.bootstrapHex;
121
+ const txv = await peer.msbClient.getTxvHex();
122
+
123
+ const bs =
124
+ b4a.isBuffer(peer.config.bootstrap)
125
+ ? b4a.toString(peer.config.bootstrap, "hex")
126
+ : peer.config.bootstrap != null
127
+ ? String(peer.config.bootstrap)
128
+ : null;
132
129
 
133
- const json = peer.protocol.instance.safeJsonStringify(prepared_command);
134
- if (json == null) throw new Error("Failed to stringify prepared_command.");
130
+ const iw = peer.writerLocalKey ?? api.getPeerWriterKey?.();
131
+ if (!iw || !/^[0-9a-f]{64}$/i.test(String(iw))) throw new Error("Peer writer key is not available.");
132
+ if (!bs || !/^[0-9a-f]{64}$/i.test(String(bs))) throw new Error("Peer subnet bootstrap is not available.");
135
133
 
136
- const command_hash = await createHash(json);
137
- const tx = await api.generateTx(addr, command_hash, n);
138
- return { tx, command_hash };
134
+ return {
135
+ msb: {
136
+ networkId,
137
+ txv,
138
+ iw: String(iw).toLowerCase(),
139
+ bs: String(bs).toLowerCase(),
140
+ mbs: String(mbs).toLowerCase(),
141
+ operationType: 12,
142
+ },
143
+ };
139
144
  }
140
145
 
141
146
  export async function contractTx(peer, { tx, prepared_command, address, signature, nonce, sim = false } = {}) {
@@ -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;
@@ -12,6 +12,7 @@ import { Peer, Protocol, Contract, createConfig, ENV } from "../../src/index.js"
12
12
  import TuxemonContract from "../../dev/tuxemonContract.js";
13
13
  import TuxemonProtocol from "../../dev/tuxemonProtocol.js";
14
14
  import Wallet from "../../src/wallet.js";
15
+ import { createHash, jsonStringify } from "../../src/utils/types.js";
15
16
  import { mkdtempPortable, rmrfPortable } from "../helpers/tmpdir.js";
16
17
 
17
18
  async function withTempDir(fn) {
@@ -205,10 +206,13 @@ test("rpc: body size limit returns 413", async (t) => {
205
206
  const baseUrl = rpc.baseUrl;
206
207
 
207
208
  const big = "x".repeat(100);
208
- const r = await httpJson("POST", `${baseUrl}/v1/contract/tx/prepare`, {
209
+ const r = await httpJson("POST", `${baseUrl}/v1/contract/tx`, {
210
+ tx: "0".repeat(64),
209
211
  prepared_command: { type: big, value: {} },
210
212
  address: wallet.publicKey,
213
+ signature: "0".repeat(128),
211
214
  nonce: "0".repeat(64),
215
+ sim: true,
212
216
  });
213
217
  t.is(r.status, 413);
214
218
  } finally {
@@ -281,13 +285,25 @@ test("rpc: wallet-signed tx simulate via prepare+sign+broadcast", async (t) => {
281
285
  const nonce = nonceRes.json?.nonce;
282
286
 
283
287
  const prepared_command = { type: "catch", value: {} };
284
- const prep = await httpJson("POST", `${baseUrl}/v1/contract/tx/prepare`, {
285
- prepared_command,
286
- address: externalWallet.publicKey,
287
- nonce,
288
- });
289
- t.is(prep.status, 200);
290
- const tx = prep.json?.tx;
288
+ const ctx = await httpJson("GET", `${baseUrl}/v1/contract/tx/context`);
289
+ t.is(ctx.status, 200);
290
+ t.is(ctx.json?.msb?.operationType, 12);
291
+ t.is(typeof ctx.json?.msb?.networkId, "number");
292
+ t.is(typeof ctx.json?.msb?.txv, "string");
293
+ t.is(typeof ctx.json?.msb?.iw, "string");
294
+ t.is(typeof ctx.json?.msb?.bs, "string");
295
+ t.is(typeof ctx.json?.msb?.mbs, "string");
296
+
297
+ const command_hash = await createHash(jsonStringify(prepared_command));
298
+ const tx = await peer.protocol.instance.generateTx(
299
+ ctx.json.msb.networkId,
300
+ ctx.json.msb.txv,
301
+ ctx.json.msb.iw,
302
+ command_hash,
303
+ ctx.json.msb.bs,
304
+ ctx.json.msb.mbs,
305
+ nonce
306
+ );
291
307
 
292
308
  const signature = externalWallet.sign(b4a.from(tx, "hex"));
293
309
  const simRes = await httpJson("POST", `${baseUrl}/v1/contract/tx`, {
@@ -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
+ });