trac-peer 0.2.12 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DOCS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # trac-peer — Setup & App Guide
2
2
 
3
- This document explains how to run `trac-peer`, connect it to an existing MSB network, create/join a subnet, and test the built‑in demo app (“contract”) (`ping` + `set`), plus chat and the HTTP RPC API.
3
+ This document explains how to run `trac-peer`, connect it to an existing MSB network, create/join a subnet, and test the built‑in demo app (“contract”) (`ping` + `set`), plus the HTTP RPC API used by wallets/apps.
4
4
 
5
5
  It’s written to be usable even if you’re not deeply familiar with P2P/blockchain systems.
6
6
 
@@ -74,7 +74,7 @@ npm run peer:run -- --msb-bootstrap=<hex32> --msb-channel=<channel>
74
74
  /deploy_subnet
75
75
  /add_admin --address <your-peer-publicKey-hex>
76
76
  /tx --command "ping hello"
77
- /get --key app/ping/<tx-hash>
77
+ /get --key app/ping/<tx-hash> --confirmed false
78
78
  ```
79
79
 
80
80
  ---
@@ -373,9 +373,11 @@ Messages are replicated like any other subnet op.
373
373
 
374
374
  ---
375
375
 
376
- ## HTTP RPC (operator API)
376
+ ## HTTP RPC (wallet/app API)
377
+
378
+ RPC is an HTTP server that runs alongside your peer and lets a wallet/app connect via URL (Ethereum-style).
377
379
 
378
- RPC is an HTTP server that runs alongside your peer and lets you control it with requests.
380
+ Important: operator/admin controls (deploy subnet, writer/indexer management, chat moderation) are **CLI-only** and are not exposed via RPC.
379
381
 
380
382
  ### Start with RPC enabled (Node)
381
383
 
@@ -383,6 +385,7 @@ RPC is an HTTP server that runs alongside your peer and lets you control it with
383
385
  npm run peer:run-rpc -- \
384
386
  --msb-bootstrap=<hex32> \
385
387
  --msb-channel=<channel> \
388
+ --api-tx-exposed \
386
389
  --rpc-host=127.0.0.1 \
387
390
  --rpc-port=5001
388
391
  ```
@@ -395,6 +398,7 @@ npm run peer:pear-rpc -- \
395
398
  --msb-channel=<channel> \
396
399
  --msb-store-name=peer-msb-rpc \
397
400
  --peer-store-name=peer-rpc \
401
+ --api-tx-exposed \
398
402
  --rpc-host=127.0.0.1 \
399
403
  --rpc-port=5001
