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 +38 -28
- package/DOCS.md +17 -9
- package/PEER_RPC.md +189 -0
- package/README.md +7 -1
- package/package.json +8 -10
- package/rpc/handlers.js +3 -9
- package/rpc/routes/v1.js +2 -3
- package/rpc/services.js +28 -23
- 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/acceptance/rpc.test.js +24 -8
- 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/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
|
-
- `
|
|
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)
|
|
131
|
+
## 6) Client → peer → contract flow (end-to-end)
|
|
132
132
|
|
|
133
|
-
This is the “Ethereum-style” flow:
|
|
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)
|
|
138
|
-
- For writes
|
|
139
|
-
1)
|
|
140
|
-
2)
|
|
141
|
-
3)
|
|
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
|
|
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
|
-
|
|
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 —
|
|
163
|
+
### Step C — Get tx context + build tx hash (client)
|
|
163
164
|
|
|
164
|
-
The
|
|
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
|
|
171
|
+
Then the client asks the peer for the MSB tx context (no computation):
|
|
171
172
|
|
|
172
173
|
```sh
|
|
173
|
-
curl -s
|
|
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
|
-
- `
|
|
184
|
-
- `
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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> \
|
|
@@ -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
|
-
-
|
|
427
|
+
- Client tx flow (dapp constructs, wallet signs):
|
|
422
428
|
- `GET /v1/contract/nonce`
|
|
423
|
-
- `
|
|
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
|
|
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
|
-
|
|
462
|
-
- The
|
|
463
|
-
- The
|
|
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`
|
|
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",
|
package/rpc/handlers.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getState,
|
|
6
6
|
getContractSchema,
|
|
7
7
|
contractGenerateNonce,
|
|
8
|
-
|
|
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
|
|
40
|
-
const
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
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
|
|
134
|
-
if (
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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();
|
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
|
@@ -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
|
|
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
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
t.is(
|
|
290
|
-
|
|
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
|
+
});
|
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
|
+
});
|