@toon-protocol/hub 0.34.3
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 +226 -0
- package/dist/chunk-5O4SBV5O.js +538 -0
- package/dist/chunk-5O4SBV5O.js.map +1 -0
- package/dist/chunk-I2R4CRUX.js +39 -0
- package/dist/chunk-I2R4CRUX.js.map +1 -0
- package/dist/chunk-JCOFMUPL.js +65 -0
- package/dist/chunk-JCOFMUPL.js.map +1 -0
- package/dist/chunk-L2U4G4OK.js +30219 -0
- package/dist/chunk-L2U4G4OK.js.map +1 -0
- package/dist/chunk-MNVIN5XK.js +125 -0
- package/dist/chunk-MNVIN5XK.js.map +1 -0
- package/dist/cli.d.ts +209 -0
- package/dist/cli.js +4809 -0
- package/dist/cli.js.map +1 -0
- package/dist/compose/townhouse-dev.yml +415 -0
- package/dist/compose/townhouse-direct.yml +391 -0
- package/dist/compose/townhouse-hs.yml +468 -0
- package/dist/demo-UJ37MLCG.js +118 -0
- package/dist/demo-UJ37MLCG.js.map +1 -0
- package/dist/index.d.ts +1342 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator-dGq7CeaO.d.ts +1507 -0
- package/dist/rsa-from-seed-XIT6EU73.js +67 -0
- package/dist/rsa-from-seed-XIT6EU73.js.map +1 -0
- package/dist/tui-QE3ZRZO3.js +638 -0
- package/dist/tui-QE3ZRZO3.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# Townhouse — Hidden Service mode (HS) operator compose template
|
|
2
|
+
#
|
|
3
|
+
# THIS IS A BUILD-TIME TEMPLATE. Do not use it directly.
|
|
4
|
+
# The digest placeholders (${TOON_*_DIGEST}) are substituted by
|
|
5
|
+
# scripts/render-compose-template.mjs during `pnpm build` (when
|
|
6
|
+
# dist/image-manifest.json is present) to produce the fully-resolved
|
|
7
|
+
# dist/compose/townhouse-hs.yml that ships inside the npm tarball.
|
|
8
|
+
#
|
|
9
|
+
# Resolved copy location at runtime: ~/.townhouse/compose/townhouse-hs.yml
|
|
10
|
+
# - Written by materializeComposeTemplate('hs') in packages/townhouse/src/compose-loader.ts
|
|
11
|
+
# - File mode: 0o600 (operator-secret — may embed private keys at deploy time)
|
|
12
|
+
#
|
|
13
|
+
# Story ownership:
|
|
14
|
+
# - Placeholder substitution: Story 45.2 (this file)
|
|
15
|
+
# - Boot sequence (townhouse hs up): Story 45.4
|
|
16
|
+
#
|
|
17
|
+
# Architecture (HS-mode v1, Epic 45):
|
|
18
|
+
# - Apex connector: standalone, anon HS publishing in-process (connector v3.5.x)
|
|
19
|
+
# - townhouse-api: containerized host API — owns /var/run/docker.sock, calls
|
|
20
|
+
# connector admin API, manages lifecycle for lazy-provisioned peers
|
|
21
|
+
# - town/mill/dvm: lazy-provisioned via Docker Compose profiles (Epic 46)
|
|
22
|
+
# Story 45.4 boots only connector + townhouse-api at apex install
|
|
23
|
+
#
|
|
24
|
+
# Digest placeholders (substituted at build time from dist/image-manifest.json):
|
|
25
|
+
# ${TOON_TOWNHOUSE_API_DIGEST} → @sha256:<hex>
|
|
26
|
+
# ${TOON_TOWN_DIGEST} → @sha256:<hex>
|
|
27
|
+
# ${TOON_MILL_DIGEST} → @sha256:<hex>
|
|
28
|
+
# ${TOON_DVM_DIGEST} → @sha256:<hex>
|
|
29
|
+
# ${TOON_CONNECTOR_DIGEST} → @sha256:<hex>
|
|
30
|
+
#
|
|
31
|
+
# Scope guard (Story 45.2 does NOT include):
|
|
32
|
+
# - ator-sidecar / ator-sidecar-relay (connector v3.5.x does HS publishing in-process)
|
|
33
|
+
# - anvil / solana / faucet (dev-stack concerns, not operator-facing)
|
|
34
|
+
# - build: directives (all images are digest-pinned GHCR pulls)
|
|
35
|
+
#
|
|
36
|
+
# Port allocation (HS-mode binds canonical ports — single-tenant operator box):
|
|
37
|
+
# 127.0.0.1:9401 connector admin
|
|
38
|
+
# 127.0.0.1:28090 townhouse-api Fastify
|
|
39
|
+
# 127.0.0.1:7100,3100 town (relay WS, BLS health) — profile gated
|
|
40
|
+
# 127.0.0.1:3200 mill BLS health — profile gated
|
|
41
|
+
# 127.0.0.1:3400 dvm BLS health — profile gated
|
|
42
|
+
#
|
|
43
|
+
# These collide with the contributor dev stack's 28xxx-namespaced bindings
|
|
44
|
+
# (28080:9401, 28100:3100, 28110:3100, 28200:3200, 28210:3200, 28400:3400,
|
|
45
|
+
# 28700:7100, 28710:7100). HS-mode and the contributor dev stack
|
|
46
|
+
# (scripts/townhouse-dev-infra.sh) MUST NOT run concurrently on the same
|
|
47
|
+
# machine. If you need to run multiple townhouse instances on a multi-tenant
|
|
48
|
+
# box, open an enhancement issue — the canonical ports here are intentional
|
|
49
|
+
# for the apex operator path (Story 45.4 `townhouse hs up`).
|
|
50
|
+
|
|
51
|
+
networks:
|
|
52
|
+
townhouse-hs-net:
|
|
53
|
+
name: townhouse-hs-net
|
|
54
|
+
driver: bridge
|
|
55
|
+
|
|
56
|
+
volumes:
|
|
57
|
+
# Named volume for the connector's .anyone keypair + HS state.
|
|
58
|
+
# Survives `docker compose down`; delete to rotate the .anyone address.
|
|
59
|
+
#
|
|
60
|
+
# The explicit `name:` fields bypass Compose's project-prefix mechanism.
|
|
61
|
+
# Without them, Compose derives the project name from the compose file's
|
|
62
|
+
# parent directory (e.g. `compose` when the file lives at
|
|
63
|
+
# ~/.townhouse/compose/townhouse-hs.yml or <tmpDir>/compose/townhouse-hs.yml),
|
|
64
|
+
# producing on-disk volumes named `compose_townhouse-hs-anon` etc. — which
|
|
65
|
+
# would break the test 5 `volumeExists('townhouse-hs-anon')` assertion AND
|
|
66
|
+
# operator-facing rotate-keys docs that reference the bare name. Discovered
|
|
67
|
+
# by Story 46.4 live gate run (Finding H, 2026-05-11).
|
|
68
|
+
townhouse-hs-anon:
|
|
69
|
+
name: townhouse-hs-anon
|
|
70
|
+
townhouse-hs-town-data:
|
|
71
|
+
name: townhouse-hs-town-data
|
|
72
|
+
townhouse-hs-mill-data:
|
|
73
|
+
name: townhouse-hs-mill-data
|
|
74
|
+
townhouse-hs-dvm-data:
|
|
75
|
+
name: townhouse-hs-dvm-data
|
|
76
|
+
|
|
77
|
+
services:
|
|
78
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
79
|
+
# Apex connector — terminates inbound BTP, publishes .anyone HS
|
|
80
|
+
#
|
|
81
|
+
# The connector image embeds @anyone-protocol/anyone-client v1.1.x+
|
|
82
|
+
# which handles HS publishing in-process (no sidecar required for v3.5.x+).
|
|
83
|
+
# Config file written by `townhouse hs up` (Story 45.4) on first-run.
|
|
84
|
+
#
|
|
85
|
+
# NFR7: connector MUST NOT mount /var/run/docker.sock.
|
|
86
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
87
|
+
# One-shot init: ensure the .anyone key volume is owned by uid 1000 (node)
|
|
88
|
+
# before the connector starts. Docker creates named volumes as root on first
|
|
89
|
+
# boot; the connector (running as node) cannot write the HS keypair/hostname
|
|
90
|
+
# without this chown step.
|
|
91
|
+
connector-init:
|
|
92
|
+
image: busybox:1.37@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028
|
|
93
|
+
user: root
|
|
94
|
+
# anon (Tor-based) requires the HS dir to be mode 0700 — too-permissive
|
|
95
|
+
# directories cause an immediate abort. Docker creates volumes as root 755.
|
|
96
|
+
command:
|
|
97
|
+
[
|
|
98
|
+
'sh',
|
|
99
|
+
'-c',
|
|
100
|
+
'mkdir -p /data && chown -R 1000:1000 /data && chmod 700 /data',
|
|
101
|
+
]
|
|
102
|
+
volumes:
|
|
103
|
+
- townhouse-hs-anon:/data
|
|
104
|
+
restart: 'no'
|
|
105
|
+
networks:
|
|
106
|
+
- townhouse-hs-net
|
|
107
|
+
|
|
108
|
+
connector:
|
|
109
|
+
image: ghcr.io/toon-protocol/connector${TOON_CONNECTOR_DIGEST}
|
|
110
|
+
# v3.5.1 has the multi-arch manifest but the default resolves to arm64 on
|
|
111
|
+
# some Docker versions. Pin to amd64 explicitly until the manifest is fixed.
|
|
112
|
+
platform: linux/amd64
|
|
113
|
+
container_name: townhouse-hs-connector
|
|
114
|
+
hostname: connector
|
|
115
|
+
depends_on:
|
|
116
|
+
connector-init:
|
|
117
|
+
condition: service_completed_successfully
|
|
118
|
+
networks:
|
|
119
|
+
- townhouse-hs-net
|
|
120
|
+
ports:
|
|
121
|
+
# Admin API on host loopback only (NFR9). Operator uses for status.
|
|
122
|
+
- '127.0.0.1:9401:9401'
|
|
123
|
+
volumes:
|
|
124
|
+
# Rendered connector config (Story 45.4 writes this on first-run).
|
|
125
|
+
# TOWNHOUSE_HOME is exported by `townhouse hs up` as the operator's
|
|
126
|
+
# config dir (default ~/.townhouse; --config-dir overrides). Docker does
|
|
127
|
+
# NOT expand `~` in bind-mount sources, so the path must come through
|
|
128
|
+
# Compose interpolation.
|
|
129
|
+
- ${TOWNHOUSE_HOME}/connector.yaml:/config/connector.yaml:ro
|
|
130
|
+
# .anyone keypair + HS state (persists across down/up cycles).
|
|
131
|
+
- townhouse-hs-anon:/var/lib/anon/hs
|
|
132
|
+
environment:
|
|
133
|
+
CONFIG_FILE: /config/connector.yaml
|
|
134
|
+
healthcheck:
|
|
135
|
+
# nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal probe
|
|
136
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:9401/health']
|
|
137
|
+
interval: 10s
|
|
138
|
+
timeout: 5s
|
|
139
|
+
retries: 5
|
|
140
|
+
# The managed anon daemon needs ~60-120s to bootstrap circuits and publish
|
|
141
|
+
# the HS hostname before the connector starts its admin API. Keep
|
|
142
|
+
# start_period generous so health-check failures during bootstrap don't
|
|
143
|
+
# prematurely mark the container unhealthy.
|
|
144
|
+
start_period: 150s
|
|
145
|
+
restart: unless-stopped
|
|
146
|
+
|
|
147
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
148
|
+
# Townhouse API — containerized host API (NEW in HS-mode v1)
|
|
149
|
+
#
|
|
150
|
+
# Owns /var/run/docker.sock (the only service that may). Provides:
|
|
151
|
+
# - Fastify REST API for operator dashboard / CLI
|
|
152
|
+
# - Calls connector admin API (/admin/hs-hostname, /admin/peers, etc.)
|
|
153
|
+
# - Manages lifecycle for lazy-provisioned peer containers (Epic 46)
|
|
154
|
+
#
|
|
155
|
+
# Planning doc §4 anchor: "host-side townhouse-api owns dockerode and
|
|
156
|
+
# runs on the townhouse-hs-net so it can reach connector via Docker DNS".
|
|
157
|
+
# Port D21-008: Fastify host API on 127.0.0.1:28090.
|
|
158
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
159
|
+
townhouse-api:
|
|
160
|
+
image: ghcr.io/toon-protocol/townhouse-api${TOON_TOWNHOUSE_API_DIGEST}
|
|
161
|
+
container_name: townhouse-hs-api
|
|
162
|
+
# Run as the operator's host UID so bind-mounted ~/.townhouse files
|
|
163
|
+
# (rw------- 600) are readable. TOWNHOUSE_UID is injected by `townhouse hs up`.
|
|
164
|
+
# Defaults to 1000 (the standard first-user UID on Linux).
|
|
165
|
+
user: '${TOWNHOUSE_UID:-1000}'
|
|
166
|
+
# Add the host's docker socket group as a supplementary group so the
|
|
167
|
+
# non-root container user can read/write /var/run/docker.sock (typically
|
|
168
|
+
# owned root:docker mode 660 on Linux). Without this, every dockerode call
|
|
169
|
+
# — including the `pull-image` step of POST /api/nodes — fails with
|
|
170
|
+
# `connect EACCES /var/run/docker.sock`. TOWNHOUSE_DOCKER_GID is injected
|
|
171
|
+
# by `townhouse hs up` via `statSync('/var/run/docker.sock').gid`.
|
|
172
|
+
group_add:
|
|
173
|
+
- '${TOWNHOUSE_DOCKER_GID:-0}'
|
|
174
|
+
networks:
|
|
175
|
+
- townhouse-hs-net
|
|
176
|
+
depends_on:
|
|
177
|
+
connector:
|
|
178
|
+
condition: service_healthy
|
|
179
|
+
ports:
|
|
180
|
+
# Fastify host API — loopback only (NFR9).
|
|
181
|
+
- '127.0.0.1:28090:28090'
|
|
182
|
+
volumes:
|
|
183
|
+
# Docker socket — townhouse-api is the sole orchestration surface.
|
|
184
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
185
|
+
# Operator home — wallet, config, compose files, snapshots (RW).
|
|
186
|
+
# TOWNHOUSE_HOME is exported by `townhouse hs up` (see connector volume
|
|
187
|
+
# above). The container-side path stays `/.townhouse` so config.yaml
|
|
188
|
+
# `TOWNHOUSE_CONFIG: /.townhouse/config.yaml` resolves identically
|
|
189
|
+
# regardless of the operator's host-side config dir.
|
|
190
|
+
- ${TOWNHOUSE_HOME}:/.townhouse:rw
|
|
191
|
+
# Wallet dir mirrored at the host-absolute path so config.yaml's
|
|
192
|
+
# `wallet.encrypted_path` (an absolute host path set by `townhouse init`)
|
|
193
|
+
# resolves correctly inside the container. TOWNHOUSE_WALLET_DIR is
|
|
194
|
+
# injected by `townhouse hs up` as path.dirname(config.wallet.encrypted_path).
|
|
195
|
+
- ${TOWNHOUSE_WALLET_DIR:-~/.townhouse}:${TOWNHOUSE_WALLET_DIR:-~/.townhouse}:ro
|
|
196
|
+
environment:
|
|
197
|
+
# Override entrypoint default '/config/config.yaml' so the API reads from
|
|
198
|
+
# the mounted operator-home dir (where Story 45.4 writes config.yaml).
|
|
199
|
+
TOWNHOUSE_CONFIG: /.townhouse/config.yaml
|
|
200
|
+
# Bind on all interfaces inside the container so Docker's port mapping
|
|
201
|
+
# (127.0.0.1:28090:28090) can reach it from the host. ALLOW_REMOTE=1
|
|
202
|
+
# is required when HOST is non-loopback; Docker's host-only binding on
|
|
203
|
+
# the outer port (127.0.0.1) is the actual access control gate.
|
|
204
|
+
TOWNHOUSE_API_HOST: 0.0.0.0
|
|
205
|
+
TOWNHOUSE_API_ALLOW_REMOTE: '1'
|
|
206
|
+
# Wallet decryption password — operator must export TOWNHOUSE_WALLET_PASSWORD
|
|
207
|
+
# in the shell that runs `docker compose up`. The container-side
|
|
208
|
+
# entrypoint (entrypoint-townhouse-api.ts) enforces the password
|
|
209
|
+
# requirement at startup and exits 1 if it is missing — so the YAML
|
|
210
|
+
# uses `:-` (lenient default) here instead of `:?` (Compose-time
|
|
211
|
+
# mandatory error). The `:?` variant would also force `docker compose
|
|
212
|
+
# down` to error out unless the operator pre-exports the password,
|
|
213
|
+
# which broke the teardown flow. Discovered by Story 46.4 live gate
|
|
214
|
+
# run (Finding J, 2026-05-11).
|
|
215
|
+
TOWNHOUSE_WALLET_PASSWORD: '${TOWNHOUSE_WALLET_PASSWORD:-}'
|
|
216
|
+
# P1b — operator/agent wallet mode. When set, the entrypoint loads the
|
|
217
|
+
# operator wallet DIRECTLY from this mnemonic (no encrypted file, no
|
|
218
|
+
# password). Lenient `:-` default keeps `down` working when unset.
|
|
219
|
+
TOWNHOUSE_MNEMONIC: '${TOWNHOUSE_MNEMONIC:-}'
|
|
220
|
+
# Pass the host-side compose-interpolation values through to the
|
|
221
|
+
# townhouse-api container, because the API itself shells out to
|
|
222
|
+
# `docker compose -f /.townhouse/compose/townhouse-hs.yml up -d <type>`
|
|
223
|
+
# for each lazy peer-node provisioning request (Story 46.2). That
|
|
224
|
+
# nested compose re-parses the SAME YAML and needs the same env vars
|
|
225
|
+
# set — otherwise the townhouse-api volume specs interpolate to
|
|
226
|
+
# `:/.townhouse:rw` (empty source) and the inner `up` aborts with
|
|
227
|
+
# `invalid spec: :/.townhouse:rw: empty section between colons`.
|
|
228
|
+
# All four values are HOST paths/IDs (because Docker bind-mount
|
|
229
|
+
# sources are resolved by the daemon, which sees host paths).
|
|
230
|
+
# Discovered by Story 46.4 live gate run (Finding L, 2026-05-12).
|
|
231
|
+
TOWNHOUSE_HOME: '${TOWNHOUSE_HOME}'
|
|
232
|
+
TOWNHOUSE_WALLET_DIR: '${TOWNHOUSE_WALLET_DIR}'
|
|
233
|
+
TOWNHOUSE_UID: '${TOWNHOUSE_UID:-1000}'
|
|
234
|
+
TOWNHOUSE_DOCKER_GID: '${TOWNHOUSE_DOCKER_GID:-0}'
|
|
235
|
+
# MILL_RELAYS is read by POST /api/nodes {type:'mill'} preflight check
|
|
236
|
+
# inside the townhouse-api process. It must be in the API container's env
|
|
237
|
+
# (not only the mill container's env) so the check works correctly.
|
|
238
|
+
MILL_RELAYS: '${MILL_RELAYS:-}'
|
|
239
|
+
# Chain RPC + token config for the wallet balances/withdraw routes. The
|
|
240
|
+
# network profile + compose .env supply these under the production names;
|
|
241
|
+
# the API must read them (it previously read only TOWNHOUSE_DEV_* dev vars,
|
|
242
|
+
# so EVM balances showed `fetch failed` / `usdc_address_not_configured`
|
|
243
|
+
# and withdraw was blocked on a real testnet/mainnet apex — #232).
|
|
244
|
+
EVM_RPC_URL: '${EVM_RPC_URL:-}'
|
|
245
|
+
EVM_USDC_ADDRESS: '${EVM_USDC_ADDRESS:-}'
|
|
246
|
+
SOLANA_RPC_URL: '${SOLANA_RPC_URL:-}'
|
|
247
|
+
SOLANA_USDC_MINT: '${SOLANA_USDC_MINT:-}'
|
|
248
|
+
healthcheck:
|
|
249
|
+
# nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal probe
|
|
250
|
+
# Use 127.0.0.1 (not localhost) to avoid IPv6 resolution surprises. The
|
|
251
|
+
# townhouse-api exposes /api/transport (not /api/health).
|
|
252
|
+
test:
|
|
253
|
+
[
|
|
254
|
+
'CMD',
|
|
255
|
+
'wget',
|
|
256
|
+
'-q',
|
|
257
|
+
'--spider',
|
|
258
|
+
'http://127.0.0.1:28090/api/transport',
|
|
259
|
+
]
|
|
260
|
+
interval: 10s
|
|
261
|
+
timeout: 5s
|
|
262
|
+
retries: 5
|
|
263
|
+
start_period: 15s
|
|
264
|
+
restart: unless-stopped
|
|
265
|
+
|
|
266
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
267
|
+
# Town — Nostr relay node (profile: town)
|
|
268
|
+
# Lazy-provisioned via Epic 46: `townhouse node add town`
|
|
269
|
+
#
|
|
270
|
+
# SECURITY TODO (Epic 46): the per-node secrets below use `${VAR:-}` empty
|
|
271
|
+
# defaults, which silently start the container with a zero/empty key when
|
|
272
|
+
# the operator forgets to export the env var. Story 45.2 review (R2-MINOR)
|
|
273
|
+
# recommends switching to `${VAR:?msg}` (fail-fast) once Epic 46 boots
|
|
274
|
+
# these profiles — keeping the empty default for now lets `docker compose
|
|
275
|
+
# config` validation pass without injecting per-node secrets, matching
|
|
276
|
+
# Story 45.4's apex-only boot semantic (only connector + townhouse-api
|
|
277
|
+
# start at first run).
|
|
278
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
279
|
+
town:
|
|
280
|
+
image: ghcr.io/toon-protocol/town${TOON_TOWN_DIGEST}
|
|
281
|
+
container_name: townhouse-hs-town
|
|
282
|
+
profiles: [town]
|
|
283
|
+
networks:
|
|
284
|
+
- townhouse-hs-net
|
|
285
|
+
depends_on:
|
|
286
|
+
connector:
|
|
287
|
+
condition: service_healthy
|
|
288
|
+
expose: ['3000']
|
|
289
|
+
ports:
|
|
290
|
+
- '127.0.0.1:7100:7100'
|
|
291
|
+
- '127.0.0.1:3100:3100'
|
|
292
|
+
environment:
|
|
293
|
+
# nosemgrep: detect-insecure-websocket -- Docker-internal
|
|
294
|
+
CONNECTOR_URL: ws://connector:3000
|
|
295
|
+
ILP_ADDRESS: g.townhouse.town
|
|
296
|
+
NODE_ID: town
|
|
297
|
+
# MUST equal the apex connector's nodeId (g.townhouse). The child connector
|
|
298
|
+
# keys peerRelations by the auth-declared peerId of the inbound session, so
|
|
299
|
+
# a mismatched value (e.g. the old literal 'apex') means the child never
|
|
300
|
+
# applies the 'parent' relation → it F06-rejects every paid PREPARE the
|
|
301
|
+
# apex forwards ("No payment channel claim attached"). entrypoint-town maps
|
|
302
|
+
# PARENT_PEER_ID → TOON_PARENT_PEER_ID.
|
|
303
|
+
PARENT_PEER_ID: g.townhouse
|
|
304
|
+
# Publish price (ILP base units per event). Default 0; the node-env overlay
|
|
305
|
+
# (assembleNodeEnv) sets it from config.nodes.town.feePerEvent. The town
|
|
306
|
+
# enforces it AND advertises it in kind:10032 (feePerByte).
|
|
307
|
+
FEE_PER_EVENT: ${FEE_PER_EVENT:-0}
|
|
308
|
+
# NIP-40 TTL (seconds) for the town's kind:10032 announcement (issue #261).
|
|
309
|
+
# The town re-publishes at half this interval so a live apex stays fresh
|
|
310
|
+
# while an offline one's announcement expires — clients then skip its
|
|
311
|
+
# unreachable BTP endpoint instead of failing against it. Especially
|
|
312
|
+
# relevant for an HS apex (relay can outlive the BTP hidden service).
|
|
313
|
+
# Default 1h; set 0 to disable the expiration tag + heartbeat.
|
|
314
|
+
TOON_ANNOUNCEMENT_TTL_SECONDS: ${TOON_ANNOUNCEMENT_TTL_SECONDS:-3600}
|
|
315
|
+
# Apex public BTP URL the town advertises in kind:10032 so clients learn
|
|
316
|
+
# where to route packets for g.townhouse.town (.anyone URL or direct).
|
|
317
|
+
# Set by the node-env overlay / boot rebinder; entrypoint-town maps it to
|
|
318
|
+
# TOON_BTP_ENDPOINT.
|
|
319
|
+
PUBLIC_BTP_URL: ${PUBLIC_BTP_URL:-}
|
|
320
|
+
# Public Nostr relay read URL advertised in kind:10032/10166 (HS .anyone
|
|
321
|
+
# relay URL, derived by `hs up` from the relay hidden service). entrypoint
|
|
322
|
+
# maps it to TOON_EXTERNAL_RELAY_URL.
|
|
323
|
+
PUBLIC_RELAY_URL: ${PUBLIC_RELAY_URL:-}
|
|
324
|
+
# Settlement asset advertised in kind:10032 (operator-configurable token).
|
|
325
|
+
ASSET_CODE: ${ASSET_CODE:-}
|
|
326
|
+
ASSET_SCALE: ${ASSET_SCALE:-}
|
|
327
|
+
# Chain selection — driven by the `network` flag (resolveNetworkProfile).
|
|
328
|
+
# EVM_CHAIN is the primary EVM preset name (e.g. base-mainnet) → town
|
|
329
|
+
# TOON_CHAIN; EVM_RPC_URL is the matching RPC. Both come from the node-env
|
|
330
|
+
# overlay (nodes-lifecycle buildNetworkNodeEnv) and ~/.townhouse/compose/.env
|
|
331
|
+
# (env-writer). 'none' makes the town node run relay-only (no settlement).
|
|
332
|
+
TOON_CHAIN: ${EVM_CHAIN:-}
|
|
333
|
+
TOON_RPC_URL: ${EVM_RPC_URL:-}
|
|
334
|
+
# x-only pubkey derived from the node secret at provisioning time and
|
|
335
|
+
# injected by the API (nodes-lifecycle buildNodeEnv → TOWN_NOSTR_PUBKEY).
|
|
336
|
+
# Informational — lets operators / SDK clients read it via `docker inspect`
|
|
337
|
+
# or `node list --json` without re-deriving from the secret (issue #81).
|
|
338
|
+
NODE_NOSTR_PUBKEY: '${TOWN_NOSTR_PUBKEY:-}'
|
|
339
|
+
NODE_EVM_ADDRESS: ''
|
|
340
|
+
# Derived from HD wallet at runtime (Story 45.4 / Epic 46).
|
|
341
|
+
NODE_NOSTR_SECRET_KEY: '${TOWN_SECRET_KEY:-}'
|
|
342
|
+
# Town reads TOON_SETTLEMENT_PRIVATE_KEY (packages/town/src/cli.ts:305),
|
|
343
|
+
# 0x-prefixed 32-byte hex. The API passes the already-0x-prefixed value
|
|
344
|
+
# via TOWN_SETTLEMENT_PRIVATE_KEY. Previously this was wired to the wrong
|
|
345
|
+
# env var name (`SETTLEMENT_PRIVATE_KEY`), so town crash-looped at boot
|
|
346
|
+
# with `TOON_SETTLEMENT_PRIVATE_KEY must be a 0x-prefixed 32-byte hex
|
|
347
|
+
# string` and never reached its /health endpoint — failing the gate
|
|
348
|
+
# at step 5 (healthcheck). Story 46.4 live gate run (Finding N+O,
|
|
349
|
+
# 2026-05-12).
|
|
350
|
+
TOON_SETTLEMENT_PRIVATE_KEY: '${TOWN_SETTLEMENT_PRIVATE_KEY:-}'
|
|
351
|
+
PARENT_EVM_ADDRESS: '${APEX_EVM_ADDRESS:-}'
|
|
352
|
+
TOON_CONNECTOR_LOG_LEVEL: '${TOON_CONNECTOR_LOG_LEVEL:-warn}'
|
|
353
|
+
volumes:
|
|
354
|
+
- townhouse-hs-town-data:/data
|
|
355
|
+
healthcheck:
|
|
356
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3100/health']
|
|
357
|
+
interval: 30s
|
|
358
|
+
timeout: 10s
|
|
359
|
+
retries: 3
|
|
360
|
+
start_period: 10s
|
|
361
|
+
restart: unless-stopped
|
|
362
|
+
|
|
363
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
364
|
+
# Mill — multi-chain swap node (profile: mill)
|
|
365
|
+
# Lazy-provisioned via Epic 46: `townhouse node add mill`
|
|
366
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
367
|
+
mill:
|
|
368
|
+
image: ghcr.io/toon-protocol/mill${TOON_MILL_DIGEST}
|
|
369
|
+
container_name: townhouse-hs-mill
|
|
370
|
+
profiles: [mill]
|
|
371
|
+
networks:
|
|
372
|
+
- townhouse-hs-net
|
|
373
|
+
depends_on:
|
|
374
|
+
connector:
|
|
375
|
+
condition: service_healthy
|
|
376
|
+
expose: ['3000']
|
|
377
|
+
ports:
|
|
378
|
+
- '127.0.0.1:3200:3200'
|
|
379
|
+
environment:
|
|
380
|
+
# nosemgrep: detect-insecure-websocket -- Docker-internal
|
|
381
|
+
CONNECTOR_URL: ws://connector:3000
|
|
382
|
+
# MUST match the route the apex registers for the mill peer
|
|
383
|
+
# (`g.townhouse.<type>` — see nodes-lifecycle.ts). entrypoint-mill maps
|
|
384
|
+
# ILP_ADDRESS → TOON_ILP_ADDRESS → the embedded connector's self-route.
|
|
385
|
+
# WITHOUT this the mill defaults its self-route to `g.toon.mill.<pubkey>`,
|
|
386
|
+
# the apex-forwarded swap PREPARE (`g.townhouse.mill`) misses it, falls
|
|
387
|
+
# through to the up-to-parent route, and the per-packet-claim-service
|
|
388
|
+
# T00-rejects trying to open an outbound channel back to g.townhouse
|
|
389
|
+
# (issue #157). town sets the equivalent ILP_ADDRESS=g.townhouse.town.
|
|
390
|
+
ILP_ADDRESS: g.townhouse.mill
|
|
391
|
+
NODE_ID: mill
|
|
392
|
+
# MUST equal the apex connector's nodeId (g.townhouse) so the mill's
|
|
393
|
+
# embedded connector applies the 'parent' relation to the apex session and
|
|
394
|
+
# treats forwarded swap PREPAREs as free parent traffic. entrypoint-mill
|
|
395
|
+
# reads TOON_PARENT_PEER_ID directly (it does NOT map PARENT_PEER_ID like
|
|
396
|
+
# town does); without it the mill defaults to 'apex' and rejects swaps with
|
|
397
|
+
# T00 "Per-packet claim service not configured".
|
|
398
|
+
TOON_PARENT_PEER_ID: g.townhouse
|
|
399
|
+
FEE_BASIS_POINTS: '0'
|
|
400
|
+
SETTLEMENT_RPC_URL: ${EVM_RPC_URL:-}
|
|
401
|
+
SETTLEMENT_CHAIN_ID: ${EVM_CHAIN_ID:-}
|
|
402
|
+
SETTLEMENT_TOKEN_ADDRESS: ${EVM_USDC_ADDRESS:-}
|
|
403
|
+
SOLANA_RPC_URL: ${SOLANA_RPC_URL:-}
|
|
404
|
+
SOLANA_USDC_MINT: ${SOLANA_USDC_MINT:-}
|
|
405
|
+
# x-only pubkey injected by the API (buildNodeEnv → MILL_NOSTR_PUBKEY).
|
|
406
|
+
# SDK clients need the mill pubkey for streamSwap seal verification —
|
|
407
|
+
# surfacing it here avoids re-deriving from the secret (issue #81).
|
|
408
|
+
NODE_NOSTR_PUBKEY: '${MILL_NOSTR_PUBKEY:-}'
|
|
409
|
+
NODE_EVM_ADDRESS: ''
|
|
410
|
+
# Derived from HD wallet at runtime (Story 45.4 / Epic 46).
|
|
411
|
+
MILL_MNEMONIC: '${MILL_MNEMONIC:-}'
|
|
412
|
+
NODE_NOSTR_SECRET_KEY: '${MILL_SECRET_KEY:-}'
|
|
413
|
+
MILL_CONFIG_PATH: /config/mill.config.json
|
|
414
|
+
MILL_RELAYS: ${MILL_RELAYS:-}
|
|
415
|
+
SETTLEMENT_PRIVATE_KEY: '${MILL_SETTLEMENT_PRIVATE_KEY:-}'
|
|
416
|
+
PARENT_EVM_ADDRESS: '${APEX_EVM_ADDRESS:-}'
|
|
417
|
+
TOON_CONNECTOR_LOG_LEVEL: '${TOON_CONNECTOR_LOG_LEVEL:-warn}'
|
|
418
|
+
volumes:
|
|
419
|
+
# Operator-managed mill config (Epic 46 provisions this on `townhouse node add mill`).
|
|
420
|
+
# TOWNHOUSE_HOME is exported by `townhouse hs up` (see connector volume above).
|
|
421
|
+
- ${TOWNHOUSE_HOME}/mill.config.json:/config/mill.config.json:ro
|
|
422
|
+
- townhouse-hs-mill-data:/data
|
|
423
|
+
healthcheck:
|
|
424
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3200/health']
|
|
425
|
+
interval: 30s
|
|
426
|
+
timeout: 10s
|
|
427
|
+
retries: 3
|
|
428
|
+
start_period: 15s
|
|
429
|
+
restart: unless-stopped
|
|
430
|
+
|
|
431
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
432
|
+
# DVM — Arweave upload DVM (profile: dvm)
|
|
433
|
+
# Lazy-provisioned via Epic 46: `townhouse node add dvm`
|
|
434
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
435
|
+
dvm:
|
|
436
|
+
image: ghcr.io/toon-protocol/dvm${TOON_DVM_DIGEST}
|
|
437
|
+
container_name: townhouse-hs-dvm
|
|
438
|
+
profiles: [dvm]
|
|
439
|
+
networks:
|
|
440
|
+
- townhouse-hs-net
|
|
441
|
+
depends_on:
|
|
442
|
+
connector:
|
|
443
|
+
condition: service_healthy
|
|
444
|
+
expose: ['3300']
|
|
445
|
+
ports:
|
|
446
|
+
- '127.0.0.1:3400:3400'
|
|
447
|
+
environment:
|
|
448
|
+
# nosemgrep: detect-insecure-websocket -- Docker-internal
|
|
449
|
+
CONNECTOR_URL: ws://connector:3000
|
|
450
|
+
FEE_PER_JOB: '0'
|
|
451
|
+
DVM_KIND: '5094'
|
|
452
|
+
# x-only pubkey injected by the API (buildNodeEnv → DVM_NOSTR_PUBKEY).
|
|
453
|
+
# Informational — readable via `docker inspect` / `node list --json`
|
|
454
|
+
# without re-deriving from the secret (issue #81).
|
|
455
|
+
NODE_NOSTR_PUBKEY: '${DVM_NOSTR_PUBKEY:-}'
|
|
456
|
+
NODE_EVM_ADDRESS: ''
|
|
457
|
+
# Derived from HD wallet at runtime (Story 45.4 / Epic 46).
|
|
458
|
+
NODE_NOSTR_SECRET_KEY: '${DVM_SECRET_KEY:-}'
|
|
459
|
+
TURBO_TOKEN: ${TURBO_TOKEN:-}
|
|
460
|
+
volumes:
|
|
461
|
+
- townhouse-hs-dvm-data:/data
|
|
462
|
+
healthcheck:
|
|
463
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3400/health']
|
|
464
|
+
interval: 30s
|
|
465
|
+
timeout: 10s
|
|
466
|
+
retries: 3
|
|
467
|
+
start_period: 10s
|
|
468
|
+
restart: unless-stopped
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_CONNECTOR_IMAGE
|
|
4
|
+
} from "./chunk-MNVIN5XK.js";
|
|
5
|
+
import "./chunk-I2R4CRUX.js";
|
|
6
|
+
|
|
7
|
+
// src/presets/demo.ts
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
var LOCAL_DEVNET_FALLBACK = {
|
|
13
|
+
anvilUrl: "http://localhost:28545",
|
|
14
|
+
solanaUrl: "http://localhost:28899"
|
|
15
|
+
};
|
|
16
|
+
var DEMO_DETERMINISTIC_PASSWORD = "townhouse-demo-INSECURE-do-not-use-in-prod";
|
|
17
|
+
function resolveChainEndpoints(leasesPath) {
|
|
18
|
+
const localEvm = { rpcUrl: LOCAL_DEVNET_FALLBACK.anvilUrl };
|
|
19
|
+
const localSol = { rpcUrl: LOCAL_DEVNET_FALLBACK.solanaUrl };
|
|
20
|
+
if (!leasesPath || !existsSync(leasesPath)) {
|
|
21
|
+
return { source: "local-fallback", evm: localEvm, solana: localSol };
|
|
22
|
+
}
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(readFileSync(leasesPath, "utf-8"));
|
|
26
|
+
} catch {
|
|
27
|
+
return { source: "local-fallback", evm: localEvm, solana: localSol };
|
|
28
|
+
}
|
|
29
|
+
const evmUrl = typeof parsed.anvil?.url === "string" ? parsed.anvil.url : void 0;
|
|
30
|
+
const evmWs = typeof parsed.anvil?.ws_url === "string" ? parsed.anvil.ws_url : void 0;
|
|
31
|
+
const solUrl = typeof parsed.solana?.url === "string" ? parsed.solana.url : void 0;
|
|
32
|
+
const solWs = typeof parsed.solana?.ws_url === "string" ? parsed.solana.ws_url : void 0;
|
|
33
|
+
return {
|
|
34
|
+
source: leasesPath,
|
|
35
|
+
evm: evmUrl ? { rpcUrl: evmUrl, ...evmWs ? { wsUrl: evmWs } : {} } : localEvm,
|
|
36
|
+
solana: solUrl ? { rpcUrl: solUrl, ...solWs ? { wsUrl: solWs } : {} } : localSol
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function defaultLeasesPath() {
|
|
40
|
+
return resolve(process.cwd(), "deploy", "akash", "leases.json");
|
|
41
|
+
}
|
|
42
|
+
function buildDemoConfig(options) {
|
|
43
|
+
const leasesPath = options.leasesPath === null ? void 0 : options.leasesPath ?? defaultLeasesPath();
|
|
44
|
+
const endpoints = resolveChainEndpoints(leasesPath);
|
|
45
|
+
return {
|
|
46
|
+
nodes: {
|
|
47
|
+
town: {
|
|
48
|
+
enabled: true,
|
|
49
|
+
feePerEvent: 0
|
|
50
|
+
},
|
|
51
|
+
mill: {
|
|
52
|
+
enabled: true,
|
|
53
|
+
feeBasisPoints: 0,
|
|
54
|
+
// Demo pair: EVM (Anvil) <-> Solana. The orchestrator does not
|
|
55
|
+
// currently consume mill.chains directly — it round-trips through
|
|
56
|
+
// YAML so the dashboard / future stories can read it.
|
|
57
|
+
chains: {
|
|
58
|
+
evm: {
|
|
59
|
+
rpcUrl: endpoints.evm.rpcUrl,
|
|
60
|
+
...endpoints.evm.wsUrl ? { wsUrl: endpoints.evm.wsUrl } : {}
|
|
61
|
+
},
|
|
62
|
+
solana: {
|
|
63
|
+
rpcUrl: endpoints.solana.rpcUrl,
|
|
64
|
+
...endpoints.solana.wsUrl ? { wsUrl: endpoints.solana.wsUrl } : {}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
pairs: ["EVM<->SOL"]
|
|
68
|
+
},
|
|
69
|
+
dvm: {
|
|
70
|
+
enabled: true,
|
|
71
|
+
feePerJob: 0,
|
|
72
|
+
// Arweave DVM (kind:5094) — frictionless demo pricing. Operators
|
|
73
|
+
// running for real should raise this; entrypoint-dvm.ts treats the
|
|
74
|
+
// value as msats per byte uploaded to Arweave.
|
|
75
|
+
kindPricing: { "5094": 0 }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
wallet: {
|
|
79
|
+
encrypted_path: options.walletPath
|
|
80
|
+
},
|
|
81
|
+
connector: {
|
|
82
|
+
image: DEFAULT_CONNECTOR_IMAGE,
|
|
83
|
+
adminPort: 9401
|
|
84
|
+
},
|
|
85
|
+
transport: {
|
|
86
|
+
// 'direct' for the demo because `townhouse up` doesn't bring up a
|
|
87
|
+
// SOCKS5 sidecar — hs mode would require one at `socks5://127.0.0.1:28050`
|
|
88
|
+
// (provided by the dev-infra stack but not the operator CLI). Switch
|
|
89
|
+
// to 'hs' once the sidecar story lands in townhouse `up`.
|
|
90
|
+
mode: "direct"
|
|
91
|
+
},
|
|
92
|
+
api: {
|
|
93
|
+
port: 9400,
|
|
94
|
+
host: "127.0.0.1"
|
|
95
|
+
},
|
|
96
|
+
logging: {
|
|
97
|
+
level: "info"
|
|
98
|
+
},
|
|
99
|
+
preset: {
|
|
100
|
+
name: "demo",
|
|
101
|
+
// Source recorded so operators can see at-a-glance whether their demo
|
|
102
|
+
// is hitting Akash or local devnets.
|
|
103
|
+
chainEndpointSource: endpoints.source
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function defaultDemoConfigDir() {
|
|
108
|
+
return join(homedir(), ".townhouse");
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
DEMO_DETERMINISTIC_PASSWORD,
|
|
112
|
+
LOCAL_DEVNET_FALLBACK,
|
|
113
|
+
buildDemoConfig,
|
|
114
|
+
defaultDemoConfigDir,
|
|
115
|
+
defaultLeasesPath,
|
|
116
|
+
resolveChainEndpoints
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=demo-UJ37MLCG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/presets/demo.ts"],"sourcesContent":["/**\n * `--preset=demo` configuration (Story D2).\n *\n * Non-interactive preset for the TOON demo: 1 town, 1 mill (EVM<->SOL pair),\n * 1 dvm, ATOR transport ON, all fees zeroed (demo = free). Chain RPC URLs are\n * sourced from `deploy/akash/leases.json` if present, otherwise from local\n * devnet defaults documented in CLAUDE.md (Anvil 28545, Solana 28899).\n *\n * Future presets (test, prod) follow the same shape — see {@link PresetBuilder}.\n *\n * NOTE on schema reach: this preset writes mill chain endpoints into a\n * `chains` field on the mill node config. The orchestrator wiring that\n * forwards these into MILL_CONFIG_JSON is out of scope for D2 — for now the\n * field round-trips through the YAML so future stories (and the dashboard)\n * can read it without re-deriving it from leases.json.\n */\n\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { readFileSync, existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type { TownhouseConfig } from '../config/schema.js';\nimport { DEFAULT_CONNECTOR_IMAGE } from '../constants.js';\n\n/**\n * Preset identifier — keep the union closed so we surface unknown presets at\n * the CLI boundary instead of silently falling through to defaults.\n */\nexport type PresetName = 'demo';\n\n/**\n * Local-devnet fallback URLs (CLAUDE.md \"Townhouse Dev Stack\" 28xxx range).\n * Used when `deploy/akash/leases.json` is absent or unreadable.\n */\nexport const LOCAL_DEVNET_FALLBACK = {\n anvilUrl: 'http://localhost:28545',\n solanaUrl: 'http://localhost:28899',\n} as const;\n\n/**\n * Deterministic-but-clearly-unsafe password used for the demo wallet when the\n * caller passes `--preset=demo --yes` without `--password`. The string\n * embeds the warning so it shows up in any log scrape; production callers\n * MUST supply their own `--password` (AC-D2-6).\n */\nexport const DEMO_DETERMINISTIC_PASSWORD =\n 'townhouse-demo-INSECURE-do-not-use-in-prod';\n\n/**\n * Shape of `deploy/akash/leases.json` that this preset cares about. The full\n * file emitted by `scripts/akash-deploy.sh` has more fields; we only need\n * RPC + WS URLs here, so the type is intentionally narrow + tolerant.\n */\nexport interface AkashLeases {\n anvil?: {\n url?: string;\n host?: string;\n port?: number | string;\n ws_url?: string;\n };\n solana?: {\n url?: string;\n host?: string;\n port?: number | string;\n ws_host?: string;\n ws_port?: number | string;\n ws_url?: string;\n };\n}\n\nexport interface ResolvedChainEndpoints {\n /** Source of truth for traceability — either an absolute leases.json path or 'local-fallback'. */\n source: string;\n evm: { rpcUrl: string; wsUrl?: string };\n solana: { rpcUrl: string; wsUrl?: string };\n}\n\n/**\n * Read `deploy/akash/leases.json` and extract chain endpoints, falling back\n * to local devnet URLs if the file is missing or any field is unusable.\n *\n * The fallback is per-chain: a leases.json that defines anvil but not solana\n * still gets the local Solana URL (and vice-versa). This keeps half-deployed\n * states usable.\n */\nexport function resolveChainEndpoints(\n leasesPath?: string\n): ResolvedChainEndpoints {\n const localEvm = { rpcUrl: LOCAL_DEVNET_FALLBACK.anvilUrl };\n const localSol = { rpcUrl: LOCAL_DEVNET_FALLBACK.solanaUrl };\n\n if (!leasesPath || !existsSync(leasesPath)) {\n return { source: 'local-fallback', evm: localEvm, solana: localSol };\n }\n\n let parsed: AkashLeases;\n try {\n parsed = JSON.parse(readFileSync(leasesPath, 'utf-8')) as AkashLeases;\n } catch {\n // Malformed JSON — better to demo on local devnets than to error out at\n // wizard-bypass time. Caller never blocks on bad leases data.\n return { source: 'local-fallback', evm: localEvm, solana: localSol };\n }\n\n const evmUrl =\n typeof parsed.anvil?.url === 'string' ? parsed.anvil.url : undefined;\n const evmWs =\n typeof parsed.anvil?.ws_url === 'string' ? parsed.anvil.ws_url : undefined;\n const solUrl =\n typeof parsed.solana?.url === 'string' ? parsed.solana.url : undefined;\n const solWs =\n typeof parsed.solana?.ws_url === 'string'\n ? parsed.solana.ws_url\n : undefined;\n\n return {\n source: leasesPath,\n evm: evmUrl\n ? { rpcUrl: evmUrl, ...(evmWs ? { wsUrl: evmWs } : {}) }\n : localEvm,\n solana: solUrl\n ? { rpcUrl: solUrl, ...(solWs ? { wsUrl: solWs } : {}) }\n : localSol,\n };\n}\n\nexport interface BuildDemoConfigOptions {\n /** Absolute path to the wallet file (typically `<configDir>/wallet.enc`). */\n walletPath: string;\n /**\n * Absolute path to `deploy/akash/leases.json`. Defaults to\n * `<repoRoot>/deploy/akash/leases.json` if not provided. Pass `null` to\n * force local-devnet fallback (used in tests and when the file is known\n * to not exist on the operator's machine).\n */\n leasesPath?: string | null;\n}\n\n/**\n * Default location of the Akash leases file. Resolved relative to CWD —\n * the demo CLI is run from anywhere, but the leases.json that matters lives\n * at `<repo>/deploy/akash/leases.json`.\n */\nexport function defaultLeasesPath(): string {\n return resolve(process.cwd(), 'deploy', 'akash', 'leases.json');\n}\n\n/**\n * Build the full TownhouseConfig for `--preset=demo`.\n *\n * AC-D2-5 invariants enforced here:\n * - 1 town, 1 mill, 1 dvm (all enabled)\n * - feePerEvent = 0, feeBasisPoints = 0, feePerJob = 0\n * - transport.mode = 'direct'\n * - mill.chains contains exactly one EVM<->SOL pair\n */\nexport function buildDemoConfig(\n options: BuildDemoConfigOptions\n): TownhouseConfig {\n const leasesPath =\n options.leasesPath === null\n ? undefined\n : (options.leasesPath ?? defaultLeasesPath());\n\n const endpoints = resolveChainEndpoints(leasesPath);\n\n return {\n nodes: {\n town: {\n enabled: true,\n feePerEvent: 0,\n },\n mill: {\n enabled: true,\n feeBasisPoints: 0,\n // Demo pair: EVM (Anvil) <-> Solana. The orchestrator does not\n // currently consume mill.chains directly — it round-trips through\n // YAML so the dashboard / future stories can read it.\n chains: {\n evm: {\n rpcUrl: endpoints.evm.rpcUrl,\n ...(endpoints.evm.wsUrl ? { wsUrl: endpoints.evm.wsUrl } : {}),\n },\n solana: {\n rpcUrl: endpoints.solana.rpcUrl,\n ...(endpoints.solana.wsUrl\n ? { wsUrl: endpoints.solana.wsUrl }\n : {}),\n },\n },\n pairs: ['EVM<->SOL'],\n },\n dvm: {\n enabled: true,\n feePerJob: 0,\n // Arweave DVM (kind:5094) — frictionless demo pricing. Operators\n // running for real should raise this; entrypoint-dvm.ts treats the\n // value as msats per byte uploaded to Arweave.\n kindPricing: { '5094': 0 },\n },\n },\n wallet: {\n encrypted_path: options.walletPath,\n },\n connector: {\n image: DEFAULT_CONNECTOR_IMAGE,\n adminPort: 9401,\n },\n transport: {\n // 'direct' for the demo because `townhouse up` doesn't bring up a\n // SOCKS5 sidecar — hs mode would require one at `socks5://127.0.0.1:28050`\n // (provided by the dev-infra stack but not the operator CLI). Switch\n // to 'hs' once the sidecar story lands in townhouse `up`.\n mode: 'direct',\n },\n api: {\n port: 9400,\n host: '127.0.0.1',\n },\n logging: {\n level: 'info',\n },\n preset: {\n name: 'demo',\n // Source recorded so operators can see at-a-glance whether their demo\n // is hitting Akash or local devnets.\n chainEndpointSource: endpoints.source,\n },\n };\n}\n\n/**\n * Default config dir used by the demo preset when `--config-dir` is omitted.\n * Mirrors the value in cli.ts; duplicated here so tests can construct the\n * same path without importing from the CLI surface.\n */\nexport function defaultDemoConfigDir(): string {\n return join(homedir(), '.townhouse');\n}\n"],"mappings":";;;;;;;AAiBA,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,cAAc,kBAAkB;AACzC,SAAS,eAAe;AAejB,IAAM,wBAAwB;AAAA,EACnC,UAAU;AAAA,EACV,WAAW;AACb;AAQO,IAAM,8BACX;AAuCK,SAAS,sBACd,YACwB;AACxB,QAAM,WAAW,EAAE,QAAQ,sBAAsB,SAAS;AAC1D,QAAM,WAAW,EAAE,QAAQ,sBAAsB,UAAU;AAE3D,MAAI,CAAC,cAAc,CAAC,WAAW,UAAU,GAAG;AAC1C,WAAO,EAAE,QAAQ,kBAAkB,KAAK,UAAU,QAAQ,SAAS;AAAA,EACrE;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AAAA,EACvD,QAAQ;AAGN,WAAO,EAAE,QAAQ,kBAAkB,KAAK,UAAU,QAAQ,SAAS;AAAA,EACrE;AAEA,QAAM,SACJ,OAAO,OAAO,OAAO,QAAQ,WAAW,OAAO,MAAM,MAAM;AAC7D,QAAM,QACJ,OAAO,OAAO,OAAO,WAAW,WAAW,OAAO,MAAM,SAAS;AACnE,QAAM,SACJ,OAAO,OAAO,QAAQ,QAAQ,WAAW,OAAO,OAAO,MAAM;AAC/D,QAAM,QACJ,OAAO,OAAO,QAAQ,WAAW,WAC7B,OAAO,OAAO,SACd;AAEN,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,KAAK,SACD,EAAE,QAAQ,QAAQ,GAAI,QAAQ,EAAE,OAAO,MAAM,IAAI,CAAC,EAAG,IACrD;AAAA,IACJ,QAAQ,SACJ,EAAE,QAAQ,QAAQ,GAAI,QAAQ,EAAE,OAAO,MAAM,IAAI,CAAC,EAAG,IACrD;AAAA,EACN;AACF;AAmBO,SAAS,oBAA4B;AAC1C,SAAO,QAAQ,QAAQ,IAAI,GAAG,UAAU,SAAS,aAAa;AAChE;AAWO,SAAS,gBACd,SACiB;AACjB,QAAM,aACJ,QAAQ,eAAe,OACnB,SACC,QAAQ,cAAc,kBAAkB;AAE/C,QAAM,YAAY,sBAAsB,UAAU;AAElD,SAAO;AAAA,IACL,OAAO;AAAA,MACL,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,gBAAgB;AAAA;AAAA;AAAA;AAAA,QAIhB,QAAQ;AAAA,UACN,KAAK;AAAA,YACH,QAAQ,UAAU,IAAI;AAAA,YACtB,GAAI,UAAU,IAAI,QAAQ,EAAE,OAAO,UAAU,IAAI,MAAM,IAAI,CAAC;AAAA,UAC9D;AAAA,UACA,QAAQ;AAAA,YACN,QAAQ,UAAU,OAAO;AAAA,YACzB,GAAI,UAAU,OAAO,QACjB,EAAE,OAAO,UAAU,OAAO,MAAM,IAChC,CAAC;AAAA,UACP;AAAA,QACF;AAAA,QACA,OAAO,CAAC,WAAW;AAAA,MACrB;AAAA,MACA,KAAK;AAAA,QACH,SAAS;AAAA,QACT,WAAW;AAAA;AAAA;AAAA;AAAA,QAIX,aAAa,EAAE,QAAQ,EAAE;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,gBAAgB,QAAQ;AAAA,IAC1B;AAAA,IACA,WAAW;AAAA,MACT,OAAO;AAAA,MACP,WAAW;AAAA,IACb;AAAA,IACA,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,MAKT,MAAM;AAAA,IACR;AAAA,IACA,KAAK;AAAA,MACH,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AAAA,IACA,SAAS;AAAA,MACP,OAAO;AAAA,IACT;AAAA,IACA,QAAQ;AAAA,MACN,MAAM;AAAA;AAAA;AAAA,MAGN,qBAAqB,UAAU;AAAA,IACjC;AAAA,EACF;AACF;AAOO,SAAS,uBAA+B;AAC7C,SAAO,KAAK,QAAQ,GAAG,YAAY;AACrC;","names":[]}
|