@toon-protocol/townhouse 0.1.0-rc5
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/LICENSE +190 -0
- package/README.md +386 -0
- package/dist/chunk-IB6TNCUQ.js +8274 -0
- package/dist/chunk-IB6TNCUQ.js.map +1 -0
- package/dist/chunk-UTFWPLTB.js +59 -0
- package/dist/chunk-UTFWPLTB.js.map +1 -0
- package/dist/cli.d.ts +38 -0
- package/dist/cli.js +684 -0
- package/dist/cli.js.map +1 -0
- package/dist/compose/townhouse-dev.yml +406 -0
- package/dist/compose/townhouse-hs.yml +276 -0
- package/dist/demo-MJR47QHZ.js +117 -0
- package/dist/demo-MJR47QHZ.js.map +1 -0
- package/dist/image-manifest.json +32 -0
- package/dist/index.d.ts +1410 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/cli/browser-opener.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * CLI entrypoint for `@toon-protocol/townhouse` (Story 21.1).\n *\n * Subcommands: init, up, down, status, --help\n *\n * Usage:\n * townhouse init [--force]\n * townhouse up\n * townhouse down\n * townhouse status\n */\n\nimport { parseArgs } from 'node:util';\nimport { mkdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { homedir } from 'node:os';\nimport { pathToFileURL } from 'node:url';\nimport { stringify } from 'yaml';\nimport Docker from 'dockerode';\n\nimport { getDefaultConfig } from './config/defaults.js';\nimport { loadConfig } from './config/loader.js';\nimport type { TownhouseConfig } from './config/schema.js';\nimport { DockerOrchestrator } from './docker/index.js';\nimport type { NodeType } from './docker/types.js';\nimport {\n ConnectorAdminClient,\n TransportProbe,\n DEFAULT_ATOR_PROXY,\n} from './connector/index.js';\nimport { createApiServer } from './api/server.js';\nimport { createWizardApiServer } from './api/wizard-server.js';\nimport type { ApiServer } from './api/index.js';\nimport { RealBrowserOpener } from './cli/browser-opener.js';\nimport type { BrowserOpener } from './cli/browser-opener.js';\nimport {\n WalletManager,\n encryptWallet,\n decryptWallet,\n saveWallet,\n loadWallet,\n} from './wallet/index.js';\n\n/**\n * Error thrown when `main()` is invoked with `--help`. Callers (tests) can\n * distinguish this from genuine failures; the top-level entrypoint catches\n * it and exits 0.\n */\nexport class CliHelpRequested extends Error {\n constructor() {\n super(HELP_TEXT);\n this.name = 'CliHelpRequested';\n }\n}\n\nconst HELP_TEXT = `townhouse — TOON node orchestrator\n\nUsage:\n townhouse setup [--no-browser] [--port <n>] [--config-dir <dir>] Run the first-run setup wizard\n townhouse init [--force] [--config-dir <dir>] [--password <pw>] [--preset <name>] [--yes] Initialize config + wallet\n townhouse up [--town] [--mill] [--dvm] [-c <path>] [--password <pw>] Start nodes\n townhouse down [-c <path>] Stop all nodes\n townhouse status [-c <path>] Show node status\n townhouse metrics [-c <path>] Show connector metrics\n townhouse wallet show [-c <path>] [--password <pw>] Show derived addresses\n townhouse --help Show this help\n\nFlags:\n --town Start Town (Nostr relay) node\n --mill Start Mill (swap) node\n --dvm Start DVM (compute) node\n --password Wallet password (non-interactive mode)\n --no-browser Skip opening the browser automatically (setup command)\n --port Override the API port (setup command, default 9400)\n --preset Init from a named preset (init only). Supported: demo\n --yes Non-interactive (init only); with --preset=demo uses demo password if --password absent\n If no flags given, starts all enabled nodes from config.`;\n\nconst DEFAULT_CONFIG_DIR = join(homedir(), '.townhouse');\nconst DEFAULT_CONFIG_PATH = join(DEFAULT_CONFIG_DIR, 'config.yaml');\n\nasync function handleInit(\n force: boolean,\n configDir?: string,\n password?: string,\n preset?: 'demo',\n yes?: boolean\n): Promise<void> {\n const dir = resolve(configDir ?? DEFAULT_CONFIG_DIR);\n const configPath = join(dir, 'config.yaml');\n\n if (existsSync(configPath) && !force) {\n console.error(\n `Config already exists at ${configPath}. Use --force to overwrite.`\n );\n process.exitCode = 1;\n return;\n }\n\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n\n // D2 — preset path takes precedence over default config for non-interactive\n // demo init. Preset writes the same TownhouseConfig shape, so the rest of\n // the init flow (wallet generation, etc.) is unaffected.\n let configToWrite;\n if (preset === 'demo') {\n const { buildDemoConfig, DEMO_DETERMINISTIC_PASSWORD } =\n await import('./presets/demo.js');\n configToWrite = buildDemoConfig({ walletPath: join(dir, 'wallet.enc') });\n // AC-D2-6: --yes without --password under --preset=demo gets the\n // deterministic demo password. Documented as DEMO ONLY.\n if (yes && !password) {\n password = DEMO_DETERMINISTIC_PASSWORD;\n console.log(\n '[demo preset] Using deterministic demo password (insecure — demo only).'\n );\n }\n } else {\n configToWrite = getDefaultConfig();\n // Override wallet path to use the config dir, not the default home-dir path.\n // getDefaultConfig() hardcodes ~/.townhouse/wallet.enc; tests and non-default\n // config dirs need the wallet collocated with config.yaml.\n configToWrite.wallet.encrypted_path = join(dir, 'wallet.enc');\n }\n const yamlContent = stringify(configToWrite);\n writeFileSync(configPath, yamlContent, {\n encoding: 'utf-8',\n mode: 0o600,\n });\n\n console.log(`Config created at ${configPath}`);\n\n // Generate wallet — use config dir for wallet path (overrides default home dir path)\n const walletPath = join(dir, 'wallet.enc');\n if (existsSync(walletPath) && !force) {\n console.log(\n `Wallet already exists at ${walletPath}. Skipping wallet generation.`\n );\n return;\n }\n\n const walletPassword = password ?? process.env['TOWNHOUSE_WALLET_PASSWORD'];\n if (!walletPassword) {\n console.error(\n 'Wallet password required. Use --password flag or TOWNHOUSE_WALLET_PASSWORD env var.'\n );\n process.exitCode = 1;\n return;\n }\n\n const walletManager = new WalletManager({ encryptedPath: walletPath });\n const { mnemonic } = await walletManager.generate();\n\n // Display mnemonic ONCE for backup — this is the only place it ever appears\n console.log('');\n console.log('=== IMPORTANT: Back up your seed phrase ===');\n console.log('');\n console.log(` ${mnemonic}`);\n console.log('');\n console.log('This is the ONLY time your seed phrase will be shown.');\n console.log('Store it safely. You will need it to recover your node keys.');\n console.log('============================================');\n console.log('');\n\n // Encrypt and save — mnemonic reference is not stored beyond this block\n const encrypted = encryptWallet(mnemonic, walletPassword);\n await saveWallet(walletPath, encrypted);\n console.log(`Wallet saved to ${walletPath}`);\n\n // Display derived addresses\n console.log('');\n console.log('Derived Node Addresses:');\n console.log('-----------------------');\n const allKeys = walletManager.getAllKeys();\n for (const info of allKeys) {\n console.log(` ${info.nodeType.padEnd(6)} Nostr: ${info.nostrPubkey}`);\n console.log(` ${''.padEnd(6)} EVM: ${info.evmAddress}`);\n }\n\n // Zero key material\n walletManager.lock();\n}\n\nasync function handleSetup(\n configDir: string | undefined,\n port: number,\n noBrowser: boolean,\n dockerInstance?: Docker,\n browserOpener?: BrowserOpener\n): Promise<void> {\n const dir = resolve(configDir ?? DEFAULT_CONFIG_DIR);\n const configPath = join(dir, 'config.yaml');\n const walletPath = join(dir, 'wallet.enc');\n\n // Short-circuit only when BOTH the config and the wallet exist — a config\n // without a wallet would land the operator in a circular dead-end (setup\n // says \"already initialized → run `townhouse up`\", up then errors that the\n // wallet is missing). Guide them to clean up and re-run setup instead.\n if (existsSync(configPath) && existsSync(walletPath)) {\n console.log('Already initialized — run `townhouse up` to start your nodes');\n return;\n }\n if (existsSync(configPath) && !existsSync(walletPath)) {\n console.error(\n `Found ${configPath} but no wallet at ${walletPath}.\\n` +\n `Delete the orphan config and re-run \\`townhouse setup\\`, or restore the wallet from backup.`\n );\n process.exitCode = 1;\n return;\n }\n\n const docker = dockerInstance ?? new Docker();\n const opener = browserOpener ?? new RealBrowserOpener();\n\n const wizardServer = await createWizardApiServer({\n configDir: dir,\n configPath,\n walletPath,\n port,\n docker,\n });\n\n const url = `http://127.0.0.1:${port}/wizard`;\n\n try {\n await wizardServer.app.listen({ host: '127.0.0.1', port });\n } catch (err: unknown) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === 'EADDRINUSE') {\n console.error(\n `Port ${port} is already in use. Pass \\`--port <n>\\` to choose a different port.`\n );\n process.exitCode = 1;\n try {\n await wizardServer.close();\n } catch {\n /* best-effort */\n }\n return;\n }\n throw err;\n }\n console.log(`Wizard ready at ${url}`);\n\n if (!noBrowser) {\n await opener.open(url);\n }\n\n // Wire signal handlers via process.once so they self-remove after firing\n // — prevents listener leaks when tests call main(['setup', ...]) repeatedly.\n // The Fastify server keeps the process alive after handleSetup returns;\n // signals trigger graceful close.\n let shuttingDown = false;\n const shutdown = async (sig: string) => {\n if (shuttingDown) return;\n shuttingDown = true;\n console.log(`\\nReceived ${sig}, shutting down...`);\n try {\n await wizardServer.close();\n } catch {\n /* best-effort */\n }\n process.exit(0);\n };\n process.once('SIGINT', () => {\n void shutdown('SIGINT');\n });\n process.once('SIGTERM', () => {\n void shutdown('SIGTERM');\n });\n}\n\nasync function handleWalletShow(\n config: TownhouseConfig,\n password?: string\n): Promise<void> {\n const walletPath = config.wallet.encrypted_path;\n const result = await loadWallet(walletPath);\n\n if (!result) {\n console.error('No wallet found. Run `townhouse init` first.');\n process.exitCode = 1;\n return;\n }\n\n if (result.permissionsWarning) {\n console.error(result.permissionsWarning);\n }\n\n const walletPassword = password ?? process.env['TOWNHOUSE_WALLET_PASSWORD'];\n if (!walletPassword) {\n console.error(\n 'Wallet password required. Use --password flag or TOWNHOUSE_WALLET_PASSWORD env var.'\n );\n process.exitCode = 1;\n return;\n }\n\n const walletManager = new WalletManager({ encryptedPath: walletPath });\n try {\n // Decrypt mnemonic in minimal scope — fromMnemonic derives keys then\n // the mnemonic string becomes unreachable (eligible for GC)\n await walletManager.fromMnemonic(\n decryptWallet(result.wallet, walletPassword)\n );\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to decrypt wallet: ${msg}`);\n process.exitCode = 1;\n return;\n }\n\n console.log(\n 'Node Type | Nostr Pubkey | EVM Address | Derivation Path'\n );\n console.log(\n '-----------|------------------------------------------------------------------|--------------------------------------------|--------------------------'\n );\n const allKeys = walletManager.getAllKeys();\n for (const info of allKeys) {\n console.log(\n `${info.nodeType.padEnd(10)} | ${info.nostrPubkey} | ${info.evmAddress} | ${info.nostrDerivationPath}`\n );\n }\n\n // Zero key material immediately after display\n walletManager.lock();\n}\n\nasync function handleStatus(\n docker: Docker,\n config: TownhouseConfig\n): Promise<void> {\n const orchestrator = new DockerOrchestrator(docker, config);\n const statuses = await orchestrator.status();\n\n console.log('Node Status:');\n console.log('------------');\n for (const s of statuses) {\n const health = s.health ? ` (${s.health})` : '';\n console.log(` ${s.name.padEnd(12)} ${s.state}${health}`);\n }\n\n const connectorHs = config.transport.hiddenService;\n const relayHs = config.transport.relayHiddenService;\n if (\n config.transport.mode === 'ator' ||\n connectorHs?.externalUrl ||\n relayHs?.externalUrl ||\n config.transport.externalUrl\n ) {\n console.log('');\n console.log('Hidden Services:');\n console.log('----------------');\n const connectorUrl =\n connectorHs?.externalUrl ?? config.transport.externalUrl;\n if (connectorUrl) {\n console.log(` Connector (BTP): ${connectorUrl}`);\n }\n if (relayHs?.externalUrl) {\n console.log(` Relay (Nostr): ${relayHs.externalUrl}`);\n }\n if (!connectorUrl && !relayHs?.externalUrl) {\n console.log(' (ator mode set but no externalUrl configured)');\n }\n }\n\n // Try to include connector metrics (graceful degradation)\n try {\n const adminClient = new ConnectorAdminClient(\n `http://127.0.0.1:${config.connector.adminPort}`\n );\n const metrics = await adminClient.getMetrics();\n const peers = await adminClient.getPeers();\n const activePeers = peers.filter((p) => p.connected).length;\n\n console.log('');\n console.log('Connector Metrics:');\n console.log('------------------');\n console.log(` Packets forwarded: ${metrics.aggregate.packetsForwarded}`);\n console.log(` Active peers: ${activePeers}/${peers.length}`);\n } catch {\n console.log('');\n console.log('Connector Metrics: unavailable');\n }\n}\n\nasync function handleMetrics(config: TownhouseConfig): Promise<void> {\n const adminClient = new ConnectorAdminClient(\n `http://127.0.0.1:${config.connector.adminPort}`\n );\n\n try {\n const metrics = await adminClient.getMetrics();\n const peers = await adminClient.getPeers();\n\n // Per-peer packet counters live on /admin/metrics.json (peers[]),\n // not /admin/peers — index by peerId so we can show counts inline.\n const peerMetrics = new Map(metrics.peers.map((p) => [p.peerId, p]));\n\n console.log('Connector Metrics:');\n console.log('------------------');\n console.log(` Packets forwarded: ${metrics.aggregate.packetsForwarded}`);\n console.log(` Packets rejected: ${metrics.aggregate.packetsRejected}`);\n console.log(` Bytes sent: ${metrics.aggregate.bytesSent}`);\n console.log('');\n console.log('Peers:');\n console.log('------');\n if (peers.length === 0) {\n console.log(' No peers connected');\n } else {\n for (const peer of peers) {\n const status = peer.connected ? 'connected' : 'disconnected';\n const packets = peerMetrics.get(peer.id)?.packetsForwarded ?? 0;\n console.log(` ${peer.id.padEnd(12)} ${status} (${packets} packets)`);\n }\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Failed to fetch connector metrics: ${msg}`);\n process.exitCode = 1;\n }\n}\n\n/**\n * Determine which node profiles to start based on CLI flags and config.\n * If explicit flags (--town, --mill, --dvm) are provided, use those.\n * Otherwise fall back to all enabled nodes from config.\n */\nfunction resolveProfiles(\n values: Record<string, unknown>,\n config: TownhouseConfig\n): NodeType[] {\n const explicitFlags: NodeType[] = [];\n if (values['town']) explicitFlags.push('town');\n if (values['mill']) explicitFlags.push('mill');\n if (values['dvm']) explicitFlags.push('dvm');\n\n if (explicitFlags.length > 0) {\n return explicitFlags;\n }\n\n // No explicit flags — start all enabled nodes from config\n const enabled: NodeType[] = [];\n if (config.nodes.town.enabled) enabled.push('town');\n if (config.nodes.mill.enabled) enabled.push('mill');\n if (config.nodes.dvm.enabled) enabled.push('dvm');\n return enabled;\n}\n\nasync function handleUp(\n configPath: string,\n config: TownhouseConfig,\n profiles: NodeType[],\n docker: Docker,\n password?: string,\n dryRun = false\n): Promise<void> {\n if (profiles.length === 0) {\n console.log(\n 'No nodes enabled in config. Enable nodes in config.yaml first.'\n );\n return;\n }\n\n // Initialize wallet (Round-2 Decision D1:c).\n // The API's GET /wallet depends on an unlocked wallet. If a wallet file\n // exists on disk it MUST be unlockable (fail-fast on bad password). If no\n // wallet file exists, we log a warning and skip API startup entirely so\n // orchestration-only callers (CI, tooling, smoke tests) still work.\n const walletPath = config.wallet.encrypted_path;\n let walletManager: WalletManager | undefined;\n if (!existsSync(walletPath)) {\n console.error(\n `Wallet not found at ${walletPath}. Run \\`townhouse setup\\` first (or restore your wallet backup).`\n );\n process.exitCode = 1;\n return;\n } else {\n const walletPassword = password ?? process.env['TOWNHOUSE_WALLET_PASSWORD'];\n if (!walletPassword) {\n throw new Error(\n 'Wallet password required to start the API. Use --password flag or TOWNHOUSE_WALLET_PASSWORD env var.'\n );\n }\n const loaded = await loadWallet(walletPath);\n if (!loaded) {\n throw new Error(`Wallet at ${walletPath} could not be read.`);\n }\n if (loaded.permissionsWarning) {\n console.error(loaded.permissionsWarning);\n }\n walletManager = new WalletManager({ encryptedPath: walletPath });\n try {\n await walletManager.fromMnemonic(\n decryptWallet(loaded.wallet, walletPassword)\n );\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to decrypt wallet: ${msg}`);\n }\n }\n\n const orchestrator = new DockerOrchestrator(docker, config, walletManager);\n\n // Wire up progress reporting\n orchestrator.on(\n 'containerState',\n (event: { name: string; state: string }) => {\n console.log(` ${event.name}: ${event.state}`);\n }\n );\n orchestrator.on(\n 'pullProgress',\n (event: { image: string; status: string; progress?: string }) => {\n const progress = event.progress ? ` ${event.progress}` : '';\n console.log(` [pull] ${event.image}: ${event.status}${progress}`);\n }\n );\n\n // API server reference for graceful shutdown\n let apiServer: ApiServer | undefined;\n\n // Register SIGINT handler for graceful shutdown\n const sigintHandler = async () => {\n console.log('\\nReceived SIGINT, shutting down gracefully...');\n\n // Close API server first\n if (apiServer) {\n try {\n await apiServer.close();\n } catch {\n // Best-effort\n }\n }\n\n // Then stop containers\n try {\n await orchestrator.down();\n } catch {\n // Best-effort cleanup\n }\n process.exit(0);\n };\n process.on('SIGINT', sigintHandler);\n\n // For SIGTERM\n const sigtermHandler = async () => {\n console.log('\\nReceived SIGTERM, shutting down gracefully...');\n\n if (apiServer) {\n try {\n await apiServer.close();\n } catch {\n // Best-effort\n }\n }\n\n try {\n await orchestrator.down();\n } catch {\n // Best-effort cleanup\n }\n process.exit(0);\n };\n process.on('SIGTERM', sigtermHandler);\n\n // Track if the server started successfully (handlers stay registered if true)\n let serverStarted = false;\n\n if (\n profiles.includes('dvm') &&\n config.nodes.dvm.enabled &&\n !process.env['TURBO_TOKEN']\n ) {\n console.warn(\n '[townhouse] WARN: TURBO_TOKEN is not set — Arweave DVM (kind:5094) uploads will fail at first job.'\n );\n console.warn(\n '[townhouse] Export TURBO_TOKEN=<arweave-jwk-json> before `townhouse up` to enable uploads.'\n );\n }\n\n try {\n console.log(`Starting nodes: ${profiles.join(', ')}...`);\n if (!dryRun) {\n await orchestrator.up(profiles);\n console.log('All nodes started successfully.');\n } else {\n console.log('[dry-run] Skipped orchestrator.up()');\n }\n\n // Start API server after nodes are up\n if (walletManager) {\n const connectorAdmin = new ConnectorAdminClient(\n `http://127.0.0.1:${config.connector.adminPort}`\n );\n\n const transportProbe = new TransportProbe({\n proxyUrl:\n config.transport.mode === 'ator'\n ? (config.transport.socksProxy ?? DEFAULT_ATOR_PROXY)\n : '',\n });\n if (config.transport.mode === 'ator') {\n transportProbe.start();\n }\n\n const apiDeps = {\n configPath,\n config,\n orchestrator,\n wallet: walletManager,\n connectorAdmin,\n transportProbe,\n };\n\n apiServer = await createApiServer(apiDeps);\n\n const { host, port } = config.api;\n if (!dryRun) {\n await apiServer.app.listen({\n host: host ?? '127.0.0.1',\n port: port ?? 9400,\n });\n serverStarted = true;\n\n console.log(\n `\\n[Townhouse API] listening on http://${host ?? '127.0.0.1'}:${port ?? 9400}`\n );\n console.log(\n ' GET /nodes, GET /nodes/:type, PATCH /nodes/:type/config, GET /wallet, WS /metrics'\n );\n } else {\n // Log a structured summary for the dry-run smoke test (Task 8.3).\n console.log(\n `[dry-run] API factory invoked: configPath=${configPath} host=${host ?? '127.0.0.1'} port=${port ?? 9400} connectorAdmin=http://127.0.0.1:${config.connector.adminPort} wallet=WalletManager`\n );\n await apiServer.close();\n apiServer = undefined;\n }\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n if (\n msg.includes('Docker is not running') ||\n msg.includes('ENOENT') ||\n msg.includes('ECONNREFUSED') ||\n msg.includes('socket')\n ) {\n throw new Error(\n `Docker is not available. Please ensure Docker is running and try again. (${msg})`\n );\n }\n throw error;\n } finally {\n // Only remove signal handlers if server never started\n // If server is running, handlers enable graceful shutdown on SIGTERM/SIGINT\n if (!serverStarted) {\n process.removeListener('SIGINT', sigintHandler);\n process.removeListener('SIGTERM', sigtermHandler);\n }\n }\n}\n\nasync function handleDown(\n config: TownhouseConfig,\n docker: Docker\n): Promise<void> {\n const orchestrator = new DockerOrchestrator(docker, config);\n\n orchestrator.on(\n 'containerState',\n (event: { name: string; state: string }) => {\n console.log(` ${event.name}: ${event.state}`);\n }\n );\n\n console.log('Stopping nodes...');\n await orchestrator.down();\n console.log('All nodes stopped.');\n}\n\n/**\n * Main CLI entry — exported for testability (same pattern as Mill CLI).\n * Accepts optional dockerode instance for dependency injection in tests.\n */\nexport async function main(\n argv: string[],\n dockerInstance?: Docker,\n browserOpener?: BrowserOpener\n): Promise<void> {\n const { values, positionals } = parseArgs({\n args: argv,\n options: {\n help: { type: 'boolean' },\n force: { type: 'boolean' },\n config: { type: 'string', short: 'c' },\n 'config-dir': { type: 'string' },\n town: { type: 'boolean' },\n mill: { type: 'boolean' },\n dvm: { type: 'boolean' },\n password: { type: 'string' },\n 'dry-run': { type: 'boolean' },\n 'no-browser': { type: 'boolean' },\n port: { type: 'string' },\n preset: { type: 'string' },\n yes: { type: 'boolean' },\n },\n strict: false,\n allowPositionals: true,\n });\n\n if (values.help) {\n console.log(HELP_TEXT);\n throw new CliHelpRequested();\n }\n\n const command = positionals[0];\n\n if (!command) {\n console.log(HELP_TEXT);\n throw new CliHelpRequested();\n }\n\n switch (command) {\n case 'setup': {\n const portStr = values['port'] as string | undefined;\n // Reject trailing junk like \"9400foo\" (parseInt would silently accept).\n const port = portStr ? Number(portStr) : 9400;\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n console.error('--port must be an integer between 1 and 65535');\n process.exitCode = 1;\n break;\n }\n await handleSetup(\n values['config-dir'] as string | undefined,\n port,\n values['no-browser'] === true,\n dockerInstance,\n browserOpener\n );\n break;\n }\n case 'init': {\n const presetVal = values.preset as string | undefined;\n if (presetVal !== undefined && presetVal !== 'demo') {\n console.error(`Unknown preset: ${presetVal}. Supported: demo`);\n process.exitCode = 1;\n break;\n }\n await handleInit(\n values.force === true,\n values['config-dir'] as string | undefined,\n values.password as string | undefined,\n presetVal,\n values.yes === true\n );\n break;\n }\n case 'wallet': {\n const subCommand = positionals[1];\n if (subCommand === 'show') {\n const configPath = (values.config as string) ?? DEFAULT_CONFIG_PATH;\n const config = loadConfig(configPath);\n await handleWalletShow(config, values.password as string | undefined);\n } else {\n console.error(\n 'Usage: townhouse wallet show [-c <path>] [--password <pw>]'\n );\n process.exitCode = 1;\n }\n break;\n }\n case 'status': {\n const configPath = (values.config as string) ?? DEFAULT_CONFIG_PATH;\n const config = loadConfig(configPath);\n const docker = dockerInstance ?? new Docker();\n await handleStatus(docker, config);\n break;\n }\n case 'up': {\n const configPath = (values.config as string) ?? DEFAULT_CONFIG_PATH;\n const config = loadConfig(configPath);\n const docker = dockerInstance ?? new Docker();\n const profiles = resolveProfiles(values, config);\n await handleUp(\n configPath,\n config,\n profiles,\n docker,\n values.password as string | undefined,\n values['dry-run'] === true\n );\n break;\n }\n case 'down': {\n const configPath = (values.config as string) ?? DEFAULT_CONFIG_PATH;\n const config = loadConfig(configPath);\n const docker = dockerInstance ?? new Docker();\n await handleDown(config, docker);\n break;\n }\n case 'metrics': {\n const configPath = (values.config as string) ?? DEFAULT_CONFIG_PATH;\n const config = loadConfig(configPath);\n await handleMetrics(config);\n break;\n }\n default: {\n // Sanitize user input to prevent log injection (CWE-117)\n // eslint-disable-next-line no-control-regex\n const sanitized = command.replace(/[\\x00-\\x1f\\x7f]/g, '');\n console.error(`Unknown command: ${sanitized}`);\n console.log(HELP_TEXT);\n process.exitCode = 1;\n }\n }\n}\n\n// Self-invoke when run as entrypoint.\nconst invokedFile = process.argv[1];\nconst invokedDirectly =\n typeof invokedFile === 'string' &&\n import.meta.url === pathToFileURL(invokedFile).href;\n\nif (invokedDirectly) {\n main(process.argv.slice(2)).catch((error: unknown) => {\n if (error instanceof CliHelpRequested) {\n process.exit(0);\n }\n console.error('[Townhouse] Error:', error);\n process.exit(1);\n });\n}\n","/**\n * Cross-platform browser opener for the wizard CLI command.\n * Uses platform-native launchers; errors are non-fatal.\n */\n\nimport { spawn } from 'node:child_process';\n\nexport interface BrowserOpener {\n open(url: string): Promise<void>;\n}\n\nexport class RealBrowserOpener implements BrowserOpener {\n async open(url: string): Promise<void> {\n let cmd: string;\n let args: string[];\n\n switch (process.platform) {\n case 'darwin':\n cmd = 'open';\n args = [url];\n break;\n case 'win32':\n cmd = 'cmd';\n args = ['/c', 'start', '', url];\n break;\n default:\n cmd = 'xdg-open';\n args = [url];\n break;\n }\n\n return new Promise<void>((resolve) => {\n let settled = false;\n const settle = () => {\n if (settled) return;\n settled = true;\n resolve();\n };\n\n try {\n const child = spawn(cmd, args, {\n stdio: ['ignore', 'ignore', 'ignore'],\n detached: true,\n });\n\n // ENOENT (e.g. xdg-open not on PATH on a minimal container/WSL2 env)\n // surfaces asynchronously via the 'error' event, NOT a synchronous\n // throw from spawn. Subscribe so we don't crash with an unhandled\n // 'error' event and so the user gets a hint about why no browser opened.\n child.once('error', (err: Error) => {\n console.warn(\n `[Townhouse] Could not open browser via ${cmd}: ${err.message}`\n );\n settle();\n });\n child.once('spawn', () => {\n child.unref();\n settle();\n });\n } catch (err: unknown) {\n // Synchronous spawn error path (rare)\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[Townhouse] Could not open browser: ${msg}`);\n settle();\n }\n });\n }\n}\n\nexport class NoopBrowserOpener implements BrowserOpener {\n public readonly calls: string[] = [];\n\n async open(url: string): Promise<void> {\n this.calls.push(url);\n }\n}\n"],"mappings":"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","names":["resolve"]}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
# Townhouse Dev Stack — Docker Compose (Story 21.8.0, D21-009)
|
|
2
|
+
#
|
|
3
|
+
# Full contributor dev topology: 1 standalone connector + 2 Town + 2 Mill + 1 DVM
|
|
4
|
+
# + 3 chain devnets + 1 SOCKS5 proxy.
|
|
5
|
+
#
|
|
6
|
+
# This file is intentionally separate from docker-compose-townhouse.yml.
|
|
7
|
+
# Production describes one operator's actual node. This file describes a
|
|
8
|
+
# contributor's rig with enough instances to exercise every dashboard view.
|
|
9
|
+
# See story 21.8.0 Dev Notes for the full rationale.
|
|
10
|
+
#
|
|
11
|
+
# Canonical path (npm tarball): packages/townhouse/compose/townhouse-dev.yml
|
|
12
|
+
# Ships verbatim inside @toon-protocol/townhouse dist/compose/townhouse-dev.yml
|
|
13
|
+
# (Story 45.2 — no digest substitution; uses local toon:* image tags).
|
|
14
|
+
#
|
|
15
|
+
# Legacy path (backward-compat): docker-compose-townhouse-dev.yml (repo root)
|
|
16
|
+
# Used by scripts/townhouse-dev-infra.sh and existing CI. Preserved until
|
|
17
|
+
# a follow-up story routes the script through the package-local copy.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# ./scripts/townhouse-dev-infra.sh up # Build, start, wait for health (reads root path)
|
|
21
|
+
# ./scripts/townhouse-dev-infra.sh down # Stop containers, remove .env.townhouse-dev
|
|
22
|
+
# ./scripts/townhouse-dev-infra.sh down-v # Same + remove volumes
|
|
23
|
+
# ./scripts/townhouse-dev-infra.sh status # Show container state
|
|
24
|
+
#
|
|
25
|
+
# Port allocation — 28xxx range (no collision with SDK E2E 18xxx/19xxx or
|
|
26
|
+
# production Townhouse 3xxx/7100/9401). See CLAUDE.md "Townhouse Dev Stack (28xxx)".
|
|
27
|
+
#
|
|
28
|
+
# All host bindings use 127.0.0.1 — never 0.0.0.0 (developer machine).
|
|
29
|
+
#
|
|
30
|
+
# Connector image tag must match DEFAULT_CONNECTOR_IMAGE in
|
|
31
|
+
# packages/townhouse/src/constants.ts — keep both in sync when bumping.
|
|
32
|
+
|
|
33
|
+
networks:
|
|
34
|
+
townhouse-dev-net:
|
|
35
|
+
driver: bridge
|
|
36
|
+
|
|
37
|
+
volumes:
|
|
38
|
+
townhouse-dev-connector-data:
|
|
39
|
+
townhouse-dev-town-01-data:
|
|
40
|
+
townhouse-dev-town-02-data:
|
|
41
|
+
townhouse-dev-mill-01-data:
|
|
42
|
+
townhouse-dev-mill-02-data:
|
|
43
|
+
townhouse-dev-dvm-01-data:
|
|
44
|
+
|
|
45
|
+
services:
|
|
46
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
47
|
+
# Connector — standalone connector for all 5 child peers
|
|
48
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
49
|
+
townhouse-dev-connector:
|
|
50
|
+
# Image tag must match DEFAULT_CONNECTOR_IMAGE in packages/townhouse/src/constants.ts
|
|
51
|
+
image: ghcr.io/toon-protocol/connector:3.4.1
|
|
52
|
+
container_name: townhouse-dev-connector
|
|
53
|
+
networks:
|
|
54
|
+
- townhouse-dev-net
|
|
55
|
+
ports:
|
|
56
|
+
- '127.0.0.1:28080:9401'
|
|
57
|
+
environment:
|
|
58
|
+
CONFIG_FILE: /config/connector.yaml
|
|
59
|
+
volumes:
|
|
60
|
+
- ./docker/configs/townhouse-dev-connector.yaml:/config/connector.yaml:ro
|
|
61
|
+
- townhouse-dev-connector-data:/data
|
|
62
|
+
restart: unless-stopped
|
|
63
|
+
healthcheck:
|
|
64
|
+
# nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal health probe
|
|
65
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:9401/health']
|
|
66
|
+
interval: 10s
|
|
67
|
+
timeout: 5s
|
|
68
|
+
retries: 5
|
|
69
|
+
start_period: 15s
|
|
70
|
+
|
|
71
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
72
|
+
# Anvil — Local Ethereum (chain-id 31337) with auto-deployed contracts
|
|
73
|
+
# Copied from docker-compose-sdk-e2e.yml, ports shifted to 28xxx range
|
|
74
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
75
|
+
townhouse-dev-anvil:
|
|
76
|
+
image: ghcr.io/foundry-rs/foundry:latest
|
|
77
|
+
container_name: townhouse-dev-anvil
|
|
78
|
+
entrypoint: []
|
|
79
|
+
command:
|
|
80
|
+
- sh
|
|
81
|
+
- -c
|
|
82
|
+
- |
|
|
83
|
+
echo "Starting Anvil..."
|
|
84
|
+
anvil --host 0.0.0.0 --port 8545 --chain-id 31337 --accounts 10 --balance 10000 &
|
|
85
|
+
ANVIL_PID=$$!
|
|
86
|
+
|
|
87
|
+
echo "Waiting for Anvil to be ready..."
|
|
88
|
+
until cast client --rpc-url http://localhost:8545 2>/dev/null | grep -q 'anvil'; do
|
|
89
|
+
sleep 1
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
echo "Deploying contracts..."
|
|
93
|
+
cd /contracts
|
|
94
|
+
forge script script/DeployLocal.s.sol:DeployLocalScript --rpc-url http://localhost:8545 --broadcast --skip-simulation || echo "Deployment failed (non-fatal)"
|
|
95
|
+
|
|
96
|
+
echo "Funding test accounts with Mock USDC..."
|
|
97
|
+
DEPLOYER_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
|
98
|
+
TOKEN=0x5FbDB2315678afecb367f032d93F642f64180aa3
|
|
99
|
+
AMOUNT=10000000000000000000000
|
|
100
|
+
for ADDR in \
|
|
101
|
+
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
|
|
102
|
+
0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 \
|
|
103
|
+
0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc \
|
|
104
|
+
0x976EA74026E726554dB657fA54763abd0C3a0aa9 \
|
|
105
|
+
0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 \
|
|
106
|
+
0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f \
|
|
107
|
+
0xa0Ee7A142d267C1f36714E4a8F75612F20a79720; do
|
|
108
|
+
cast send --rpc-url http://localhost:8545 --private-key $$DEPLOYER_KEY \
|
|
109
|
+
$$TOKEN "transfer(address,uint256)" $$ADDR $$AMOUNT 2>/dev/null \
|
|
110
|
+
&& echo "Funded $$ADDR" || echo "Fund $$ADDR failed (non-fatal)"
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
echo "Anvil ready with contracts deployed!"
|
|
114
|
+
wait $$ANVIL_PID
|
|
115
|
+
volumes:
|
|
116
|
+
- ./contracts/evm:/contracts
|
|
117
|
+
ports:
|
|
118
|
+
- '127.0.0.1:28545:8545'
|
|
119
|
+
networks:
|
|
120
|
+
- townhouse-dev-net
|
|
121
|
+
restart: unless-stopped
|
|
122
|
+
healthcheck:
|
|
123
|
+
test:
|
|
124
|
+
[
|
|
125
|
+
'CMD-SHELL',
|
|
126
|
+
"cast client --rpc-url http://localhost:8545 2>&1 | grep -q 'anvil' || exit 1",
|
|
127
|
+
]
|
|
128
|
+
interval: 10s
|
|
129
|
+
timeout: 5s
|
|
130
|
+
retries: 3
|
|
131
|
+
start_period: 15s
|
|
132
|
+
|
|
133
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
134
|
+
# Solana Test Validator — ports shifted to 28xxx range
|
|
135
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
136
|
+
townhouse-dev-solana:
|
|
137
|
+
image: ghcr.io/beeman/solana-test-validator:latest
|
|
138
|
+
container_name: townhouse-dev-solana
|
|
139
|
+
entrypoint: []
|
|
140
|
+
command:
|
|
141
|
+
- sh
|
|
142
|
+
- /entrypoint.sh
|
|
143
|
+
volumes:
|
|
144
|
+
- ./infra/solana/entrypoint.sh:/entrypoint.sh:ro
|
|
145
|
+
- ./contracts/solana:/programs:ro
|
|
146
|
+
tmpfs:
|
|
147
|
+
- /tmp/test-ledger
|
|
148
|
+
ports:
|
|
149
|
+
- '127.0.0.1:28899:8899' # RPC
|
|
150
|
+
- '127.0.0.1:28900:8900' # WebSocket
|
|
151
|
+
networks:
|
|
152
|
+
- townhouse-dev-net
|
|
153
|
+
security_opt:
|
|
154
|
+
- seccomp=unconfined
|
|
155
|
+
restart: unless-stopped
|
|
156
|
+
healthcheck:
|
|
157
|
+
test: ['CMD-SHELL', 'solana cluster-version --url http://localhost:8899 >/dev/null 2>&1']
|
|
158
|
+
interval: 5s
|
|
159
|
+
timeout: 5s
|
|
160
|
+
retries: 12
|
|
161
|
+
start_period: 10s
|
|
162
|
+
|
|
163
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
164
|
+
# Mina Lightnet — ports shifted to 28xxx range
|
|
165
|
+
# NOTE: Requires 4-8 GB RAM (see SDK E2E notes)
|
|
166
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
167
|
+
townhouse-dev-mina:
|
|
168
|
+
image: o1labs/mina-local-network:compatible-latest-lightnet
|
|
169
|
+
container_name: townhouse-dev-mina
|
|
170
|
+
environment:
|
|
171
|
+
NETWORK_TYPE: single-node
|
|
172
|
+
PROOF_LEVEL: none
|
|
173
|
+
LOG_LEVEL: Info
|
|
174
|
+
RUN_ARCHIVE_NODE: 'false'
|
|
175
|
+
SLOT_TIME: '20000'
|
|
176
|
+
ports:
|
|
177
|
+
- '127.0.0.1:28085:3101' # GraphQL (direct to daemon)
|
|
178
|
+
- '127.0.0.1:28181:8181' # Accounts Manager
|
|
179
|
+
networks:
|
|
180
|
+
- townhouse-dev-net
|
|
181
|
+
deploy:
|
|
182
|
+
resources:
|
|
183
|
+
limits:
|
|
184
|
+
memory: 4g
|
|
185
|
+
restart: unless-stopped
|
|
186
|
+
healthcheck:
|
|
187
|
+
test:
|
|
188
|
+
[
|
|
189
|
+
'CMD-SHELL',
|
|
190
|
+
'curl -sf -X POST -H "Content-Type: application/json" -d "{\"query\":\"{syncStatus}\"}" http://localhost:3101/graphql | grep -q SYNCED || exit 1',
|
|
191
|
+
]
|
|
192
|
+
interval: 10s
|
|
193
|
+
timeout: 10s
|
|
194
|
+
retries: 30
|
|
195
|
+
start_period: 180s
|
|
196
|
+
|
|
197
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
198
|
+
# SOCKS5 Proxy — optional; required for ATOR transport testing (story 21.15)
|
|
199
|
+
# Connector defaults to TRANSPORT_MODE: direct; proxy only needed for ATOR mode.
|
|
200
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
201
|
+
townhouse-dev-socks5:
|
|
202
|
+
image: serjs/go-socks5-proxy:latest
|
|
203
|
+
container_name: townhouse-dev-socks5
|
|
204
|
+
ports:
|
|
205
|
+
- '127.0.0.1:28050:1080'
|
|
206
|
+
networks:
|
|
207
|
+
- townhouse-dev-net
|
|
208
|
+
restart: unless-stopped
|
|
209
|
+
|
|
210
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
211
|
+
# Town 01 — Nostr relay node (instance 1 of 2)
|
|
212
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
213
|
+
townhouse-dev-town-01:
|
|
214
|
+
image: toon:town
|
|
215
|
+
container_name: townhouse-dev-town-01
|
|
216
|
+
networks:
|
|
217
|
+
- townhouse-dev-net
|
|
218
|
+
depends_on:
|
|
219
|
+
townhouse-dev-connector:
|
|
220
|
+
condition: service_healthy
|
|
221
|
+
expose:
|
|
222
|
+
- '3000'
|
|
223
|
+
ports:
|
|
224
|
+
- '127.0.0.1:28100:3100' # BLS health
|
|
225
|
+
- '127.0.0.1:28700:7100' # Nostr relay WebSocket
|
|
226
|
+
environment:
|
|
227
|
+
# nosemgrep: detect-insecure-websocket -- Docker-internal, TLS unnecessary
|
|
228
|
+
CONNECTOR_URL: ws://townhouse-dev-connector:3000
|
|
229
|
+
CONNECTOR_ADMIN_URL: http://townhouse-dev-connector:9401
|
|
230
|
+
FEE_PER_EVENT: '0'
|
|
231
|
+
NODE_NOSTR_PUBKEY: ''
|
|
232
|
+
NODE_EVM_ADDRESS: ''
|
|
233
|
+
# Interpolated from TOWN_01_SECRET_KEY exported by townhouse-dev-infra.sh.
|
|
234
|
+
NODE_NOSTR_SECRET_KEY: '${TOWN_01_SECRET_KEY:?TOWN_01_SECRET_KEY required — source scripts/townhouse-dev-infra.sh first}'
|
|
235
|
+
volumes:
|
|
236
|
+
- townhouse-dev-town-01-data:/data
|
|
237
|
+
healthcheck:
|
|
238
|
+
# nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal health probe
|
|
239
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3100/health']
|
|
240
|
+
interval: 30s
|
|
241
|
+
timeout: 10s
|
|
242
|
+
retries: 3
|
|
243
|
+
start_period: 5s
|
|
244
|
+
restart: unless-stopped
|
|
245
|
+
|
|
246
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
247
|
+
# Town 02 — Nostr relay node (instance 2 of 2)
|
|
248
|
+
# Story 21.10 exercises degraded state via `docker pause townhouse-dev-town-02`
|
|
249
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
250
|
+
townhouse-dev-town-02:
|
|
251
|
+
image: toon:town
|
|
252
|
+
container_name: townhouse-dev-town-02
|
|
253
|
+
networks:
|
|
254
|
+
- townhouse-dev-net
|
|
255
|
+
depends_on:
|
|
256
|
+
townhouse-dev-connector:
|
|
257
|
+
condition: service_healthy
|
|
258
|
+
expose:
|
|
259
|
+
- '3000'
|
|
260
|
+
ports:
|
|
261
|
+
- '127.0.0.1:28110:3100' # BLS health
|
|
262
|
+
- '127.0.0.1:28710:7100' # Nostr relay WebSocket
|
|
263
|
+
environment:
|
|
264
|
+
# nosemgrep: detect-insecure-websocket -- Docker-internal, TLS unnecessary
|
|
265
|
+
CONNECTOR_URL: ws://townhouse-dev-connector:3000
|
|
266
|
+
CONNECTOR_ADMIN_URL: http://townhouse-dev-connector:9401
|
|
267
|
+
FEE_PER_EVENT: '0'
|
|
268
|
+
NODE_NOSTR_PUBKEY: ''
|
|
269
|
+
NODE_EVM_ADDRESS: ''
|
|
270
|
+
# Interpolated from TOWN_02_SECRET_KEY exported by townhouse-dev-infra.sh.
|
|
271
|
+
NODE_NOSTR_SECRET_KEY: '${TOWN_02_SECRET_KEY:?TOWN_02_SECRET_KEY required — source scripts/townhouse-dev-infra.sh first}'
|
|
272
|
+
volumes:
|
|
273
|
+
- townhouse-dev-town-02-data:/data
|
|
274
|
+
healthcheck:
|
|
275
|
+
# nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal health probe
|
|
276
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3100/health']
|
|
277
|
+
interval: 30s
|
|
278
|
+
timeout: 10s
|
|
279
|
+
retries: 3
|
|
280
|
+
start_period: 5s
|
|
281
|
+
restart: unless-stopped
|
|
282
|
+
|
|
283
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
284
|
+
# Mill 01 — EVM↔Solana swap peer (story 21.11 AC names this exact container)
|
|
285
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
286
|
+
townhouse-dev-mill-01:
|
|
287
|
+
image: toon:mill
|
|
288
|
+
container_name: townhouse-dev-mill-01
|
|
289
|
+
networks:
|
|
290
|
+
- townhouse-dev-net
|
|
291
|
+
depends_on:
|
|
292
|
+
townhouse-dev-connector:
|
|
293
|
+
condition: service_healthy
|
|
294
|
+
townhouse-dev-anvil:
|
|
295
|
+
condition: service_healthy
|
|
296
|
+
townhouse-dev-solana:
|
|
297
|
+
condition: service_healthy
|
|
298
|
+
expose:
|
|
299
|
+
- '3000'
|
|
300
|
+
ports:
|
|
301
|
+
- '127.0.0.1:28200:3200' # BLS health
|
|
302
|
+
environment:
|
|
303
|
+
# nosemgrep: detect-insecure-websocket -- Docker-internal, TLS unnecessary
|
|
304
|
+
CONNECTOR_URL: ws://townhouse-dev-connector:3000
|
|
305
|
+
FEE_BASIS_POINTS: '0'
|
|
306
|
+
NODE_NOSTR_PUBKEY: ''
|
|
307
|
+
NODE_EVM_ADDRESS: ''
|
|
308
|
+
# MILL_MNEMONIC takes priority over NODE_NOSTR_SECRET_KEY for BIP-32 swap key derivation.
|
|
309
|
+
# Interpolated from MILL_01_MNEMONIC exported by townhouse-dev-infra.sh.
|
|
310
|
+
MILL_MNEMONIC: '${MILL_01_MNEMONIC:?MILL_01_MNEMONIC required — source scripts/townhouse-dev-infra.sh first}'
|
|
311
|
+
# Kept for backward-compat; ignored when MILL_MNEMONIC is set.
|
|
312
|
+
NODE_NOSTR_SECRET_KEY: '${MILL_01_SECRET_KEY:?MILL_01_SECRET_KEY required — source scripts/townhouse-dev-infra.sh first}'
|
|
313
|
+
MILL_CONFIG_PATH: /config/mill.config.json
|
|
314
|
+
MILL_RELAYS: ws://townhouse-dev-town-01:7100,ws://townhouse-dev-town-02:7100
|
|
315
|
+
volumes:
|
|
316
|
+
- ./docker/dev-fixtures/mill-01.config.json:/config/mill.config.json:ro
|
|
317
|
+
- townhouse-dev-mill-01-data:/data
|
|
318
|
+
healthcheck:
|
|
319
|
+
# nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal health probe
|
|
320
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3200/health']
|
|
321
|
+
interval: 30s
|
|
322
|
+
timeout: 10s
|
|
323
|
+
retries: 3
|
|
324
|
+
start_period: 5s
|
|
325
|
+
restart: unless-stopped
|
|
326
|
+
|
|
327
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
328
|
+
# Mill 02 — EVM↔Mina swap peer (story 21.11 AC names this exact container)
|
|
329
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
330
|
+
townhouse-dev-mill-02:
|
|
331
|
+
image: toon:mill
|
|
332
|
+
container_name: townhouse-dev-mill-02
|
|
333
|
+
networks:
|
|
334
|
+
- townhouse-dev-net
|
|
335
|
+
depends_on:
|
|
336
|
+
townhouse-dev-connector:
|
|
337
|
+
condition: service_healthy
|
|
338
|
+
townhouse-dev-anvil:
|
|
339
|
+
condition: service_healthy
|
|
340
|
+
townhouse-dev-mina:
|
|
341
|
+
condition: service_healthy
|
|
342
|
+
expose:
|
|
343
|
+
- '3000'
|
|
344
|
+
ports:
|
|
345
|
+
- '127.0.0.1:28210:3200' # BLS health
|
|
346
|
+
environment:
|
|
347
|
+
# nosemgrep: detect-insecure-websocket -- Docker-internal, TLS unnecessary
|
|
348
|
+
CONNECTOR_URL: ws://townhouse-dev-connector:3000
|
|
349
|
+
FEE_BASIS_POINTS: '0'
|
|
350
|
+
NODE_NOSTR_PUBKEY: ''
|
|
351
|
+
NODE_EVM_ADDRESS: ''
|
|
352
|
+
# MILL_MNEMONIC takes priority over NODE_NOSTR_SECRET_KEY for BIP-32 swap key derivation.
|
|
353
|
+
# Interpolated from MILL_02_MNEMONIC exported by townhouse-dev-infra.sh.
|
|
354
|
+
MILL_MNEMONIC: '${MILL_02_MNEMONIC:?MILL_02_MNEMONIC required — source scripts/townhouse-dev-infra.sh first}'
|
|
355
|
+
# Kept for backward-compat; ignored when MILL_MNEMONIC is set.
|
|
356
|
+
NODE_NOSTR_SECRET_KEY: '${MILL_02_SECRET_KEY:?MILL_02_SECRET_KEY required — source scripts/townhouse-dev-infra.sh first}'
|
|
357
|
+
MILL_CONFIG_PATH: /config/mill.config.json
|
|
358
|
+
MILL_RELAYS: ws://townhouse-dev-town-01:7100,ws://townhouse-dev-town-02:7100
|
|
359
|
+
volumes:
|
|
360
|
+
- ./docker/dev-fixtures/mill-02.config.json:/config/mill.config.json:ro
|
|
361
|
+
- townhouse-dev-mill-02-data:/data
|
|
362
|
+
healthcheck:
|
|
363
|
+
# nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal health probe
|
|
364
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3200/health']
|
|
365
|
+
interval: 30s
|
|
366
|
+
timeout: 10s
|
|
367
|
+
retries: 3
|
|
368
|
+
start_period: 5s
|
|
369
|
+
restart: unless-stopped
|
|
370
|
+
|
|
371
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
372
|
+
# DVM 01 — data vending machine (story 21.12 AC names this exact container)
|
|
373
|
+
# TURBO_TOKEN: pass through from host env; absence is non-fatal (disabled-upload mode)
|
|
374
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
375
|
+
townhouse-dev-dvm-01:
|
|
376
|
+
image: toon:dvm
|
|
377
|
+
container_name: townhouse-dev-dvm-01
|
|
378
|
+
networks:
|
|
379
|
+
- townhouse-dev-net
|
|
380
|
+
depends_on:
|
|
381
|
+
townhouse-dev-connector:
|
|
382
|
+
condition: service_healthy
|
|
383
|
+
expose:
|
|
384
|
+
- '3300'
|
|
385
|
+
ports:
|
|
386
|
+
- '127.0.0.1:28400:3400' # BLS health
|
|
387
|
+
environment:
|
|
388
|
+
# nosemgrep: detect-insecure-websocket -- Docker-internal, TLS unnecessary
|
|
389
|
+
CONNECTOR_URL: ws://townhouse-dev-connector:3000
|
|
390
|
+
FEE_PER_JOB: '0'
|
|
391
|
+
NODE_NOSTR_PUBKEY: ''
|
|
392
|
+
NODE_EVM_ADDRESS: ''
|
|
393
|
+
# Interpolated from DVM_01_SECRET_KEY exported by townhouse-dev-infra.sh.
|
|
394
|
+
NODE_NOSTR_SECRET_KEY: '${DVM_01_SECRET_KEY:?DVM_01_SECRET_KEY required — source scripts/townhouse-dev-infra.sh first}'
|
|
395
|
+
# TURBO_TOKEN may legitimately be unset for non-Turbo dev paths — empty string is intended.
|
|
396
|
+
TURBO_TOKEN: '${TURBO_TOKEN:-}'
|
|
397
|
+
volumes:
|
|
398
|
+
- townhouse-dev-dvm-01-data:/data
|
|
399
|
+
healthcheck:
|
|
400
|
+
# nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal health probe
|
|
401
|
+
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3400/health']
|
|
402
|
+
interval: 30s
|
|
403
|
+
timeout: 10s
|
|
404
|
+
retries: 3
|
|
405
|
+
start_period: 5s
|
|
406
|
+
restart: unless-stopped
|