trac-peer 0.2.12-rc.1 → 0.3.0

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.
@@ -36,4 +36,4 @@ jobs:
36
36
  registry-url: 'https://registry.npmjs.org/'
37
37
  cache: 'npm'
38
38
  - run: npm ci
39
- - run: npm publish --tag rc
39
+ - run: npm publish --public
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
 
@@ -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
377
 
378
- RPC is an HTTP server that runs alongside your peer and lets you control it with requests.
378
+ RPC is an HTTP server that runs alongside your peer and lets a wallet/app connect via URL (Ethereum-style).
379
+
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,24 @@ 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.
425
+ - To allow wallet tx submission, start the peer with `--api-tx-exposed` (or env `PEER_API_TX_EXPOSED=1`).
426
+ - The peer must be subnet-writable (writer) to broadcast a tx.
424
427
 
425
428
  ---
426
429
 
427
430
  ## Building your own app (Protocol + Contract)
428
431
 
429
- The runner uses a demo `DevProtocol` and `DevContract` (see `peer-main.mjs` / `scripts/run-peer.mjs`) so you can test quickly.
432
+ The runner uses demo protocol/contract files under `src/dev/` (wired in `scripts/run-peer.mjs`) so you can test quickly.
430
433
 
431
434
  For a real app, you typically:
432
435
 
@@ -443,7 +446,7 @@ All nodes in the subnet must run the same Protocol/Contract logic for determinis
443
446
 
444
447
  ## How `/tx` works (the lifecycle)
445
448
 
446
- When you run `/tx --command "..."` (or call `POST /v1/tx`) the flow is:
449
+ When you run `/tx --command "..."` in the CLI (or a wallet uses the RPC tx flow), the flow is:
447
450
 
448
451
  1) The command string is mapped into an operation object: `{ type, value }`.
449
452
  2) trac-peer hashes and signs the operation and broadcasts a settlement tx to MSB.
@@ -452,9 +455,13 @@ When you run `/tx --command "..."` (or call `POST /v1/tx`) the flow is:
452
455
  5) Every subnet node applies the subnet op and runs contract logic locally, deriving the same results from the same ordered log.
453
456
 
454
457
  Where does step (1) happen?
455
- - In the demo runner (`scripts/run-peer.mjs` / `peer-main.mjs`) it’s in `DevProtocol.mapTxCommand(...)`.
458
+ - In the demo runner (`scripts/run-peer.mjs`) it’s in the protocol class’s `mapTxCommand(...)` (example: `src/dev/pokemonProtocol.js`).
456
459
  - The base protocol method is `Protocol.mapTxCommand(...)` in `src/protocol.js`. For your own app you override that function.
457
460
 
461
+ Wallet tx flow specifics:
462
+ - The wallet 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.
464
+
458
465
  ---
459
466
 
460
467
  ## Reset / clean start
package/README.md CHANGED
@@ -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 `--api-tx-exposed` (or env `PEER_API_TX_EXPOSED=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-rc.1",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "pear": {
7
7
  "name": "trac-peer",
@@ -13,11 +13,9 @@
13
13
  },
14
14
  "exports": {
15
15
  ".": {
16
- "bare": "./peer.mjs",
17
16
  "node": "./src/index.js",
18
17
  "default": "./src/index.js"
19
18
  },
20
- "./peer": "./peer.mjs",
21
19
  "./src/*": "./src/*"
22
20
  },
23
21
  "scripts": {
@@ -57,7 +55,7 @@
57
55
  "bare-tls": "2.0.4",
58
56
  "bare-tty": "5.0.2",
59
57
  "bare-url": "2.1.5",
60
- "bare-utils": "1.2.0",
58
+ "bare-utils": "1.5.1",
61
59
  "bare-worker": "3.0.0",
62
60
  "bare-zlib": "1.2.5",
63
61
  "bip39": "3.1.0",
@@ -85,7 +83,7 @@
85
83
  "multicoin-address-validator": "0.5.25",
86
84
  "os": "npm:bare-node-os",
87
85
  "path": "npm:bare-node-path",
88
- "pear-interface": "1.0.0",
86
+ "pear-interface": "1.1.0",
89
87
  "process": "npm:bare-node-process",
90
88
  "protomux": "^3.10.1",
91
89
  "protomux-wakeup": "^2.4.0",
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
  }