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 +23 -18
- package/README.md +10 -13
- package/package.json +4 -4
- package/rpc/handlers.js +27 -76
- package/rpc/routes/v1.js +9 -26
- package/rpc/services.js +99 -87
- package/rpc/utils/schemaToJson.js +76 -0
- package/scripts/run-peer.mjs +18 -1
- package/src/contract.js +6 -0
- package/src/dev/HyperMallConctract.js +990 -0
- package/src/dev/HyperMallProtocol.js +689 -0
- package/src/dev/pokemonProtocol.js +7 -5
- package/src/protocol.js +63 -1
- package/tests/acceptance/rpc.test.js +159 -52
- package/tests/unit/baseContractProtocol.test.js +72 -0
- package/tests/unit/cliTx.test.js +86 -0
- package/tests/unit/unit.test.js +2 -1
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
-
|
|
414
|
-
- `
|
|
415
|
-
-
|
|
416
|
-
- `POST /v1/
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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`
|
|
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
|
-
- `
|
|
110
|
-
- `
|
|
111
|
-
- `POST /v1/
|
|
112
|
-
- `POST /v1/
|
|
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
|
-
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
37
|
-
const
|
|
38
|
-
|
|
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
|
|
65
|
-
const
|
|
66
|
-
|
|
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
|
|
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
|
|
75
|
-
|
|
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
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
respond(200,
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
24
|
-
{ method: "
|
|
25
|
-
|
|
26
|
-
{ method: "POST", path: "/
|
|
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 {
|
|
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
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
68
|
+
return ops;
|
|
69
|
+
};
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
if (!
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
129
|
-
if (
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
147
|
-
const
|
|
148
|
-
if (!
|
|
149
|
-
|
|
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
|
}
|