trac-peer 0.4.4 → 0.4.5

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
@@ -418,9 +418,9 @@ npm run peer:pear-rpc -- \
418
418
  - `GET /v1/contract/schema`
419
419
  - Read state:
420
420
  - `GET /v1/state?key=app%2Fkv%2Ffoo&confirmed=true`
421
- - Wallet/dApp tx flow:
421
+ - Client tx flow (dapp constructs, wallet signs):
422
422
  - `GET /v1/contract/nonce`
423
- - `POST /v1/contract/tx/prepare` body: `{ "prepared_command": { "type": "...", "value": {} }, "address": "<pubkey-hex32>", "nonce": "<hex32>" }`
423
+ - `GET /v1/contract/tx/context` (returns MSB tx context for client-side tx derivation)
424
424
  - `POST /v1/contract/tx` body: `{ "tx": "<hex32>", "prepared_command": { ... }, "address": "<pubkey-hex32>", "signature": "<hex64>", "nonce": "<hex32>", "sim": true|false }`
425
425
 
426
426
  Notes:
@@ -446,7 +446,7 @@ All nodes in the subnet must run the same Protocol/Contract logic for determinis
446
446
 
447
447
  ## How `/tx` works (the lifecycle)
448
448
 
449
- When you run `/tx --command "..."` in the CLI (or a wallet uses the RPC tx flow), the flow is:
449
+ When you run `/tx --command "..."` in the CLI (or a client uses the RPC tx flow), the flow is:
450
450
 
451
451
  1) The command string is mapped into an operation object: `{ type, value }`.
452
452
  2) trac-peer hashes and signs the operation and broadcasts a settlement tx to MSB.
@@ -458,9 +458,11 @@ Where does step (1) happen?
458
458
  - In the demo runner (`scripts/run-peer.mjs`) it’s in the protocol class’s `mapTxCommand(...)` (example: `src/dev/tuxemonProtocol.js`).
459
459
  - The base protocol method is `Protocol.mapTxCommand(...)` in `src/protocol.js`. For your own app you override that function.
460
460
 
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.
461
+ Client tx flow specifics:
462
+ - The client fetches MSB tx context from `GET /v1/contract/tx/context`.
463
+ - The client computes `command_hash = blake3(JSON.stringify(prepared_command))`, then computes `tx` from the MSB preimage fields + `nonce`.
464
+ - The wallet signs `tx`.
465
+ - The client submits the signed payload to `POST /v1/contract/tx` with `sim: true` to simulate (recommended), then `sim: false` to broadcast.
464
466
 
465
467
  ---
466
468
 
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/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.5",
5
5
  "type": "module",
6
6
  "pear": {
7
7
  "name": "trac-peer",
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 } = {}) {
@@ -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`, {