400
404
  ```
@@ -408,25 +412,22 @@ npm run peer:pear-rpc -- \
408
412
 
409
413
  - Status:
410
414
  - `GET /v1/status`
415
+ - Contract schema (JSON Schema; contract tx types + Protocol API methods):
416
+ - `GET /v1/contract/schema`
411
417
  - Read state:
412
418
  - `GET /v1/state?key=app%2Fkv%2Ffoo&confirmed=true`
413
- - Enable chat (admin-only):
414
- - `POST /v1/chat/status` body: `{ "enabled": true }`
415
- - Post message:
416
- - `POST /v1/chat/post` body: `{ "message": "hello" }`
417
- - Broadcast tx:
418
- - `POST /v1/tx` body: `{ "command": "set foo bar", "sim": false }`
419
- - Add writer (admin-only):
420
- - `POST /v1/admin/add-writer` body: `{ "key": "<writerKeyHex>" }`
419
+ - Wallet tx flow:
420
+ - `GET /v1/contract/nonce`
421
+ - `POST /v1/contract/tx/prepare` body: `{ "prepared_command": { "type": "...", "value": {} }, "address": "<pubkey-hex32>", "nonce": "<hex32>" }`
422
+ - `POST /v1/contract/tx` body: `{ "tx": "<hex32>", "prepared_command": { ... }, "address": "<pubkey-hex32>", "signature": "<hex64>", "nonce": "<hex32>", "sim": true|false }`
421
423
 
422
424
  Notes:
423
- - These RPC endpoints currently operate the **local node** (operator style). They do not accept an external user signature format yet.
424
-
425
- ---
425
+ - To allow wallet tx submission, start the peer with `--rpc` and `--api-tx-exposed` (or env `PEER_API_TX_EXPOSED=1` + `PEER_RPC=1`).
426
+ - The peer must be subnet-writable (writer) to broadcast a tx.
426
427
 
427
428
  ## Building your own app (Protocol + Contract)
428
429
 
429
- The runner uses a demo `DevProtocol` and `DevContract` (see `peer-main.mjs` / `scripts/run-peer.mjs`) so you can test quickly.
430
+ The runner uses demo protocol/contract files under `src/dev/` (wired in `scripts/run-peer.mjs`) so you can test quickly.
430
431
 
431
432
  For a real app, you typically:
432
433
 
@@ -443,7 +444,7 @@ All nodes in the subnet must run the same Protocol/Contract logic for determinis
443
444
 
444
445
  ## How `/tx` works (the lifecycle)
445
446
 
446
- When you run `/tx --command "..."` (or call `POST /v1/tx`) the flow is:
447
+ When you run `/tx --command "..."` in the CLI (or a wallet uses the RPC tx flow), the flow is:
447
448
 
448
449
  1) The command string is mapped into an operation object: `{ type, value }`.
449
450
  2) trac-peer hashes and signs the operation and broadcasts a settlement tx to MSB.
@@ -452,9 +453,13 @@ When you run `/tx --command "..."` (or call `POST /v1/tx`) the flow is:
452
453
  5) Every subnet node applies the subnet op and runs contract logic locally, deriving the same results from the same ordered log.
453
454
 
454
455
  Where does step (1) happen?
455
- - In the demo runner (`scripts/run-peer.mjs` / `peer-main.mjs`) it’s in `DevProtocol.mapTxCommand(...)`.
456
+ - In the demo runner (`scripts/run-peer.mjs`) it’s in the protocol class’s `mapTxCommand(...)` (example: `src/dev/pokemonProtocol.js`).
456
457
  - The base protocol method is `Protocol.mapTxCommand(...)` in `src/protocol.js`. For your own app you override that function.
457
458
 
459
+ Wallet tx flow specifics:
460
+ - The wallet sends a typed command (`prepared_command`) and asks the peer to compute `tx` via `POST /v1/contract/tx/prepare`.
461
+ - The wallet signs `tx` and submits it to `POST /v1/contract/tx` with `sim: true` to simulate (recommended), then `sim: false` to broadcast.
462
+
458
463
  ---
459
464
 
460
465
  ## Reset / clean start
package/README.md CHANGED
@@ -68,7 +68,7 @@ npm run peer:run -- \
68
68
  The runner prints the Peer MSB address. Fund that address on MSB (so the node entry exists and fee checks pass), then in the peer console run:
69
69
  - `/deploy_subnet`
70
70
  - `/tx --command "ping hello"` (dev protocol)
71
- - If you want to use admin-only commands (writer/indexer management, chat moderation), run `/add_admin --address "<peer-publicKey-hex>"` and verify with `/get --key admin`.
71
+ - If you want to use admin-only commands (writer/indexer management, chat moderation), run `/add_admin --address "<peer-publicKey-hex>"` and verify with `/get --key admin --confirmed false`.
72
72
 
73
73
  Notes:
74
74
  - The subnet bootstrap key is auto-generated the first time and persisted to `stores/<peer-store>/subnet-bootstrap.hex`.
@@ -91,13 +91,16 @@ npm run peer:run -- \
91
91
 
92
92
  ## RPC API (HTTP)
93
93
 
94
- You can start an HTTP API alongside the interactive peer:
94
+ You can start an HTTP API alongside the interactive peer.
95
+
96
+ This RPC is intended for **wallet/app connectivity** (URL + JSON), not for operating the peer node. Operator/admin actions remain CLI-only.
95
97
 
96
98
  ```sh
97
99
  npm run peer:run -- \
98
100
  --msb-bootstrap <32-byte-hex> \
99
101
  --msb-channel <channel-string> \
100
102
  --rpc \
103
+ --api-tx-exposed \
101
104
  --rpc-host 127.0.0.1 \
102
105
  --rpc-port 5001
