@toon-protocol/townhouse 0.1.0 → 0.1.2
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/README.md +95 -438
- package/dist/chunk-5O4SBV5O.js +538 -0
- package/dist/chunk-5O4SBV5O.js.map +1 -0
- package/dist/{chunk-4WCMVIO4.js → chunk-W33MEOPM.js} +10568 -180
- package/dist/chunk-W33MEOPM.js.map +1 -0
- package/dist/cli.js +65 -12
- package/dist/cli.js.map +1 -1
- package/dist/compose/townhouse-hs.yml +8 -8
- package/dist/image-manifest.json +10 -10
- package/dist/index.d.ts +36 -4
- package/dist/index.js +2 -1
- package/dist/{rsa-from-seed-VMNLNDZM.js → rsa-from-seed-XIT6EU73.js} +9 -4
- package/dist/rsa-from-seed-XIT6EU73.js.map +1 -0
- package/package.json +4 -4
- package/dist/chunk-4WCMVIO4.js.map +0 -1
- package/dist/rsa-from-seed-VMNLNDZM.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,503 +1,160 @@
|
|
|
1
1
|
# @toon-protocol/townhouse
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Run a TOON node on your own machine with two commands.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Stories 21.9–21.13 (dashboard views) and 21.8.5 (design system) **must** be developed against the Townhouse dev stack, not against mocks or the SDK E2E topology (D21-009).
|
|
8
|
-
|
|
9
|
-
### One-command boot
|
|
5
|
+
TOON is a pay-to-write, free-to-read [Nostr](https://nostr.com) relay network: writers pay a tiny per-byte fee to publish, anyone reads for free. `townhouse` is the command-line installer and dashboard for running your own node. It sets up your keys, starts the node in Docker, and publishes it as a private hidden-service address so peers can reach you without exposing anything to the public internet.
|
|
10
6
|
|
|
11
7
|
```bash
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
This command:
|
|
16
|
-
1. Builds `toon:town`, `toon:mill`, `toon:dvm` Docker images (Docker layer cache applies on subsequent runs)
|
|
17
|
-
2. Starts Anvil (EVM), Solana test-validator, and Mina lightnet chain devnets
|
|
18
|
-
3. Deploys Mock USDC to Anvil and the Solana payment-channel program
|
|
19
|
-
4. Starts the standalone connector with all 5 child peers registered
|
|
20
|
-
5. Starts 5 child nodes with deterministic Nostr keys
|
|
21
|
-
6. Polls each child's `/health` endpoint until ready (60 s timeout per node)
|
|
22
|
-
7. Prints a success banner listing every endpoint URL
|
|
23
|
-
8. Writes `.env.townhouse-dev` at the workspace root
|
|
24
|
-
|
|
25
|
-
**First run** (no cached images): ~5 minutes (dominated by image pulls).
|
|
26
|
-
**Subsequent runs**: ~90 seconds (cached images, warm Docker daemon).
|
|
27
|
-
|
|
28
|
-
### Endpoint banner
|
|
29
|
-
|
|
30
|
-
On success, the script prints every endpoint grouped by category. Copy these URLs into your browser or `curl` to verify the stack manually:
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
Connector http://127.0.0.1:28080
|
|
34
|
-
town-01 relay ws://127.0.0.1:28700
|
|
35
|
-
town-01 health http://127.0.0.1:28100
|
|
36
|
-
town-02 relay ws://127.0.0.1:28710
|
|
37
|
-
town-02 health http://127.0.0.1:28110
|
|
38
|
-
mill-01 health http://127.0.0.1:28200 (EVM↔Solana)
|
|
39
|
-
mill-02 health http://127.0.0.1:28210 (EVM↔Mina)
|
|
40
|
-
dvm-01 health http://127.0.0.1:28400
|
|
41
|
-
Anvil RPC http://127.0.0.1:28545
|
|
42
|
-
Solana RPC http://127.0.0.1:28899
|
|
43
|
-
Mina GraphQL http://127.0.0.1:28085
|
|
44
|
-
Mina Accounts http://127.0.0.1:28181
|
|
45
|
-
SOCKS5 socks5://127.0.0.1:28050
|
|
8
|
+
npx @toon-protocol/townhouse init # 1. create your config + wallet (one time)
|
|
9
|
+
npx @toon-protocol/townhouse hs up # 2. start your node
|
|
46
10
|
```
|
|
47
11
|
|
|
48
|
-
|
|
12
|
+
That's it — `hs up` prints `Apex live at <your-address>` when your node is running.
|
|
49
13
|
|
|
50
|
-
|
|
14
|
+
---
|
|
51
15
|
|
|
52
|
-
|
|
53
|
-
- `TOWNHOUSE_CONNECTOR_ADMIN_URL` — connector admin base URL
|
|
54
|
-
- `TOWNHOUSE_DEV_TOWN_01_RELAY`, `TOWNHOUSE_DEV_TOWN_02_RELAY` — relay WebSocket URLs
|
|
55
|
-
- `TOWNHOUSE_DEV_TOWN_0{1,2}_HEALTH`, `TOWNHOUSE_DEV_MILL_0{1,2}_HEALTH`, `TOWNHOUSE_DEV_DVM_01_HEALTH` — BLS health URLs
|
|
56
|
-
- `TOWNHOUSE_DEV_ANVIL_RPC`, `TOWNHOUSE_DEV_SOLANA_RPC`, `TOWNHOUSE_DEV_MINA_GRAPHQL` — chain RPC URLs
|
|
57
|
-
- `SOLANA_PROGRAM_ID`, `MINA_ZKAPP_ADDRESS`, `TOON_USDC_ADDRESS` — deployed contract addresses
|
|
58
|
-
- `TOWNHOUSE_DEV_WALLET_MNEMONIC` — **DEV ONLY** BIP-39 test-vector-zero mnemonic (`abandon … about`); read by `api-server.mjs` to auto-initialize the `WalletManager` without running `townhouse init`. This is the publicly known test vector — NEVER use in production. The dev API loop **rejects any other value** at startup so a developer who pastes a real mnemonic by accident gets a loud error rather than silent address derivation.
|
|
16
|
+
## Before you start
|
|
59
17
|
|
|
60
|
-
|
|
18
|
+
You'll need:
|
|
61
19
|
|
|
62
|
-
|
|
20
|
+
- [ ] **Docker** running — verify with `docker version` (it pulls and runs your node's containers)
|
|
21
|
+
- [ ] **Node.js 20+** — verify with `node --version`
|
|
22
|
+
- [ ] **Network access to `ghcr.io`** — your node's container images are pulled from there
|
|
23
|
+
- [ ] **A few free ports on `127.0.0.1`** — `9401`, `28090`, `7100`, `3100`, `3200`, `3400` (all loopback-only)
|
|
24
|
+
- [ ] A little disk space for container images
|
|
63
25
|
|
|
64
|
-
|
|
26
|
+
> Supported on Linux and macOS (including WSL2). Everything binds to `127.0.0.1` only — nothing is exposed to the public internet except your node's hidden-service address.
|
|
65
27
|
|
|
66
|
-
|
|
28
|
+
---
|
|
67
29
|
|
|
68
|
-
|
|
69
|
-
- **Working on DVM views (21.12) or upload flows:** Set `TURBO_TOKEN` in your shell before running `up`.
|
|
30
|
+
## Quickstart
|
|
70
31
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
### Teardown
|
|
32
|
+
### 1. Initialize — `init`
|
|
74
33
|
|
|
75
34
|
```bash
|
|
76
|
-
|
|
77
|
-
./scripts/townhouse-dev-infra.sh down-v # Same + delete named volumes (fresh state next run)
|
|
78
|
-
./scripts/townhouse-dev-infra.sh status # Show container state + health summary
|
|
35
|
+
npx @toon-protocol/townhouse init
|
|
79
36
|
```
|
|
80
37
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
### Port allocation
|
|
84
|
-
|
|
85
|
-
All ports are `127.0.0.1` only (never `0.0.0.0`). Full table also in `CLAUDE.md` "Townhouse Dev Stack (28xxx)".
|
|
38
|
+
This creates `~/.townhouse/config.yaml` and an encrypted wallet, then **shows your seed phrase once**:
|
|
86
39
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
| 28080 | Connector admin |
|
|
90
|
-
| 28050 | SOCKS5 proxy |
|
|
91
|
-
| 28100 | town-01 BLS health |
|
|
92
|
-
| 28110 | town-02 BLS health |
|
|
93
|
-
| 28200 | mill-01 BLS health (EVM↔Solana) |
|
|
94
|
-
| 28210 | mill-02 BLS health (EVM↔Mina) |
|
|
95
|
-
| 28400 | dvm-01 BLS health |
|
|
96
|
-
| 28700 | town-01 relay WebSocket |
|
|
97
|
-
| 28710 | town-02 relay WebSocket |
|
|
98
|
-
| 28545 | Anvil JSON-RPC |
|
|
99
|
-
| 28899 | Solana RPC |
|
|
100
|
-
| 28900 | Solana WebSocket |
|
|
101
|
-
| 28085 | Mina GraphQL |
|
|
102
|
-
| 28181 | Mina accounts manager |
|
|
40
|
+
```text
|
|
41
|
+
Config created at ~/.townhouse/config.yaml
|
|
103
42
|
|
|
104
|
-
|
|
43
|
+
=== IMPORTANT: Back up your seed phrase ===
|
|
105
44
|
|
|
106
|
-
|
|
107
|
-
- **Not the SDK E2E topology.** The SDK E2E stack (`docker-compose-sdk-e2e.yml` / `scripts/sdk-e2e-infra.sh`) uses embedded connectors inside SDK peers. The Townhouse dev stack uses a standalone connector fronting separate child nodes — the production Townhouse shape.
|
|
108
|
-
- **Not for performance testing.** Boot-and-smoke only. Performance tuning is out of scope for this stack; it belongs in a dedicated story.
|
|
109
|
-
- **Not multi-tenant.** The 5 child nodes use deterministic dev keys that never change across `up`/`down`/`up` cycles. They are NOT for use as real TOON nodes.
|
|
45
|
+
snake juice eternal vendor remove ladder aisle crumble match hockey weasel guide
|
|
110
46
|
|
|
111
|
-
|
|
47
|
+
This is the ONLY time your seed phrase will be shown.
|
|
48
|
+
Store it safely. You will need it to recover your node keys.
|
|
49
|
+
============================================
|
|
112
50
|
|
|
113
|
-
|
|
51
|
+
Wallet saved to ~/.townhouse/wallet.enc
|
|
114
52
|
|
|
115
|
-
|
|
53
|
+
Derived Node Addresses:
|
|
54
|
+
-----------------------
|
|
55
|
+
town Nostr: 5eb3ba2d... EVM: 0x90bd2F2f...
|
|
56
|
+
mill Nostr: 47838cd5... EVM: 0xAAE12f6B...
|
|
57
|
+
dvm Nostr: 1b52a745... EVM: 0x18Ac7427...
|
|
116
58
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
./scripts/townhouse-dev-infra.sh up
|
|
122
|
-
pnpm --filter @toon-protocol/townhouse test:integration -- dev-stack-smoke
|
|
59
|
+
Next — start your node:
|
|
60
|
+
npx @toon-protocol/townhouse hs up
|
|
123
61
|
```
|
|
124
62
|
|
|
125
|
-
|
|
63
|
+
**Write the seed phrase down.** It is shown only once and is the only way to recover your keys.
|
|
126
64
|
|
|
127
|
-
|
|
65
|
+
`init` needs a password to encrypt the wallet. It will prompt you when run in a terminal, or you can pass one non-interactively:
|
|
128
66
|
|
|
129
67
|
```bash
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
# Run the integration suite (CLI lifecycle + config propagation)
|
|
134
|
-
RUN_DOCKER_INTEGRATION=1 pnpm --filter @toon-protocol/townhouse test:integration
|
|
135
|
-
|
|
136
|
-
# Or: combined script (up + test + down)
|
|
137
|
-
pnpm --filter @toon-protocol/townhouse test:e2e:docker
|
|
68
|
+
npx @toon-protocol/townhouse init --password "<your-password>"
|
|
69
|
+
# or: export TOWNHOUSE_WALLET_PASSWORD=... then run init
|
|
138
70
|
```
|
|
139
71
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
| | Dev stack (`townhouse-dev-infra.sh`) | Real-CLI E2E (`townhouse-test-infra.sh`) |
|
|
143
|
-
|---|---|---|
|
|
144
|
-
| Starts containers? | Yes (multi-peer compose) | No (tests run the real CLI) |
|
|
145
|
-
| Keys | Deterministic dev keys | Fresh wallet per test (mkdtempSync) |
|
|
146
|
-
| Topology | 2 Town + 2 Mill + 1 DVM + SOCKS5 | 1 Town + 1 Mill + 1 DVM |
|
|
147
|
-
| Port range | 28xxx | 9400 (API), 9401 (connector admin) |
|
|
148
|
-
| Audience | Dashboard developers | CI / publish gate validation |
|
|
149
|
-
|
|
150
|
-
**Diagnostic runbook:** see the header comment in `scripts/townhouse-test-infra.sh`.
|
|
151
|
-
|
|
152
|
-
**Playwright SPA tests (mock-driven + real-stack):**
|
|
72
|
+
### 2. Start your node — `hs up`
|
|
153
73
|
|
|
154
74
|
```bash
|
|
155
|
-
|
|
156
|
-
pnpm --filter @toon-protocol/townhouse-web e2e
|
|
157
|
-
|
|
158
|
-
# Real-stack lifecycle spec — requires townhouse up to be running
|
|
159
|
-
TOWNHOUSE_E2E_REAL_STACK=1 pnpm --filter @toon-protocol/townhouse-web e2e:real
|
|
75
|
+
npx @toon-protocol/townhouse hs up
|
|
160
76
|
```
|
|
161
77
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
The published `@toon-protocol/townhouse` package ships two Docker Compose templates:
|
|
165
|
-
|
|
166
|
-
| Profile | File in tarball | Purpose |
|
|
167
|
-
|---------|-----------------|---------|
|
|
168
|
-
| `hs` | `dist/compose/townhouse-hs.yml` | Operator-facing apex boot — digest-pinned GHCR images |
|
|
169
|
-
| `dev` | `dist/compose/townhouse-dev.yml` | Contributor dev stack — local `toon:*` build images |
|
|
170
|
-
|
|
171
|
-
> **Port collision warning.** The HS template binds canonical ports
|
|
172
|
-
> (`127.0.0.1:9401`, `:28090`, `:7100`, `:3100`, `:3200`, `:3400`); the
|
|
173
|
-
> contributor dev stack binds 28xxx-namespaced equivalents (28080:9401,
|
|
174
|
-
> 28100:3100, 28110:3100, 28200:3200, 28210:3200, 28400:3400, 28700:7100,
|
|
175
|
-
> 28710:7100). HS-mode and the dev stack (`scripts/townhouse-dev-infra.sh`)
|
|
176
|
-
> **must not run concurrently on the same machine** — host:9401, host:3100,
|
|
177
|
-
> host:3200, host:3400, host:7100 will conflict. The HS template's
|
|
178
|
-
> single-tenant defaults are intentional for the apex operator path
|
|
179
|
-
> (Story 45.4 `townhouse hs up`); open an enhancement issue if multi-tenant
|
|
180
|
-
> bindings become a real need.
|
|
181
|
-
|
|
182
|
-
### API
|
|
78
|
+
The first run pulls images and bootstraps the hidden service, narrating each stage:
|
|
183
79
|
|
|
184
|
-
```
|
|
185
|
-
|
|
80
|
+
```text
|
|
81
|
+
Pulling 2 apex images...
|
|
82
|
+
[1/2] ghcr.io/toon-protocol/connector@sha256:...
|
|
83
|
+
[2/2] ghcr.io/toon-protocol/townhouse-api@sha256:...
|
|
84
|
+
Bootstrapping hidden service (this takes 30–90s)…
|
|
85
|
+
Apex live at uagxuabpuvm6mf4l4zptgth2442sbct5lvtur2nffpqnouesgawyv2ad.anon
|
|
86
|
+
```
|
|
186
87
|
|
|
187
|
-
|
|
188
|
-
const yaml = loadComposeTemplate('hs');
|
|
88
|
+
The address on the final line is **your node's hidden-service address** — share it with peers, who reach you at `wss://<your-address>/btp`. It's also saved to `~/.townhouse/host.json`. On a cold image cache the first boot can take a few minutes; later boots are faster.
|
|
189
89
|
|
|
190
|
-
|
|
191
|
-
// Both output files are written with mode 0o600 (NFR8 — operator-secret).
|
|
192
|
-
const { composePath, manifestPath } = materializeComposeTemplate('hs');
|
|
193
|
-
// composePath → ~/.townhouse/compose/townhouse-hs.yml
|
|
194
|
-
// manifestPath → ~/.townhouse/image-manifest.json
|
|
195
|
-
```
|
|
90
|
+
If you run it in an interactive terminal, a live dashboard opens after the node is up. Press `Ctrl-C` to exit the dashboard — your node keeps running.
|
|
196
91
|
|
|
197
|
-
|
|
92
|
+
### 3. Stop your node — `hs down`
|
|
198
93
|
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
townhouseHome?: string; // Override ~/.townhouse/ write target (useful in tests)
|
|
202
|
-
distDir?: string; // Override dist/ read root (useful in tests)
|
|
203
|
-
}
|
|
94
|
+
```bash
|
|
95
|
+
npx @toon-protocol/townhouse hs down
|
|
204
96
|
```
|
|
205
97
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
The manifest pinning every image to a content-addressed `sha256:` digest:
|
|
209
|
-
|
|
210
|
-
```json
|
|
211
|
-
{
|
|
212
|
-
"schemaVersion": 1,
|
|
213
|
-
"townhouseVersion": "0.1.0",
|
|
214
|
-
"builtAt": "<ISO timestamp>",
|
|
215
|
-
"images": {
|
|
216
|
-
"townhouse-api": { "name": "ghcr.io/toon-protocol/townhouse-api", "tag": "0.1.0", "digest": "sha256:..." },
|
|
217
|
-
"town": { "name": "ghcr.io/toon-protocol/town", "tag": "0.1.0", "digest": "sha256:..." },
|
|
218
|
-
"mill": { "name": "ghcr.io/toon-protocol/mill", "tag": "0.1.0", "digest": "sha256:..." },
|
|
219
|
-
"dvm": { "name": "ghcr.io/toon-protocol/dvm", "tag": "0.1.0", "digest": "sha256:..." },
|
|
220
|
-
"connector": { "name": "ghcr.io/toon-protocol/connector", "tag": "3.4.1", "digest": "sha256:..." }
|
|
221
|
-
}
|
|
222
|
-
}
|
|
98
|
+
```text
|
|
99
|
+
Apex stopped. Volumes preserved — your .anyone address is stable.
|
|
223
100
|
```
|
|
224
101
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
### Dev stack compose (canonical source)
|
|
102
|
+
Your hidden-service address stays the same across stop/start. To deliberately rotate to a brand-new address, use `npx @toon-protocol/townhouse hs down --rotate-keys`.
|
|
228
103
|
|
|
229
|
-
|
|
104
|
+
---
|
|
230
105
|
|
|
231
|
-
|
|
106
|
+
## Everyday commands
|
|
232
107
|
|
|
233
|
-
|
|
108
|
+
| Command | What it does |
|
|
109
|
+
| -------------------------------------------------- | ------------------------------------------------------ |
|
|
110
|
+
| `townhouse init` | Create config + wallet (one time) |
|
|
111
|
+
| `townhouse hs up` | Start your node and publish its hidden-service address |
|
|
112
|
+
| `townhouse hs down` | Stop your node (address preserved) |
|
|
113
|
+
| `townhouse status` | Show node status |
|
|
114
|
+
| `townhouse health` | Health summary |
|
|
115
|
+
| `townhouse logs <node-id> [-f]` | Tail a node's logs |
|
|
116
|
+
| `townhouse wallet show` | Show your derived addresses |
|
|
117
|
+
| `townhouse node list` / `node add` / `node remove` | Manage child nodes |
|
|
118
|
+
| `townhouse --help` | Full command list |
|
|
234
119
|
|
|
235
|
-
|
|
236
|
-
the operator HS-mode apex stack via a single `profile: 'dev' | 'hs'`
|
|
237
|
-
parameter:
|
|
120
|
+
(Prefix each with `npx @toon-protocol/townhouse`, or install once and call `townhouse` directly.)
|
|
238
121
|
|
|
239
|
-
|
|
240
|
-
programmatic control. Matches the lifecycle the existing `townhouse up`
|
|
241
|
-
CLI has shipped since Epic 21. No `composePath` required.
|
|
242
|
-
- **`profile: 'hs'`** — shells out to `docker compose -f <composePath> up -d`
|
|
243
|
-
with `--profile <type>` flags for each enabled peer. Waits on the
|
|
244
|
-
connector's `GET /admin/hs-hostname` endpoint (connector v3.5.0+) until
|
|
245
|
-
the `.anyone` hostname is published. Requires `composePath` (typically
|
|
246
|
-
the path returned by `materializeComposeTemplate('hs')`).
|
|
122
|
+
> Running with a config in a non-default location? Add `-c <path-to-config.yaml>` to any command. `init`'s next-step hint includes the right `-c` flag automatically when you use `--config-dir`.
|
|
247
123
|
|
|
248
|
-
|
|
249
|
-
```typescript
|
|
250
|
-
import { materializeComposeTemplate, DockerOrchestrator } from '@toon-protocol/townhouse';
|
|
251
|
-
import Docker from 'dockerode';
|
|
252
|
-
|
|
253
|
-
const { composePath } = materializeComposeTemplate('hs');
|
|
254
|
-
const docker = new Docker();
|
|
255
|
-
const orch = new DockerOrchestrator(docker, config, walletManager, {
|
|
256
|
-
profile: 'hs',
|
|
257
|
-
composePath,
|
|
258
|
-
});
|
|
259
|
-
await orch.up([]); // apex-only (connector + townhouse-api)
|
|
260
|
-
```
|
|
124
|
+
---
|
|
261
125
|
|
|
262
|
-
|
|
126
|
+
## Troubleshooting
|
|
263
127
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
`
|
|
270
|
-
|
|
128
|
+
| Symptom | Fix |
|
|
129
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
130
|
+
| `cannot start — host ports already in use` | Another stack is using the canonical ports. Stop it (the message tells you how), or run `hs up --skip-preflight` if you know the conflict is harmless. |
|
|
131
|
+
| Boot fails pulling images | Check your network and that you can reach `ghcr.io`, then retry. |
|
|
132
|
+
| `Docker daemon unreachable` | Start Docker and re-run. |
|
|
133
|
+
| `Wallet password required, but no interactive terminal…` | In CI/SSH there's no prompt — pass `--password` or set `TOWNHOUSE_WALLET_PASSWORD`. |
|
|
134
|
+
| `Wallet not found … Run \`townhouse init\` first.` | Run `init` before `hs up`. |
|
|
135
|
+
| Forgot your password | The wallet is encrypted and can't be recovered without it. Re-run `init --force` to regenerate (this **replaces** your keys — only do this if you've backed up the seed elsewhere or are starting fresh). |
|
|
271
136
|
|
|
272
|
-
|
|
137
|
+
For verbose logs on any failure, re-run with `DEBUG=townhouse:*`.
|
|
273
138
|
|
|
274
|
-
|
|
275
|
-
apex stack (connector + townhouse-api) and publishes a `.anyone` hidden-service
|
|
276
|
-
address, writing the address to `~/.townhouse/host.json` as the final step.
|
|
139
|
+
---
|
|
277
140
|
|
|
278
|
-
|
|
141
|
+
## Using it as a library
|
|
279
142
|
|
|
280
|
-
|
|
281
|
-
npx @toon-protocol/townhouse init # initialise config + wallet (one-time)
|
|
282
|
-
npx @toon-protocol/townhouse hs up # boot apex — prints "Apex live at <hostname>.anyone"
|
|
283
|
-
```
|
|
143
|
+
The package also exports its building blocks for programmatic use — `DockerOrchestrator`, `ConnectorAdminClient`, wallet helpers, and the Compose-template loaders:
|
|
284
144
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
| File | Mode | Purpose |
|
|
292
|
-
|------|------|---------|
|
|
293
|
-
| `~/.townhouse/config.yaml` | 0o600 | Townhouse config (written by `init`) |
|
|
294
|
-
| `~/.townhouse/wallet.enc` | 0o600 | Encrypted BIP-39 wallet (written by `init`) |
|
|
295
|
-
| `~/.townhouse/compose/townhouse-hs.yml` | 0o600 | Materialised HS compose template |
|
|
296
|
-
| `~/.townhouse/image-manifest.json` | 0o600 | Digest-pinned image manifest |
|
|
297
|
-
| `~/.townhouse/connector.yaml` | 0o600 | Connector config with `anon.enabled: true` |
|
|
298
|
-
| `~/.townhouse/host.json` | 0o600 | Published hostname + metadata |
|
|
299
|
-
|
|
300
|
-
`host.json` schema:
|
|
301
|
-
```json
|
|
302
|
-
{
|
|
303
|
-
"hostname": "<onion>.anyone",
|
|
304
|
-
"publishedAt": "<ISO-8601>",
|
|
305
|
-
"connectorAdminUrl": "http://127.0.0.1:9401",
|
|
306
|
-
"townhouseApiUrl": "http://127.0.0.1:28090",
|
|
307
|
-
"writtenAt": "<ISO-8601>"
|
|
308
|
-
}
|
|
145
|
+
```typescript
|
|
146
|
+
import {
|
|
147
|
+
materializeComposeTemplate,
|
|
148
|
+
DockerOrchestrator,
|
|
149
|
+
} from '@toon-protocol/townhouse';
|
|
309
150
|
```
|
|
310
151
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
Re-running `townhouse hs up` against an already-running apex detects the
|
|
314
|
-
running connector, re-prints the hostname, and exits 0 without pulling images
|
|
315
|
-
or restarting containers. `~/.townhouse/host.json` is refreshed.
|
|
152
|
+
See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full API surface and internals.
|
|
316
153
|
|
|
317
|
-
|
|
154
|
+
## Contributing / local development
|
|
318
155
|
|
|
319
|
-
|
|
320
|
-
|---------|---------|-------------|--------------|
|
|
321
|
-
| `townhouse hs down` | **Preserved** (`townhouse-hs-anon`) | Kept | **Same** `.anyone` address |
|
|
322
|
-
| `townhouse hs down --rotate-keys` | **Deleted** | Deleted | **New** `.anyone` address |
|
|
156
|
+
The local multi-node dev stack, the E2E test harnesses, Compose-template internals, and advanced docker-compose operator paths are documented in [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
|
323
157
|
|
|
324
|
-
|
|
325
|
-
a TTY (CI, scripted), it proceeds without prompting.
|
|
326
|
-
|
|
327
|
-
### Password sourcing
|
|
328
|
-
|
|
329
|
-
Resolution order:
|
|
330
|
-
1. `--password <pw>` flag
|
|
331
|
-
2. `TOWNHOUSE_WALLET_PASSWORD` environment variable
|
|
332
|
-
3. Interactive prompt (only when `process.stdin.isTTY === true`)
|
|
333
|
-
4. Exit 1 with an error message (non-interactive, no password provided)
|
|
334
|
-
|
|
335
|
-
### Failure-state copy (UX-DR5)
|
|
336
|
-
|
|
337
|
-
| Class | Detection | Next step shown |
|
|
338
|
-
|-------|-----------|-----------------|
|
|
339
|
-
| anon-timeout | `HS hostname publication timeout` in error | `Re-run with DEBUG=townhouse:*` |
|
|
340
|
-
| anon-disabled | `anon-disabled (HTTP 503)` from probe | Edit `connector.yaml`, set `anon.enabled: true` |
|
|
341
|
-
| image-pull-failure | `failed to pull` / `pull access denied` in stderr | Check your network |
|
|
342
|
-
| port-collision | `address already in use` / `port is already allocated` in stderr | Stop the conflicting service |
|
|
343
|
-
| missing-docker-sock | `Cannot connect to the Docker daemon` / `docker CLI not found` | Start Docker |
|
|
344
|
-
| generic | Any other error | `Run with DEBUG=townhouse:*` |
|
|
345
|
-
|
|
346
|
-
## Running the townhouse as a hidden service (laptop)
|
|
347
|
-
|
|
348
|
-
`docker-compose-townhouse-hs.yml` brings up the full operator stack —
|
|
349
|
-
apex connector + town + mill + dvm + (optional) Anvil + Solana + EVM
|
|
350
|
-
faucet — with the connector publishing a `.anyone` hidden service via
|
|
351
|
-
the Anyone Protocol overlay. External peers reach the townhouse only
|
|
352
|
-
through that `.anyone` address; the laptop never exposes anything to
|
|
353
|
-
the public clearnet.
|
|
354
|
-
|
|
355
|
-
```bash
|
|
356
|
-
# 1. Build the local node images (one-time, until they're on ghcr)
|
|
357
|
-
docker compose -f docker-compose-townhouse.yml --profile town --profile mill --profile dvm build
|
|
358
|
-
|
|
359
|
-
# 2. Pick a chain profile (localnet is the default — copy when ready to switch)
|
|
360
|
-
cp .env.townhouse-hs.example .env
|
|
361
|
-
|
|
362
|
-
# 3. Boot the HS stack. Profiles select what runs:
|
|
363
|
-
# --profile localnet bundles anvil + solana (skip for real testnets)
|
|
364
|
-
# --profile town/mill/dvm child nodes
|
|
365
|
-
# --profile faucet EVM ETH + Mock USDC faucet UI on :3500
|
|
366
|
-
docker compose -f docker-compose-townhouse-hs.yml \
|
|
367
|
-
--profile localnet --profile town --profile mill --profile dvm --profile faucet up -d
|
|
368
|
-
|
|
369
|
-
# 4. Wait ~30-90s for anon to bootstrap and publish the descriptor.
|
|
370
|
-
# Then read your published .anyone address:
|
|
371
|
-
docker compose -f docker-compose-townhouse-hs.yml exec connector \
|
|
372
|
-
cat /var/lib/anon/hs/hostname
|
|
373
|
-
# → eag2qnhil4vpvfo2eu3qtqj3rzzkrzbmboivwwbbgzr4svfvjigoxpad.anyone
|
|
374
|
-
|
|
375
|
-
# 5. Share that address with peers. They reach you over Tor at:
|
|
376
|
-
# wss://<address>.anyone/btp
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### Chain configuration
|
|
380
|
-
|
|
381
|
-
Mill and the faucet read chain endpoints from environment variables, with
|
|
382
|
-
localnet defaults. Override via `.env` to switch profiles — see
|
|
383
|
-
`.env.townhouse-hs.example` for the four supported shapes:
|
|
384
|
-
|
|
385
|
-
| Profile | EVM | Solana | Mock USDC |
|
|
386
|
-
|---|---|---|---|
|
|
387
|
-
| **localnet** (default) | bundled Anvil at `anvil:8545` | bundled validator at `solana:8899` | pre-deployed in both bundled images |
|
|
388
|
-
| **Akash devnet** | `anvil` lease URL from `deploy/akash/leases.json` | `solana` lease URL from same | baked into `akash-anvil` + `akash-solana` images (same addresses as localnet) |
|
|
389
|
-
| **Public testnet** | Sepolia (`infura.io/v3/<KEY>`) | `api.devnet.solana.com` | real Circle testnet USDC contracts |
|
|
390
|
-
| **Mainnet** | mainnet RPC | `api.mainnet-beta.solana.com` | real Circle mainnet USDC — **disable the faucet profile** |
|
|
391
|
-
|
|
392
|
-
Variables consumed: `EVM_RPC_URL`, `EVM_CHAIN_ID`, `EVM_USDC_ADDRESS`,
|
|
393
|
-
`SOLANA_RPC_URL`, `SOLANA_USDC_MINT`. Setting these in `.env` configures
|
|
394
|
-
the laptop compose AND the Akash deploy (`scripts/akash-deploy.sh
|
|
395
|
-
townhouse`) identically.
|
|
396
|
-
|
|
397
|
-
### Faucet workflow
|
|
398
|
-
|
|
399
|
-
**EVM ETH + Mock USDC** — bundled `faucet` service runs at
|
|
400
|
-
`http://127.0.0.1:3500`. Operator pastes their address, gets ETH for gas
|
|
401
|
-
and Mock USDC for transfers. Rate-limited 1 request per address per hour
|
|
402
|
-
by default (override via `FAUCET_RATE_LIMIT_HOURS`). The faucet uses
|
|
403
|
-
well-known Anvil dev keys — only meaningful against localnet or the
|
|
404
|
-
Akash devnet; harmless against testnets/mainnet (transactions just fail).
|
|
405
|
-
|
|
406
|
-
**Solana SOL + Mock USDC** — the standalone EVM faucet container does
|
|
407
|
-
NOT yet handle Solana. Two paths until that gap closes:
|
|
408
|
-
|
|
409
|
-
1. **Dashboard panel**: the townhouse host API (`pnpm --filter
|
|
410
|
-
@toon-protocol/townhouse-web dev` + the host-side townhouse API)
|
|
411
|
-
exposes a Faucet panel that does both EVM and Solana drips through
|
|
412
|
-
`POST /api/faucet`. Best for live operator use.
|
|
413
|
-
2. **Script**: `scripts/faucet-sol-usdc.mjs <recipient>` from the host —
|
|
414
|
-
talks to whatever `SOLANA_RPC_URL` resolves to in `leases.json`.
|
|
415
|
-
|
|
416
|
-
Both options use the bootstrap-baked Mock USDC mint
|
|
417
|
-
(`6GbdrVghwNKTz9raga7y3Y4qqX5Zgg3AC4d48Kt7C59Q`) and faucet authority
|
|
418
|
-
keypair at `infra/solana/keys/faucet-authority.json`.
|
|
419
|
-
|
|
420
|
-
### Persistence
|
|
421
|
-
|
|
422
|
-
The `townhouse-hs-anon` named docker volume preserves
|
|
423
|
-
`hs_ed25519_secret_key` across `docker compose down` cycles — the
|
|
424
|
-
`.anyone` address is stable for as long as the volume exists. Delete the
|
|
425
|
-
volume to rotate the address.
|
|
426
|
-
|
|
427
|
-
### What's exposed on the host (loopback only)
|
|
428
|
-
|
|
429
|
-
- Connector admin: `127.0.0.1:9401` — no auth, **never expose publicly**
|
|
430
|
-
- Anvil RPC: `127.0.0.1:8545` (localnet profile)
|
|
431
|
-
- Solana RPC: `127.0.0.1:8899`, WS `127.0.0.1:8900` (localnet profile)
|
|
432
|
-
- Town Nostr relay clearnet: `127.0.0.1:7100` — direct local clients
|
|
433
|
-
bypass the HS, handy for debugging
|
|
434
|
-
- EVM faucet UI: `127.0.0.1:3500` (faucet profile)
|
|
435
|
-
|
|
436
|
-
### Akash deployment of the same stack
|
|
437
|
-
|
|
438
|
-
`deploy/akash/townhouse.sdl.yaml` deploys apex + town + mill + dvm +
|
|
439
|
-
faucet to Akash with the same architecture. Chain devnets stay as
|
|
440
|
-
separate Akash leases (clearnet) — see `scripts/akash-deploy.sh`'s
|
|
441
|
-
`cmd_townhouse` (TODO) for the leases.json wiring. The faucet's HTTP
|
|
442
|
-
port is the SDL's "one global service" validator scaffolding;
|
|
443
|
-
operationally, external peers reach the townhouse only via the `.anyone`
|
|
444
|
-
hidden service.
|
|
445
|
-
|
|
446
|
-
## Package overview
|
|
447
|
-
|
|
448
|
-
The `@toon-protocol/townhouse` package provides:
|
|
449
|
-
|
|
450
|
-
- **DockerOrchestrator** — manages container lifecycle for Town/Mill/DVM nodes
|
|
451
|
-
- **ConnectorConfigGenerator** — generates connector peer config from node identities
|
|
452
|
-
- **ConnectorAdminClient** — typed HTTP client for the connector admin API (`/health`, `/admin/peers`, `/admin/metrics.json`)
|
|
453
|
-
- **HD wallet management** — BIP-44 key derivation per node type (story 21.4)
|
|
454
|
-
- **Fastify REST/WebSocket metrics API** — host-side API for the dashboard (story 21.8)
|
|
455
|
-
|
|
456
|
-
See `packages/townhouse/src/index.ts` for the full public API surface.
|
|
457
|
-
|
|
458
|
-
## Transport configuration
|
|
459
|
-
|
|
460
|
-
The `transport` block selects how the connector reaches peers (outbound) and
|
|
461
|
-
how peers reach the connector (inbound).
|
|
462
|
-
|
|
463
|
-
```yaml
|
|
464
|
-
transport:
|
|
465
|
-
mode: direct # 'direct' | 'ator'
|
|
466
|
-
socksProxy: socks5h://proxy.ator.io:9050 # required when mode='ator'
|
|
467
|
-
externalUrl: wss://my-connector.example/btp # see below
|
|
468
|
-
hiddenService: # optional — connector publishes its own .anyone HS
|
|
469
|
-
dir: /var/lib/anon/hs
|
|
470
|
-
port: 3000
|
|
471
|
-
startupTimeoutMs: 60000 # optional
|
|
472
|
-
stopTimeoutMs: 10000 # optional
|
|
473
|
-
externalUrl: wss://forced.anyone/btp # optional override of "auto"
|
|
474
|
-
```
|
|
158
|
+
## License
|
|
475
159
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
**`mode: 'ator'`** — outbound BTP through the Anyone Protocol (ATOR) overlay
|
|
479
|
-
via SOCKS5. Requires either `externalUrl` (operator-managed anon binary
|
|
480
|
-
external to the connector) OR `hiddenService` (connector manages its own
|
|
481
|
-
anon binary in-process and publishes a `.anyone` hidden service). Without
|
|
482
|
-
one of these the connector rejects the manifest at boot — the validator
|
|
483
|
-
catches this case before deploy.
|
|
484
|
-
|
|
485
|
-
**`hiddenService` (Story 35.5 of the connector repo)** — when set, the
|
|
486
|
-
connector boots `@anyone-protocol/anyone-client` in-process, spawns the
|
|
487
|
-
`anon` binary, and publishes a v3 hidden service. The keypair lives at
|
|
488
|
-
`dir`; persist that path on a mounted volume to keep the `.anyone` address
|
|
489
|
-
stable across redeploys, or delete it to rotate. The connector reads
|
|
490
|
-
`${dir}/hostname` after publish and advertises `wss://<hostname>.anyone/btp`
|
|
491
|
-
to peers (you can override with an explicit `externalUrl` if needed).
|
|
492
|
-
|
|
493
|
-
**Wire-format note (silent-bug fix in this story):** the previous shape
|
|
494
|
-
emitted `transport: { mode: 'ator', socksProxy }` to the connector image,
|
|
495
|
-
but the connector at 3.3.x reads a discriminated union keyed on `type`
|
|
496
|
-
(`'direct' | 'socks5'`). The unknown `mode` field was silently discarded,
|
|
497
|
-
defaulting to direct — operators toggling ATOR got direct traffic anyway.
|
|
498
|
-
The current generator emits the correct `type: 'socks5'` shape with
|
|
499
|
-
`externalUrl`, `managed`, and `managedOptions` per the connector contract.
|
|
500
|
-
|
|
501
|
-
## Notes
|
|
502
|
-
|
|
503
|
-
`townhouse status --units=sats` exists as an undocumented power-user flag for Bitcoin-native operators. It converts the earnings block to integer sats using a CLI-supplied rate (`--rate <sats-per-usdc>`) or the `TOWNHOUSE_SATS_PER_USDC` environment variable; if neither is set, the command exits 1. There is no built-in price oracle — this is intentionally a manual conversion. USDC remains the canonical denomination across every other Townhouse surface (TUI hero band, drill subcommands like `townhouse peer` and `townhouse channels`); this flag is absent from `townhouse --help` per design decision D44-002.
|
|
160
|
+
MIT
|