103
106
  ```
@@ -106,17 +109,11 @@ Endpoints (all JSON):
106
109
  - `GET /v1/health`
107
110
  - `GET /v1/status`
108
111
  - `GET /v1/state?key=<hyperbee-key>&confirmed=true|false`
109
- - `POST /v1/tx` body: `{ "command": "ping hello", "sim": false }`
110
- - `POST /v1/deploy-subnet`
111
- - `POST /v1/chat/status` body: `{ "enabled": true }`
112
- - `POST /v1/chat/post` body: `{ "message": "hello", "reply_to": 1 }`
113
- - `POST /v1/chat/nick` body: `{ "nick": "alice" }`
114
- - `POST /v1/admin/add-admin` body: `{ "address": "<pubkey-hex32>" }`
115
- - `POST /v1/admin/add-writer` body: `{ "key": "<writerKey-hex32>" }`
116
- - `POST /v1/admin/add-indexer` body: `{ "key": "<writerKey-hex32>" }`
117
- - `POST /v1/admin/remove-writer` body: `{ "key": "<writerKey-hex32>" }`
118
- - `POST /v1/msb/join-validator` body: `{ "address": "<msb-bech32m-address>" }`
112
+ - `GET /v1/contract/schema` (JSON Schema; contract tx types + Protocol API methods)
113
+ - `GET /v1/contract/nonce`
114
+ - `POST /v1/contract/tx/prepare` body: `{ "prepared_command": { "type": "...", "value": {} }, "address": "<pubkey-hex32>", "nonce": "<hex32>" }`
115
+ - `POST /v1/contract/tx` body: `{ "tx": "<hex32>", "prepared_command": { ... }, "address": "<pubkey-hex32>", "signature": "<hex64>", "nonce": "<hex32>", "sim": true|false }`
119
116
 
120
117
  Notes:
121
- - Write endpoints require the node to be subnet-writable (`/v1/status` shows `peer.baseWritable`).
118
+ - To allow wallet tx submission, start the peer with `--rpc` and `--api-tx-exposed` (or env `PEER_API_TX_EXPOSED=1` + `PEER_RPC=1`).
122
119
  - RPC request bodies are limited to `1_000_000` bytes by default (override with `--rpc-max-body-bytes`).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trac-peer",
3
3
  "main": "src/index.js",
4
- "version": "0.2.12",
4
+ "version": "0.3.1",
5
5
  "type": "module",
6
6
  "pear": {
7
7
  "name": "trac-peer",
@@ -55,7 +55,7 @@
55
55
  "bare-tls": "2.0.4",
56
56
  "bare-tty": "5.0.2",
57
57
  "bare-url": "2.1.5",
58
- "bare-utils": "1.2.0",
58
+ "bare-utils": "1.5.1",
59
59
  "bare-worker": "3.0.0",
60
60
  "bare-zlib": "1.2.5",
61
61
  "bip39": "3.1.0",
@@ -83,7 +83,7 @@
83
83
  "multicoin-address-validator": "0.5.25",
84
84
  "os": "npm:bare-node-os",
85
85
  "path": "npm:bare-node-path",
86
- "pear-interface": "1.0.0",
86
+ "pear-interface": "1.1.0",
87
87
  "process": "npm:bare-node-process",
88
88
  "protomux": "^3.10.1",
89
89
  "protomux-wakeup": "^2.4.0",
@@ -96,7 +96,7 @@
96
96
  "timers": "npm:bare-node-timers",
97
97
  "tls": "npm:bare-node-tls",
98
98
  "trac-crypto-api": "^0.1.3",
99
- "trac-msb": "^0.2.8",
99
+ "trac-msb": "^0.2.9",
100
100
  "trac-wallet": "^0.0.43-msb-r2.9",
101
101
  "tty": "npm:bare-node-tty",
102
102
  "url": "npm:bare-node-url",
package/rpc/handlers.js CHANGED
@@ -3,17 +3,10 @@ import { readJsonBody } from "./utils/body.js";
3
3
  import {
4
4
  getStatus,
5
5
  getState,
6
- broadcastTx,
7
- deploySubnet,
8
- setChatStatus,
9
- postChatMessage,
10
- setNick,
11
- addAdmin,
12
- addWriter,
13
- addIndexer,
14
- removeWriter,
15
- removeIndexer,
16
- joinValidator,
6
+ getContractSchema,
7
+ contractGenerateNonce,
8
+ contractPrepareTx,
9
+ contractTx,
17
10
  } from "./services.js";
18
11
 
19
12
  export async function handleHealth({ respond }) {
@@ -33,79 +26,37 @@ export async function handleGetState({ req, respond, peer }) {
33
26
  respond(200, { key, confirmed: confirmedBool, value });
34
27
  }
35
28
 
36
- export async function handleBroadcastTx({ req, respond, peer, maxBodyBytes }) {
37
- const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
38
- if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
39
- const payload = await broadcastTx(peer, { command: body.command, sim: body.sim });
40
- respond(200, { payload });
41
- }
42
-
43
- export async function handleDeploySubnet({ req, respond, peer, maxBodyBytes }) {
44
- // Optional body for future compatibility.
45
- await readJsonBody(req, { maxBytes: maxBodyBytes }).catch(() => null);
46
- const payload = await deploySubnet(peer);
47
- respond(200, { payload });
48
- }
49
-
50
- export async function handleSetChatStatus({ req, respond, peer, maxBodyBytes }) {
51
- const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
52
- if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
53
- await setChatStatus(peer, body.enabled);
54
- respond(200, { ok: true });
55
- }
56
-
57
- export async function handlePostChatMessage({ req, respond, peer, maxBodyBytes }) {
58
- const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
59
- if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
60
- await postChatMessage(peer, { message: body.message, reply_to: body.reply_to });
61
- respond(200, { ok: true });
29
+ export async function handleGetContractSchema({ respond, peer }) {
30
+ const schema = await getContractSchema(peer);
31
+ respond(200, schema);
62
32
  }
63
33
 
64
- export async function handleSetNick({ req, respond, peer, maxBodyBytes }) {
65
- const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
66
- if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
67
- await setNick(peer, { nick: body.nick, user: body.user });
68
- respond(200, { ok: true });
34
+ export async function handleContractNonce({ respond, peer }) {
35
+ const nonce = await contractGenerateNonce(peer);
36
+ respond(200, { nonce });
69
37
  }
70
38
 
71
- export async function handleAddAdmin({ req, respond, peer, maxBodyBytes }) {
39
+ export async function handleContractPrepareTx({ req, respond, peer, maxBodyBytes }) {
72
40
  const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
73
41
  if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
74
- await addAdmin(peer, { address: body.address });
75
- respond(200, { ok: true });
42
+ const payload = await contractPrepareTx(peer, {
43
+ prepared_command: body.prepared_command,
44
+ address: body.address,
45
+ nonce: body.nonce,
46
+ });
47
+ respond(200, payload);
76
48
  }
77
49
 
78
- export async function handleAddWriter({ req, respond, peer, maxBodyBytes }) {
50
+ export async function handleContractTx({ req, respond, peer, maxBodyBytes }) {
79
51
  const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
80
52
  if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
81
- await addWriter(peer, { key: body.key });
82
- respond(200, { ok: true });
83
- }
84
-
85
- export async function handleAddIndexer({ req, respond, peer, maxBodyBytes }) {
86
- const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
87
- if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
88
- await addIndexer(peer, { key: body.key });
89
- respond(200, { ok: true });
90
- }
91
-
92
- export async function handleRemoveWriter({ req, respond, peer, maxBodyBytes }) {
93
- const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
94
- if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
95
- await removeWriter(peer, { key: body.key });
96
- respond(200, { ok: true });
97
- }
98
-
99
- export async function handleRemoveIndexer({ req, respond, peer, maxBodyBytes }) {
100
- const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
101
- if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
102
- await removeIndexer(peer, { key: body.key });
103
- respond(200, { ok: true });
104
- }
105
-
106
- export async function handleJoinValidator({ req, respond, peer, maxBodyBytes }) {
107
- const body = await readJsonBody(req, { maxBytes: maxBodyBytes });
108
- if (!body || typeof body !== "object") return respond(400, { error: "Missing JSON body." });
109
- await joinValidator(peer, { address: body.address });
110
- respond(200, { ok: true });
53
+ const payload = await contractTx(peer, {
54
+ tx: body.tx,
55
+ prepared_command: body.prepared_command,
56
+ address: body.address,
57
+ signature: body.signature,
58
+ nonce: body.nonce,
59
+ sim: body.sim,
60
+ });
61
+ respond(200, payload);
111
62
  }
package/rpc/routes/v1.js CHANGED
@@ -2,36 +2,19 @@ import {
2
2
  handleHealth,
3
3
  handleStatus,
4
4
  handleGetState,
5
- handleBroadcastTx,
6
- handleDeploySubnet,
7
- handleSetChatStatus,
8
- handlePostChatMessage,
9
- handleSetNick,
10
- handleAddAdmin,
11
- handleAddWriter,
12
- handleAddIndexer,
13
- handleRemoveWriter,
14
- handleRemoveIndexer,
15
- handleJoinValidator,
5
+ handleGetContractSchema,
6
+ handleContractNonce,
7
+ handleContractPrepareTx,
8
+ handleContractTx,
16
9
  } from "../handlers.js";
17
10
 
18
11
  export const v1Routes = [
19
12
  { method: "GET", path: "/health", handler: handleHealth },
20
13
  { method: "GET", path: "/status", handler: handleStatus },
21
14
  { method: "GET", path: "/state", handler: handleGetState },
22
-
23
- { method: "POST", path: "/tx", handler: handleBroadcastTx },
24
- { method: "POST", path: "/deploy-subnet", handler: handleDeploySubnet },
25
-
26
- { method: "POST", path: "/chat/status", handler: handleSetChatStatus },
27
- { method: "POST", path: "/chat/post", handler: handlePostChatMessage },
28
- { method: "POST", path: "/chat/nick", handler: handleSetNick },
29
-
30
- { method: "POST", path: "/admin/add-admin", handler: handleAddAdmin },
31
- { method: "POST", path: "/admin/add-writer", handler: handleAddWriter },
32
- { method: "POST", path: "/admin/add-indexer", handler: handleAddIndexer },
33
- { method: "POST", path: "/admin/remove-writer", handler: handleRemoveWriter },
34
- { method: "POST", path: "/admin/remove-indexer", handler: handleRemoveIndexer },
35
-
36
- { method: "POST", path: "/msb/join-validator", handler: handleJoinValidator },
15
+ { method: "GET", path: "/contract/schema", handler: handleGetContractSchema },
16
+ // Wallet→peer flow: server-side tx prepare + wallet signature + broadcast.
17
+ { method: "GET", path: "/contract/nonce", handler: handleContractNonce },
18
+ { method: "POST", path: "/contract/tx/prepare", handler: handleContractPrepareTx },
19
+ { method: "POST", path: "/contract/tx", handler: handleContractTx },
37
20
  ];
package/rpc/services.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import b4a from "b4a";
2
- import { deploySubnet as deploySubnetFn, setChatStatus as setChatStatusFn, setNick as setNickFn } from "../src/functions.js";
3
- import { addAdminKey, addWriterKey, addIndexerKey, removeWriterKey, removeIndexerKey, joinValidator as joinValidatorFn } from "../src/functions.js";
2
+ import { fastestToJsonSchema } from "./utils/schemaToJson.js";
4
3
 
5
4
  const asHex32 = (value, field) => {
6
5
  const hex = String(value ?? "").trim().toLowerCase();
@@ -8,6 +7,14 @@ const asHex32 = (value, field) => {
8
7
  return hex;
9
8
  };
10
9
 
10
+ const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
11
+
12
+ const requireApi = (peer) => {
13
+ const api = peer?.protocol_instance?.api;
14
+ if (!api) throw new Error("Protocol API not initialized.");
15
+ return api;
16
+ };
17
+
11
18
  export async function getStatus(peer) {
12
19
  const subnetBootstrapHex = b4a.isBuffer(peer.bootstrap)
13
20
  ? b4a.toString(peer.bootstrap, "hex")
@@ -44,107 +51,112 @@ export async function getStatus(peer) {
44
51
  };
45
52
  }
46
53
 
47
- export async function getState(peer, key, { confirmed = true } = {}) {
48
- const k = String(key ?? "");
49
- if (!k) throw new Error("Missing key.");
50
- if (!peer.base?.view) throw new Error("Peer view not ready.");
51
- if (confirmed) {
52
- const viewSession = peer.base.view.checkout(peer.base.view.core.signedLength);
53
- try {
54
- const res = await viewSession.get(k);
55
- return res?.value ?? null;
56
- } finally {
57
- await viewSession.close();
58
- }
54
+ const inferPrototypeOps = (contract) => {
55
+ const proto = Object.getPrototypeOf(contract);
56
+ const baseProto = Object.getPrototypeOf(proto);
57
+ const baseNames = new Set(Object.getOwnPropertyNames(baseProto ?? {}));
58
+ const names = Object.getOwnPropertyNames(proto);
59
+ const ops = [];
60
+ for (const name of names) {
61
+ if (name === "constructor") continue;
62
+ if (name.startsWith("_")) continue;
63
+ if (baseNames.has(name)) continue;
64
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
65
+ if (!desc || typeof desc.value !== "function") continue;
66
+ ops.push(name);
59
67
  }
60
- const res = await peer.base.view.get(k);
61
- return res?.value ?? null;
62
- }
68
+ return ops;
69
+ };
63
70
 
64
- export async function broadcastTx(peer, { command, sim = false } = {}) {
65
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required for /tx).");
66
- const cmd = command;
67
- if (!cmd) throw new Error("Missing command.");
68
- return await peer.protocol_instance.tx({ command: cmd }, !!sim);
69
- }
71
+ const convertContractOpSchema = (fv) => {
72
+ if (!isObject(fv)) return { value: {} };
73
+ const out = {};
74
+ if (fv.key !== undefined) out.key = fastestToJsonSchema(fv.key);
75
+ if (fv.value !== undefined) out.value = fastestToJsonSchema(fv.value);
76
+ if (Object.keys(out).length === 0) out.value = fastestToJsonSchema(fv);
77
+ return out;
78
+ };
70
79
 
71
- export async function deploySubnet(peer) {
72
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required for /deploy-subnet).");
73
- return await deploySubnetFn("/deploy_subnet", peer);
74
- }
80
+ export async function getContractSchema(peer) {
81
+ const contract = peer?.contract_instance;
82
+ if (!contract) throw new Error("Contract instance not initialized.");
75
83
 
76
- export async function setChatStatus(peer, enabled) {
77
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required).");
78
- const on = enabled === true || enabled === 1 || enabled === "1";
79
- return await setChatStatusFn(`/set_chat_status --enabled ${on ? 1 : 0}`, peer);
80
- }
84
+ const registrations = contract.metadata ?? {};
85
+ const regSchemas = registrations.schemas ?? {};
86
+ const regFunctions = registrations.functions ?? {};
87
+ const regFeatures = registrations.features ?? {};
88
+
89
+ const schemaNames = Object.keys(regSchemas);
90
+ const functionNames = Object.keys(regFunctions);
91
+ const featureNames = Object.keys(regFeatures);
81
92
 
82
- export async function postChatMessage(peer, { message, reply_to = null } = {}) {
83
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required).");
84
- const chatStatus = await peer.base.view.get('chat_status');
85
- if (chatStatus === null || chatStatus.value !== 'on') throw new Error('Chat is disabled.');
86
- const msg = String(message ?? "");
87
- if (!msg.trim()) throw new Error("Empty message not allowed.");
88
- if (b4a.byteLength(msg) > peer.protocol_instance.msgMaxBytes()) throw new Error("Message too large.");
89
- const nonce = peer.protocol_instance.generateNonce();
90
- const signature = {
91
- dispatch: {
92
- type: "msg",
93
- msg,
94
- address: peer.wallet.publicKey,
95
- attachments: [],
96
- deleted_by: null,
97
- reply_to: reply_to != null ? parseInt(reply_to) : null,
98
- pinned: false,
99
- pin_id: null,
93
+ const hasAnyExplicit = schemaNames.length > 0 || functionNames.length > 0 || featureNames.length > 0;
94
+
95
+ const inferred = hasAnyExplicit ? [] : inferPrototypeOps(contract);
96
+ const txTypes = [...new Set([...schemaNames, ...functionNames, ...featureNames, ...inferred])].sort();
97
+
98
+ const ops = {};
99
+ for (const type of txTypes) {
100
+ if (regSchemas[type] !== undefined) ops[type] = convertContractOpSchema(regSchemas[type]);
101
+ else ops[type] = { value: {} };
102
+ }
103
+
104
+ return {
105
+ schemaVersion: 1,
106
+ schemaFormat: "json-schema",
107
+ contract: {
108
+ contractClass: contract.constructor?.name ?? null,
109
+ protocolClass: peer.protocol_instance?.constructor?.name ?? null,
110
+ txTypes,
111
+ ops,
100
112
  },
113
+ api: peer.protocol_instance?.getApiSchema ? peer.protocol_instance.getApiSchema() : { methods: {} },
101
114
  };
102
- const hash = peer.wallet.sign(JSON.stringify(signature) + nonce);
103
- await peer.base.append({ type: "msg", value: signature, hash, nonce });
104
- return { ok: true };
105
115
  }
106
116
 
107
- export async function setNick(peer, { nick, user = null } = {}) {
108
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required).");
109
- const n = String(nick ?? "").trim();
110
- if (!n) throw new Error("Missing nick.");
111
- const u = user != null ? String(user).trim().toLowerCase() : null;
112
- const input = u ? `/set_nick --nick "${n}" --user "${u}"` : `/set_nick --nick "${n}"`;
113
- return await setNickFn(input, peer);
117
+ export async function contractGenerateNonce(peer) {
118
+ const api = requireApi(peer);
119
+ return api.generateNonce();
114
120
  }
115
121
 
116
- export async function addAdmin(peer, { address }) {
117
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required).");
118
- const pk = asHex32(address, "address");
119
- return await addAdminKey(pk, peer);
120
- }
122
+ export async function contractPrepareTx(peer, { prepared_command, address, nonce } = {}) {
123
+ const api = requireApi(peer);
124
+ if (!isObject(prepared_command)) throw new Error("prepared_command must be an object.");
125
+ const addr = asHex32(address, "address");
126
+ const n = asHex32(nonce, "nonce");
121
127
 
122
- export async function addWriter(peer, { key }) {
123
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required).");
124
- const wk = asHex32(key, "key");
125
- return await addWriterKey(wk, peer);
126
- }
128
+ if (peer?.protocol_instance?.safeJsonStringify == null) {
129
+ throw new Error("safeJsonStringify is not available on protocol instance.");
130
+ }
127
131
 
128
- export async function addIndexer(peer, { key }) {
129
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required).");
130
- const wk = asHex32(key, "key");
131
- return await addIndexerKey(wk, peer);
132
- }
132
+ const json = peer.protocol_instance.safeJsonStringify(prepared_command);
133
+ if (json == null) throw new Error("Failed to stringify prepared_command.");
133
134
 
134
- export async function removeWriter(peer, { key }) {
135
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required).");
136
- const wk = asHex32(key, "key");
137
- return await removeWriterKey(wk, peer);
135
+ const command_hash = await peer.createHash("blake3", json);
136
+ const tx = await api.generateTx(addr, command_hash, n);
137
+ return { tx, command_hash };
138
138
  }
139
139
 
140
- export async function removeIndexer(peer, { key }) {
141
- if (!peer.base?.writable) throw new Error("Peer subnet is not writable (writer required).");
142
- const wk = asHex32(key, "key");
143
- return await removeIndexerKey(wk, peer);
140
+ export async function contractTx(peer, { tx, prepared_command, address, signature, nonce, sim = false } = {}) {
141
+ const api = requireApi(peer);
142
+ if (!isObject(prepared_command)) throw new Error("prepared_command must be an object.");
143
+ const res = await api.tx(tx, prepared_command, address, signature, nonce, sim === true);
144
+ return { result: res };
144
145
  }
145
146
 
146
- export async function joinValidator(peer, { address }) {
147
- const addr = String(address ?? "").trim();
148
- if (!addr) throw new Error("Missing address.");
149
- return await joinValidatorFn(`/join_validator --address ${addr}`, peer);
147
+ export async function getState(peer, key, { confirmed = true } = {}) {
148
+ const k = String(key ?? "");
149
+ if (!k) throw new Error("Missing key.");
150
+ if (!peer.base?.view) throw new Error("Peer view not ready.");
151
+ if (confirmed) {
152
+ const viewSession = peer.base.view.checkout(peer.base.view.core.signedLength);
153
+ try {
154
+ const res = await viewSession.get(k);
155
+ return res?.value ?? null;
156
+ } finally {
157
+ await viewSession.close();
158
+ }
159
+ }
160
+ const res = await peer.base.view.get(k);
161
+ return res?.value ?? null;
150
162
  }