@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,1507 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import Docker from 'dockerode';
|
|
3
|
+
import { NetworkMode } from '@toon-protocol/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Townhouse configuration schema — TypeScript interfaces only.
|
|
7
|
+
* Runtime validation lives in validator.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface TownNodeConfig {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Publish price in ILP base units per event. Injected into the town container
|
|
14
|
+
* (FEE_PER_EVENT), enforced by the town's pricing validator, AND advertised in
|
|
15
|
+
* the town's kind:10032 `feePerByte` so clients know the cost before sending.
|
|
16
|
+
*/
|
|
17
|
+
feePerEvent?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Settlement chain the town advertises + prices in (kind:10032). Must be one
|
|
20
|
+
* of the deployment's supported chains (the `network` profile ∪ `chains add`).
|
|
21
|
+
* Picked via `node add town --settlement-chain`. When unset, defaults to the
|
|
22
|
+
* deployment's first supported chain. Validated at provision time.
|
|
23
|
+
*/
|
|
24
|
+
settlementChainId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Settlement token on `settlementChainId` (USDC | ETH | SOL | MINA). Must be a
|
|
27
|
+
* token that chain supports (EVM: USDC/ETH · Solana: USDC/SOL · Mina: MINA
|
|
28
|
+
* only). Picked via `--asset`. When unset, defaults to USDC where supported,
|
|
29
|
+
* else the native token. `assetScale` is DERIVED from this token, not set here.
|
|
30
|
+
*/
|
|
31
|
+
assetCode?: string;
|
|
32
|
+
/**
|
|
33
|
+
* @deprecated assetScale is derived from `assetCode`/`settlementChainId`
|
|
34
|
+
* (USDC 6, ETH 18, SOL 9, MINA 9). Retained for back-compat; ignored for the
|
|
35
|
+
* advertised value.
|
|
36
|
+
*/
|
|
37
|
+
assetScale?: number;
|
|
38
|
+
/** Docker image override */
|
|
39
|
+
image?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Apex connector negotiation values an operator can tune. Currently the routing
|
|
43
|
+
* fee the apex takes on packets it forwards to children. NOTE: enforcement is
|
|
44
|
+
* connector-side — emitting this only has effect on a connector image that reads
|
|
45
|
+
* a routing-fee field; see config-generator.ts.
|
|
46
|
+
*/
|
|
47
|
+
interface ApexConfig {
|
|
48
|
+
/** Routing fee in basis points (1 = 0.01%) the apex takes per forwarded packet. */
|
|
49
|
+
routingFeeBasisPoints?: number;
|
|
50
|
+
}
|
|
51
|
+
interface ChainEndpoint {
|
|
52
|
+
rpcUrl: string;
|
|
53
|
+
wsUrl?: string;
|
|
54
|
+
}
|
|
55
|
+
interface MillChainsConfig {
|
|
56
|
+
evm?: ChainEndpoint;
|
|
57
|
+
solana?: ChainEndpoint;
|
|
58
|
+
mina?: ChainEndpoint;
|
|
59
|
+
}
|
|
60
|
+
interface MillNodeConfig {
|
|
61
|
+
enabled: boolean;
|
|
62
|
+
/** Swap fee basis points (1 = 0.01%) */
|
|
63
|
+
feeBasisPoints?: number;
|
|
64
|
+
/** Docker image override */
|
|
65
|
+
image?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Nostr relay URLs the mill listens on for swap requests. Mill's
|
|
68
|
+
* `validateConfig()` REQUIRES a non-empty list, so this must be supplied at
|
|
69
|
+
* `node add mill` time — via `--relays`, this field, or the `MILL_RELAYS`
|
|
70
|
+
* env var (resolution precedence: flag > config > env). Persisted here so
|
|
71
|
+
* subsequent `node remove && node add` and reconciliation no longer depend on
|
|
72
|
+
* a shell env var being exported at the right moment. Injected into the mill
|
|
73
|
+
* container as the comma-joined `MILL_RELAYS` env.
|
|
74
|
+
*/
|
|
75
|
+
relays?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Chain RPC endpoints the mill should swap against (D2). The orchestrator
|
|
78
|
+
* does not currently forward this directly into MILL_CONFIG_JSON — it
|
|
79
|
+
* round-trips through YAML so the dashboard and future stories can read it.
|
|
80
|
+
*/
|
|
81
|
+
chains?: MillChainsConfig;
|
|
82
|
+
/** Enabled swap pairs, e.g. ['EVM<->SOL']. Informational; D2-introduced. */
|
|
83
|
+
pairs?: string[];
|
|
84
|
+
}
|
|
85
|
+
interface DvmNodeConfig {
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
/** DVM job fee in millisatoshis */
|
|
88
|
+
feePerJob?: number;
|
|
89
|
+
/** Per-kind pricing in millisatoshis (key = stringified kind number) */
|
|
90
|
+
kindPricing?: Record<string, number>;
|
|
91
|
+
/** Docker image override */
|
|
92
|
+
image?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Arweave Turbo upload credential (a JWK JSON string) for kind:5094 blob
|
|
95
|
+
* jobs. OPTIONAL — the DVM boots without it and free-tier (<100KB)
|
|
96
|
+
* unauthenticated uploads still work; only larger/paid uploads need it.
|
|
97
|
+
* Supplied at `node add dvm` time via `--turbo-token`, this field, or the
|
|
98
|
+
* `TURBO_TOKEN` env var (resolution precedence: flag > config > env), then
|
|
99
|
+
* injected into the dvm container as `TURBO_TOKEN`.
|
|
100
|
+
*
|
|
101
|
+
* SECRET: unlike `mill.relays`, this is NOT auto-written here by `node add`
|
|
102
|
+
* (a private key in plaintext config.yaml is avoided by design). It is only
|
|
103
|
+
* READ if an operator chooses to set it manually, accepting that risk. The
|
|
104
|
+
* `--turbo-token` flag injects at runtime without persisting.
|
|
105
|
+
*/
|
|
106
|
+
turboToken?: string;
|
|
107
|
+
}
|
|
108
|
+
interface NodesConfig {
|
|
109
|
+
town: TownNodeConfig;
|
|
110
|
+
mill: MillNodeConfig;
|
|
111
|
+
dvm: DvmNodeConfig;
|
|
112
|
+
}
|
|
113
|
+
interface WalletConfig {
|
|
114
|
+
/** Path to encrypted wallet file (no plaintext mnemonic in config) */
|
|
115
|
+
encrypted_path: string;
|
|
116
|
+
}
|
|
117
|
+
interface ConnectorConfig {
|
|
118
|
+
/** Docker image for the shared ILP connector */
|
|
119
|
+
image: string;
|
|
120
|
+
/** Admin API port */
|
|
121
|
+
adminPort: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Connector chain-provider entry — what the connector needs to spin up its
|
|
125
|
+
* settlement subsystem (AccountManager + ClaimReceiver) for one chain. Without
|
|
126
|
+
* at least one entry, `/admin/earnings.json` returns 503 and Townhouse's
|
|
127
|
+
* earnings data plane breaks (Epic 47 BUG-1).
|
|
128
|
+
*
|
|
129
|
+
* This is a discriminated union on `chainType` that mirrors the connector's
|
|
130
|
+
* `ProviderConfig` contract (EVM | Solana | Mina), so entries pass straight
|
|
131
|
+
* through `ConnectorConfigGenerator` to the connector. The connector already
|
|
132
|
+
* implements payment-channel providers for all three chains.
|
|
133
|
+
*
|
|
134
|
+
* Dev-Anvil deterministic placeholders are exposed via
|
|
135
|
+
* `DEFAULT_HS_CHAIN_PROVIDERS` in `defaults.ts`; operators running on real
|
|
136
|
+
* chains override this in their `config.yaml`.
|
|
137
|
+
*/
|
|
138
|
+
/** Supported settlement chain families. */
|
|
139
|
+
type ChainType = 'evm' | 'solana' | 'mina';
|
|
140
|
+
/** EVM settlement chain (Base, Arbitrum, Anvil dev, …). */
|
|
141
|
+
interface EvmChainProvider {
|
|
142
|
+
chainType: 'evm';
|
|
143
|
+
/** Canonical chain id, e.g. 'evm:base:31337' (dev-Anvil) or 'evm:base:8453' (mainnet). */
|
|
144
|
+
chainId: string;
|
|
145
|
+
/** JSON-RPC endpoint. May be a dead address in offline/demo mode. */
|
|
146
|
+
rpcUrl: string;
|
|
147
|
+
/** PaymentChannel registry contract. */
|
|
148
|
+
registryAddress: string;
|
|
149
|
+
/** Settlement token (USDC, etc.) contract. */
|
|
150
|
+
tokenAddress: string;
|
|
151
|
+
/**
|
|
152
|
+
* Hex private key / key id the connector signs settlement claims with.
|
|
153
|
+
* Optional: when omitted, `townhouse hs up` fills it with the operator's
|
|
154
|
+
* mnemonic-derived apex settlement key (acct index 3). Set it only to use an
|
|
155
|
+
* external/hardware key.
|
|
156
|
+
*/
|
|
157
|
+
keyId?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Settlement tuning knobs. The connector reads its GLOBAL settlement
|
|
160
|
+
* threshold from the FIRST EVM chainProvider that carries `settlementOptions`
|
|
161
|
+
* (connector `connector-node.ts`: `chainProviders.find(evm && settlementOptions)`)
|
|
162
|
+
* and applies that single `threshold` as the `defaultThreshold` for the
|
|
163
|
+
* event-driven settlement monitor across ALL chains (EVM, Solana, Mina).
|
|
164
|
+
*
|
|
165
|
+
* When omitted the connector falls back to a default threshold of `1000000`,
|
|
166
|
+
* which exactly equals the per-publish fee (1 USDC at scale 6). Because the
|
|
167
|
+
* monitor triggers on `cumulativeAmount > threshold` (STRICTLY greater), a
|
|
168
|
+
* single paid publish at the default never crosses it and on-chain
|
|
169
|
+
* settlement (`claimFromChannel`/`SETTLE_CHANNEL`) is never triggered for a
|
|
170
|
+
* dynamically-registered (anonymous HS) peer. Set `threshold` BELOW the
|
|
171
|
+
* per-publish fee to make a single paid publish settle on-chain.
|
|
172
|
+
*
|
|
173
|
+
* Mirrors the connector's `EVMProviderConfig.settlementOptions` contract.
|
|
174
|
+
*/
|
|
175
|
+
settlementOptions?: {
|
|
176
|
+
/** Cumulative balance (in token base units, as a decimal string) that
|
|
177
|
+
* must be exceeded before the connector settles a peer on-chain. */
|
|
178
|
+
threshold?: string;
|
|
179
|
+
/** Channel settlement timeout in seconds (connector default 86400). */
|
|
180
|
+
settlementTimeoutSecs?: number;
|
|
181
|
+
/** Initial channel deposit multiplier (connector default 1). */
|
|
182
|
+
initialDepositMultiplier?: number;
|
|
183
|
+
/** Settlement polling interval in ms (legacy poll-based monitor). */
|
|
184
|
+
pollingIntervalMs?: number;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Solana settlement chain. */
|
|
188
|
+
interface SolanaChainProvider {
|
|
189
|
+
chainType: 'solana';
|
|
190
|
+
/** Canonical chain id, e.g. 'solana:devnet' or 'solana:mainnet'. */
|
|
191
|
+
chainId: string;
|
|
192
|
+
/** Cluster RPC endpoint (HTTP). */
|
|
193
|
+
rpcUrl: string;
|
|
194
|
+
/** WebSocket endpoint for account subscriptions (derived from rpcUrl if absent). */
|
|
195
|
+
wsUrl?: string;
|
|
196
|
+
/** On-chain payment-channel program id (base58). */
|
|
197
|
+
programId: string;
|
|
198
|
+
/** Settlement token mint (base58). */
|
|
199
|
+
tokenMint?: string;
|
|
200
|
+
/**
|
|
201
|
+
* Key id the connector signs settlement claims with. Optional — when omitted,
|
|
202
|
+
* `townhouse hs up` fills it with the operator's mnemonic-derived apex key.
|
|
203
|
+
*/
|
|
204
|
+
keyId?: string;
|
|
205
|
+
}
|
|
206
|
+
/** Mina settlement chain. */
|
|
207
|
+
interface MinaChainProvider {
|
|
208
|
+
chainType: 'mina';
|
|
209
|
+
/** Canonical chain id, e.g. 'mina:devnet' or 'mina:mainnet'. */
|
|
210
|
+
chainId: string;
|
|
211
|
+
/** Mina GraphQL endpoint. */
|
|
212
|
+
graphqlUrl: string;
|
|
213
|
+
/** zkApp address for the payment-channel contract (base58). */
|
|
214
|
+
zkAppAddress: string;
|
|
215
|
+
/** Key id the connector signs settlement claims with. */
|
|
216
|
+
keyId?: string;
|
|
217
|
+
}
|
|
218
|
+
type ChainProviderEntry = EvmChainProvider | SolanaChainProvider | MinaChainProvider;
|
|
219
|
+
/**
|
|
220
|
+
* Hidden-service publication config (Story 35.5 of the connector repo).
|
|
221
|
+
*
|
|
222
|
+
* When set, the connector boots `@anyone-protocol/anyone-client` in-process,
|
|
223
|
+
* spawns the `anon` binary, publishes a v3 hidden service, and advertises
|
|
224
|
+
* its `wss://<addr>.anyone/btp` URL to peers. The keypair lives at `dir`
|
|
225
|
+
* inside the connector container and persists across restarts when that
|
|
226
|
+
* path is on a mounted volume.
|
|
227
|
+
*
|
|
228
|
+
* Operator surface (this type) is intentionally narrow; the connector's
|
|
229
|
+
* own config has more knobs (binaryPath, configFilePath) that we omit
|
|
230
|
+
* here until a real use case demands them.
|
|
231
|
+
*/
|
|
232
|
+
interface HiddenServiceConfig$1 {
|
|
233
|
+
/** Path inside the connector container for hs_ed25519_secret_key etc. */
|
|
234
|
+
dir: string;
|
|
235
|
+
/** Hidden service port — peers dial <addr>.anyone:<port>. */
|
|
236
|
+
port: number;
|
|
237
|
+
/**
|
|
238
|
+
* Optional override of the externalUrl the connector advertises. Default
|
|
239
|
+
* is `"auto"` — the connector reads `${dir}/hostname` at startup and
|
|
240
|
+
* builds `wss://<hostname>.anyone/btp` itself.
|
|
241
|
+
*/
|
|
242
|
+
externalUrl?: string;
|
|
243
|
+
/** Optional override of the SDK's start-up readiness deadline (ms). */
|
|
244
|
+
startupTimeoutMs?: number;
|
|
245
|
+
/** Optional override of the SDK's shutdown deadline (ms). */
|
|
246
|
+
stopTimeoutMs?: number;
|
|
247
|
+
}
|
|
248
|
+
interface TransportConfig {
|
|
249
|
+
/** Transport mode: 'hs' for hidden-service (SOCKS5/Tor-based), 'direct' for clearnet */
|
|
250
|
+
mode: 'hs' | 'direct';
|
|
251
|
+
/** SOCKS5 proxy address when using hs transport */
|
|
252
|
+
socksProxy?: string;
|
|
253
|
+
/**
|
|
254
|
+
* Externally reachable BTP URL. Required when mode='hs' AND
|
|
255
|
+
* hiddenService is unset (operator runs their own anon binary external
|
|
256
|
+
* to the connector and is responsible for the URL). Ignored for
|
|
257
|
+
* mode='direct'. When hiddenService is set and externalUrl is unset,
|
|
258
|
+
* the generator emits the literal `"auto"` so the connector resolves
|
|
259
|
+
* the .anyone hostname from disk at startup.
|
|
260
|
+
*/
|
|
261
|
+
externalUrl?: string;
|
|
262
|
+
/**
|
|
263
|
+
* Optional inbound hidden-service publication. When set, the connector
|
|
264
|
+
* manages its own anon binary and publishes a .anyone hidden service.
|
|
265
|
+
*/
|
|
266
|
+
hiddenService?: HiddenServiceConfig$1;
|
|
267
|
+
/**
|
|
268
|
+
* Town relay hidden service. When set, the orchestrator starts a second
|
|
269
|
+
* ator sidecar (parallel to any connector HS sidecar) that forwards
|
|
270
|
+
* inbound HS traffic to the town container's Nostr WebSocket port (7100),
|
|
271
|
+
* and the town container is configured to advertise the .anyone URL.
|
|
272
|
+
* Reuses HiddenServiceConfig — same shape as connector HS config.
|
|
273
|
+
*/
|
|
274
|
+
relayHiddenService?: HiddenServiceConfig$1;
|
|
275
|
+
/**
|
|
276
|
+
* Public Nostr relay read URL the town advertises (kind:10032 `relayUrl` +
|
|
277
|
+
* kind:10166 seed entry) so clients know where to subscribe for FREE reads.
|
|
278
|
+
* - DIRECT mode: set this to your externally-reachable `ws(s)://<host>:7100`
|
|
279
|
+
* and bind the port with `TOWNHOUSE_RELAY_BIND=0.0.0.0`.
|
|
280
|
+
* - HS mode: leave unset — `hs up` derives `wss://<relay-addr>.anyone/` from
|
|
281
|
+
* the relay hidden service. Set it only to override the derived value.
|
|
282
|
+
*/
|
|
283
|
+
relayExternalUrl?: string;
|
|
284
|
+
}
|
|
285
|
+
interface ApiConfig {
|
|
286
|
+
/** Dashboard/API port */
|
|
287
|
+
port: number;
|
|
288
|
+
/** Bind address */
|
|
289
|
+
host: string;
|
|
290
|
+
}
|
|
291
|
+
interface LoggingConfig {
|
|
292
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
293
|
+
}
|
|
294
|
+
interface PresetMetadata {
|
|
295
|
+
/** Preset that produced this config (D2). */
|
|
296
|
+
name: 'demo';
|
|
297
|
+
/** Where the chain endpoints came from — leases.json path or 'local-fallback'. */
|
|
298
|
+
chainEndpointSource: string;
|
|
299
|
+
}
|
|
300
|
+
interface TownhouseConfig {
|
|
301
|
+
nodes: NodesConfig;
|
|
302
|
+
wallet: WalletConfig;
|
|
303
|
+
connector: ConnectorConfig;
|
|
304
|
+
transport: TransportConfig;
|
|
305
|
+
api: ApiConfig;
|
|
306
|
+
logging: LoggingConfig;
|
|
307
|
+
/**
|
|
308
|
+
* Network mode selecting the chain tier for BOTH the apex connector and the
|
|
309
|
+
* child node containers (EVM = Base primary + Arbitrum; Solana; Mina):
|
|
310
|
+
* - `testnet` (default when unset) — public Sepolia / Solana+Mina devnets,
|
|
311
|
+
* settlement-complete (this is the sane default so an operator who omits
|
|
312
|
+
* `--network` gets a settlement-ready node, not a relay-only/dev fallback)
|
|
313
|
+
* - `devnet` — public Sepolia / Solana+Mina devnets (no local chain)
|
|
314
|
+
* - `mainnet` — public production chains, but TOON settlement contracts are
|
|
315
|
+
* NOT deployed there yet → resolves relay-only (no chainProviders)
|
|
316
|
+
* - `custom` — operator supplies `chainProviders` directly, OR just RPC URLs
|
|
317
|
+
* via `endpoints` (below) to point at the project's dev chains
|
|
318
|
+
*
|
|
319
|
+
* Resolved via `resolveNetworkProfile()` (@toon-protocol/core). An explicit
|
|
320
|
+
* non-empty `chainProviders` always overrides the derived providers.
|
|
321
|
+
*/
|
|
322
|
+
network?: NetworkMode;
|
|
323
|
+
/**
|
|
324
|
+
* Operator-supplied RPC URLs for `network: 'custom'` (`--evm-url`/`--sol-url`
|
|
325
|
+
* or EVM_URL/SOL_URL env). Points the apex + nodes at the project's dev chains
|
|
326
|
+
* hosted anywhere — e.g. the anvil + solana that scripts/akash-deploy.sh
|
|
327
|
+
* deploys to Akash (ingress hostnames rotate per redeploy, so the operator
|
|
328
|
+
* passes the current URLs). EVM is the chain-id 31338 Anvil
|
|
329
|
+
* (settlement-complete); Solana is RPC + Mock-USDC only (relay-only).
|
|
330
|
+
*/
|
|
331
|
+
endpoints?: {
|
|
332
|
+
evmUrl?: string;
|
|
333
|
+
solUrl?: string;
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* Connector chain providers — required for the connector's settlement
|
|
337
|
+
* subsystem (AccountManager + ClaimReceiver) to initialize. When unset on
|
|
338
|
+
* `townhouse hs up`, `hs-config-writer.ts` derives them from `network` (or
|
|
339
|
+
* injects `DEFAULT_HS_CHAIN_PROVIDERS` as a last resort) so the earnings
|
|
340
|
+
* route returns 200 out of the box (Epic 47 BUG-1 product fix).
|
|
341
|
+
*/
|
|
342
|
+
chainProviders?: ChainProviderEntry[];
|
|
343
|
+
/** Present only when the config was generated by `init --preset=<name>`. */
|
|
344
|
+
preset?: PresetMetadata;
|
|
345
|
+
/** Apex connector negotiation values (e.g. routing fee). */
|
|
346
|
+
apex?: ApexConfig;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Docker orchestration types for Townhouse (Story 21.2).
|
|
351
|
+
*
|
|
352
|
+
* STUB FILE: Created by ATDD workflow (red phase).
|
|
353
|
+
* Implementation will be added during the green phase.
|
|
354
|
+
*/
|
|
355
|
+
/** Node types that can be orchestrated by Townhouse. */
|
|
356
|
+
type NodeType = 'town' | 'mill' | 'dvm';
|
|
357
|
+
/** Specification for a container to be created by the orchestrator. */
|
|
358
|
+
interface ContainerSpec {
|
|
359
|
+
/** Container name (e.g., 'townhouse-town') */
|
|
360
|
+
name: string;
|
|
361
|
+
/** Docker image to use */
|
|
362
|
+
image: string;
|
|
363
|
+
/** Environment variables to pass to the container */
|
|
364
|
+
env: Record<string, string>;
|
|
365
|
+
/** Network to attach the container to */
|
|
366
|
+
network: string;
|
|
367
|
+
/** Port mappings (host:container) */
|
|
368
|
+
ports?: Record<string, string>;
|
|
369
|
+
}
|
|
370
|
+
/** Events emitted by the orchestrator during operations. */
|
|
371
|
+
interface OrchestratorEvents {
|
|
372
|
+
/** Emitted during image pull with progress info */
|
|
373
|
+
pullProgress: {
|
|
374
|
+
image: string;
|
|
375
|
+
status: string;
|
|
376
|
+
id?: string;
|
|
377
|
+
progress?: string;
|
|
378
|
+
};
|
|
379
|
+
/** Emitted when a container changes state */
|
|
380
|
+
containerState: {
|
|
381
|
+
name: string;
|
|
382
|
+
state: 'creating' | 'starting' | 'running' | 'stopping' | 'stopped' | 'error';
|
|
383
|
+
/** Additional context for error states (e.g., error message) */
|
|
384
|
+
detail?: string;
|
|
385
|
+
};
|
|
386
|
+
/** Emitted during health check polling */
|
|
387
|
+
healthCheck: {
|
|
388
|
+
name: string;
|
|
389
|
+
status: string;
|
|
390
|
+
attempt: number;
|
|
391
|
+
};
|
|
392
|
+
/** Emitted before connector restart during peer registration update */
|
|
393
|
+
connectorRestarting: {
|
|
394
|
+
reason: string;
|
|
395
|
+
};
|
|
396
|
+
/** Emitted after connector restart and health check passes */
|
|
397
|
+
connectorRestarted: {
|
|
398
|
+
peers: string[];
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/** Options for health check polling. */
|
|
402
|
+
interface HealthCheckOptions {
|
|
403
|
+
/** Polling interval in milliseconds (default: 2000) */
|
|
404
|
+
interval?: number;
|
|
405
|
+
/** Timeout in milliseconds (default: 60000) */
|
|
406
|
+
timeout?: number;
|
|
407
|
+
}
|
|
408
|
+
/** Network I/O stats for a container (from dockerode stats stream) */
|
|
409
|
+
interface BandwidthStats {
|
|
410
|
+
bytesIn: number;
|
|
411
|
+
bytesOut: number;
|
|
412
|
+
sampleAt: number;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
type ComposeProfile = 'dev' | 'hs' | 'direct';
|
|
416
|
+
interface ComposeLoaderOptions {
|
|
417
|
+
/** Override default `~/.townhouse/` write target. Used by tests. */
|
|
418
|
+
townhouseHome?: string;
|
|
419
|
+
/** Override the package-relative dist directory the loader reads from.
|
|
420
|
+
* Defaults to the `dist/` adjacent to compose-loader.js at runtime.
|
|
421
|
+
* Tests use this to point at fixture directories. */
|
|
422
|
+
distDir?: string;
|
|
423
|
+
}
|
|
424
|
+
declare class ComposeLoaderError extends Error {
|
|
425
|
+
constructor(message: string);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Returns the rendered compose YAML for the requested profile.
|
|
429
|
+
* For 'hs', digest substitutions are already applied (resolved at build time).
|
|
430
|
+
* For 'dev', the YAML is returned verbatim (uses local `toon:*` image tags).
|
|
431
|
+
* Throws `ComposeLoaderError` if the requested profile's YAML is unreadable.
|
|
432
|
+
*/
|
|
433
|
+
declare function loadComposeTemplate(profile: ComposeProfile, options?: ComposeLoaderOptions): string;
|
|
434
|
+
/**
|
|
435
|
+
* Writes the resolved compose YAML to `<townhouseHome>/compose/<profile>.yml`
|
|
436
|
+
* and copies `dist/image-manifest.json` to `<townhouseHome>/image-manifest.json`.
|
|
437
|
+
* BOTH output files are written with mode 0o600 (NFR8 — operator-secret file mode).
|
|
438
|
+
* Returns the absolute paths of the two files written.
|
|
439
|
+
*/
|
|
440
|
+
declare function materializeComposeTemplate(profile: ComposeProfile, options?: ComposeLoaderOptions): {
|
|
441
|
+
composePath: string;
|
|
442
|
+
manifestPath: string;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Wallet management types for Townhouse (Story 21.4).
|
|
447
|
+
*
|
|
448
|
+
* Defines interfaces for HD wallet key derivation, encryption at rest,
|
|
449
|
+
* and node key management.
|
|
450
|
+
*/
|
|
451
|
+
|
|
452
|
+
/** Configuration for the WalletManager */
|
|
453
|
+
interface WalletManagerConfig {
|
|
454
|
+
/** Path to encrypted wallet file */
|
|
455
|
+
encryptedPath: string;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Arweave JWK (RSA-4096) — matches `arweave-js` / `@ardrive/turbo-sdk`
|
|
459
|
+
* `JWKInterface` shape. Defined locally to avoid an arweave-js dependency.
|
|
460
|
+
*
|
|
461
|
+
* All fields are base64url-encoded big-endian integers per JWA (RFC 7518).
|
|
462
|
+
*/
|
|
463
|
+
interface ArweaveJwk {
|
|
464
|
+
kty: 'RSA';
|
|
465
|
+
/** Public exponent (typically "AQAB" = 65537) */
|
|
466
|
+
e: string;
|
|
467
|
+
/** Modulus n (base64url) */
|
|
468
|
+
n: string;
|
|
469
|
+
/** Private exponent d */
|
|
470
|
+
d?: string;
|
|
471
|
+
/** First prime factor p */
|
|
472
|
+
p?: string;
|
|
473
|
+
/** Second prime factor q */
|
|
474
|
+
q?: string;
|
|
475
|
+
/** d mod (p-1) */
|
|
476
|
+
dp?: string;
|
|
477
|
+
/** d mod (q-1) */
|
|
478
|
+
dq?: string;
|
|
479
|
+
/** q^-1 mod p */
|
|
480
|
+
qi?: string;
|
|
481
|
+
}
|
|
482
|
+
/** Keys derived for a specific node type */
|
|
483
|
+
interface NodeKeys {
|
|
484
|
+
/** Nostr public key (hex-encoded, 32 bytes) */
|
|
485
|
+
nostrPubkey: string;
|
|
486
|
+
/** Nostr secret key (raw 32-byte scalar) */
|
|
487
|
+
nostrSecretKey: Uint8Array;
|
|
488
|
+
/** EVM address (checksummed, 0x-prefixed) */
|
|
489
|
+
evmAddress: string;
|
|
490
|
+
/** EVM private key (raw 32 bytes) */
|
|
491
|
+
evmPrivateKey: Uint8Array;
|
|
492
|
+
/** BIP-44 derivation path used for Nostr key */
|
|
493
|
+
nostrDerivationPath: string;
|
|
494
|
+
/** BIP-44 derivation path used for EVM key */
|
|
495
|
+
evmDerivationPath: string;
|
|
496
|
+
/** Base58-encoded Solana public key — derived for all node types */
|
|
497
|
+
solanaAddress?: string;
|
|
498
|
+
/** Raw 32-byte Ed25519 Solana private key seed */
|
|
499
|
+
solanaPrivateKey?: Uint8Array;
|
|
500
|
+
/** BIP-44 derivation path used for Solana key (SLIP-0010 all-hardened) */
|
|
501
|
+
solanaDerivationPath?: string;
|
|
502
|
+
/** Mina public key hex — mill only, omitted for town/dvm */
|
|
503
|
+
minaAddress?: string;
|
|
504
|
+
/** Arweave wallet address (base64url SHA-256 of modulus) — DVM only */
|
|
505
|
+
arweaveAddress?: string;
|
|
506
|
+
/**
|
|
507
|
+
* Arweave RSA-4096 JWK — DVM only. The full private JWK is held in
|
|
508
|
+
* memory for credit-buy + upload signing. Zeroed by `lock()`.
|
|
509
|
+
*/
|
|
510
|
+
arweaveJwk?: ArweaveJwk;
|
|
511
|
+
/** BIP-44 derivation path used for the Arweave RSA sub-seed */
|
|
512
|
+
arweaveDerivationPath?: string;
|
|
513
|
+
}
|
|
514
|
+
/** Map of node type to its derived keys */
|
|
515
|
+
interface DerivedNodeKeys {
|
|
516
|
+
town: NodeKeys;
|
|
517
|
+
mill: NodeKeys;
|
|
518
|
+
dvm: NodeKeys;
|
|
519
|
+
}
|
|
520
|
+
/** Summary info for display (no secrets) */
|
|
521
|
+
interface NodeKeyInfo {
|
|
522
|
+
/** Node type */
|
|
523
|
+
nodeType: NodeType;
|
|
524
|
+
/** Nostr public key (hex) */
|
|
525
|
+
nostrPubkey: string;
|
|
526
|
+
/** EVM address (checksummed) */
|
|
527
|
+
evmAddress: string;
|
|
528
|
+
/** Nostr derivation path */
|
|
529
|
+
nostrDerivationPath: string;
|
|
530
|
+
/** EVM derivation path */
|
|
531
|
+
evmDerivationPath: string;
|
|
532
|
+
/** Base58-encoded Solana public key — derived for all node types */
|
|
533
|
+
solanaAddress?: string;
|
|
534
|
+
/** Solana derivation path — present whenever solanaAddress is */
|
|
535
|
+
solanaDerivationPath?: string;
|
|
536
|
+
/** Mina public key hex — mill only, omitted for town/dvm */
|
|
537
|
+
minaAddress?: string;
|
|
538
|
+
/** Arweave wallet address (base64url) — DVM only */
|
|
539
|
+
arweaveAddress?: string;
|
|
540
|
+
/** Arweave derivation path — present whenever arweaveAddress is */
|
|
541
|
+
arweaveDerivationPath?: string;
|
|
542
|
+
}
|
|
543
|
+
/** Persisted wallet state (in memory after decryption) */
|
|
544
|
+
interface WalletState {
|
|
545
|
+
/** All derived node keys */
|
|
546
|
+
keys: DerivedNodeKeys;
|
|
547
|
+
/**
|
|
548
|
+
* BIP-39 mnemonic held in memory for transient re-derivation.
|
|
549
|
+
* Cleared on lock() by setting this.state = null. Never serialized.
|
|
550
|
+
*/
|
|
551
|
+
mnemonic: string;
|
|
552
|
+
}
|
|
553
|
+
/** Encrypted wallet file format (JSON, all fields base64-encoded) */
|
|
554
|
+
interface EncryptedWallet {
|
|
555
|
+
/** scrypt salt (base64) */
|
|
556
|
+
salt: string;
|
|
557
|
+
/** AES-GCM initialization vector (base64) */
|
|
558
|
+
iv: string;
|
|
559
|
+
/** AES-256-GCM ciphertext (base64) */
|
|
560
|
+
ciphertext: string;
|
|
561
|
+
/** AES-GCM authentication tag (base64) */
|
|
562
|
+
tag: string;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* WalletManager — HD key derivation for Townhouse (Story 21.4, Task 1).
|
|
567
|
+
*
|
|
568
|
+
* Single BIP-39 mnemonic, deterministic HD derivation per node type.
|
|
569
|
+
* Uses BIP-44 paths with distinct account indices per node type:
|
|
570
|
+
* - Town: account 0
|
|
571
|
+
* - Mill: account 1
|
|
572
|
+
* - DVM: account 2
|
|
573
|
+
*
|
|
574
|
+
* Nostr keys use NIP-06 coin type 1237: m/44'/1237'/{account}'/0/0
|
|
575
|
+
* EVM keys use standard coin type 60: m/44'/60'/{account}'/0/0
|
|
576
|
+
* Solana keys (all node types) coin 501: m/44'/501'/{account}'/0'/0'
|
|
577
|
+
* (SLIP-0010 all-hardened, via @toon-protocol/mill::deriveMillKeys)
|
|
578
|
+
* Arweave keys (DVM only) coin 472: m/44'/472'/2'/0/0
|
|
579
|
+
* (32-byte BIP-32 sub-seed feeds a deterministic RSA-4096 PRNG via
|
|
580
|
+
* rsa-from-seed.ts (HMAC-DRBG + node-forge 1.3.3). Derivation takes 5–30s per DVM unlock; this runs
|
|
581
|
+
* once at unlock time, not per operation.)
|
|
582
|
+
*/
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* WalletManager handles mnemonic generation, key derivation, and in-memory
|
|
586
|
+
* key lifecycle for Townhouse node operations.
|
|
587
|
+
*/
|
|
588
|
+
declare class WalletManager {
|
|
589
|
+
private readonly config;
|
|
590
|
+
private state;
|
|
591
|
+
constructor(config: WalletManagerConfig);
|
|
592
|
+
/** Path to the encrypted wallet file */
|
|
593
|
+
get encryptedPath(): string;
|
|
594
|
+
/**
|
|
595
|
+
* Generate a new 12-word BIP-39 mnemonic and derive all node keys.
|
|
596
|
+
* Returns the mnemonic (for one-time display) and the derived state.
|
|
597
|
+
*/
|
|
598
|
+
generate(): Promise<{
|
|
599
|
+
mnemonic: string;
|
|
600
|
+
state: WalletState;
|
|
601
|
+
}>;
|
|
602
|
+
/**
|
|
603
|
+
* Import an existing mnemonic (12 or 24 words) and derive all node keys.
|
|
604
|
+
* Throws if mnemonic is invalid (wrong checksum, wrong word count, etc).
|
|
605
|
+
*/
|
|
606
|
+
fromMnemonic(mnemonic: string): Promise<WalletState>;
|
|
607
|
+
/**
|
|
608
|
+
* Get derived keys for a specific node type.
|
|
609
|
+
* Throws if wallet has not been initialized (call generate() or fromMnemonic() first).
|
|
610
|
+
*/
|
|
611
|
+
getNodeKeys(nodeType: NodeType): NodeKeys;
|
|
612
|
+
/**
|
|
613
|
+
* Get display-safe info for all node types (no secrets).
|
|
614
|
+
*/
|
|
615
|
+
getAllKeys(): NodeKeyInfo[];
|
|
616
|
+
/**
|
|
617
|
+
* List keys for all node types (alias for getAllKeys for API compatibility).
|
|
618
|
+
*/
|
|
619
|
+
listKeys(): NodeKeyInfo[];
|
|
620
|
+
/**
|
|
621
|
+
* Zero all in-memory key material. After calling lock(),
|
|
622
|
+
* getNodeKeys() and getAllKeys() will throw.
|
|
623
|
+
*/
|
|
624
|
+
lock(): void;
|
|
625
|
+
/**
|
|
626
|
+
* Return the BIP-39 mnemonic from in-memory wallet state.
|
|
627
|
+
* Returns null when the wallet is locked or not initialized.
|
|
628
|
+
*/
|
|
629
|
+
getMnemonic(): string | null;
|
|
630
|
+
/**
|
|
631
|
+
* Get derived keys for a specific node type at a given derivation index.
|
|
632
|
+
*
|
|
633
|
+
* Pure derivation — does NOT mutate `state`. Re-derives from the stored
|
|
634
|
+
* mnemonic each time it is called. For every node type, also derives the
|
|
635
|
+
* Solana key at the same account index. For 'dvm', also derives Arweave.
|
|
636
|
+
* Throws if the wallet is locked.
|
|
637
|
+
*
|
|
638
|
+
* v1 callers MUST pass `derivationIndex = ACCOUNT_INDEX_{type}` for the
|
|
639
|
+
* first (and only) instance per type. Multi-instance support is out of
|
|
640
|
+
* scope for v1 — the route layer enforces single-instance-per-type.
|
|
641
|
+
*/
|
|
642
|
+
deriveNodeKey(type: NodeType, derivationIndex: number): Promise<NodeKeys>;
|
|
643
|
+
/**
|
|
644
|
+
* Returns the EVM private key for a node as a 64-char lowercase hex string.
|
|
645
|
+
* Throws when the wallet is locked. Callers MUST treat the returned string
|
|
646
|
+
* as sensitive (no logging, no persisting). The underlying Uint8Array is
|
|
647
|
+
* still owned by WalletManager and will be zeroed by `lock()`.
|
|
648
|
+
*/
|
|
649
|
+
getEvmPrivateKeyHex(nodeType: NodeType): string;
|
|
650
|
+
/**
|
|
651
|
+
* Derive the APEX (connector) settlement key from the operator mnemonic at
|
|
652
|
+
* ACCOUNT_INDEX_APEX. The apex signs settlement claims with this key, so the
|
|
653
|
+
* operator never has to supply a raw `keyId` to `townhouse chains add`.
|
|
654
|
+
*
|
|
655
|
+
* Returns the EVM private key as a `0x`-prefixed 64-char hex string — the
|
|
656
|
+
* form the connector config's `keyId` expects (matches the dev placeholder
|
|
657
|
+
* `0x7c85…`). Also returns Solana + Mina settlement keys (connector 3.9.0)
|
|
658
|
+
* derived at the same `ACCOUNT_INDEX_APEX`, in the RAW base58 form the
|
|
659
|
+
* connector resolves a non-EVM `keyId` as:
|
|
660
|
+
* - Solana: base58 of the 32-byte Ed25519 seed,
|
|
661
|
+
* - Mina: `EK…` base58check (via `hexToMinaBase58PrivateKey`).
|
|
662
|
+
*
|
|
663
|
+
* EVM derivation must always succeed (it throws on failure). Solana/Mina
|
|
664
|
+
* derivation is best-effort: if `deriveMillKeys` throws (unsupported
|
|
665
|
+
* platform, library load error) the corresponding key is OMITTED rather than
|
|
666
|
+
* failing the whole method, so the EVM keyId path is never blocked.
|
|
667
|
+
*
|
|
668
|
+
* Async because `deriveMillKeys` is async. Throws when the wallet is locked.
|
|
669
|
+
*/
|
|
670
|
+
getApexSettlementKeys(): Promise<{
|
|
671
|
+
evmPrivateKeyHex: string;
|
|
672
|
+
solanaPrivateKeyBase58?: string;
|
|
673
|
+
minaPrivateKeyBase58?: string;
|
|
674
|
+
}>;
|
|
675
|
+
/**
|
|
676
|
+
* Returns the Solana Ed25519 private key seed for a node as a 64-char
|
|
677
|
+
* lowercase hex string (32 raw seed bytes). Throws when the wallet is
|
|
678
|
+
* locked or when the Solana key was not derived for this node type.
|
|
679
|
+
*/
|
|
680
|
+
getSolanaPrivateKeyHex(nodeType: NodeType): string;
|
|
681
|
+
/**
|
|
682
|
+
* Returns the Arweave RSA JWK for a node. Throws when the wallet is locked
|
|
683
|
+
* or when AR derivation has not yet been triggered for this node type.
|
|
684
|
+
*
|
|
685
|
+
* Callers MUST `await ensureArweaveKey(nodeType)` first the first time per
|
|
686
|
+
* unlock — RSA-4096 derivation is 5–30s and is therefore not done eagerly
|
|
687
|
+
* at `fromMnemonic`/`generate` time. After the first `ensureArweaveKey`
|
|
688
|
+
* the JWK is cached on the in-memory state until `lock()`.
|
|
689
|
+
*/
|
|
690
|
+
getArweaveJwk(nodeType: NodeType): ArweaveJwk;
|
|
691
|
+
/**
|
|
692
|
+
* Lazily derive the Arweave RSA-4096 JWK for a node type and cache it on
|
|
693
|
+
* the in-memory wallet state. Subsequent calls (within the same unlocked
|
|
694
|
+
* session) return the cached result without re-deriving.
|
|
695
|
+
*
|
|
696
|
+
* Only meaningful for node types that participate in the Arweave credit
|
|
697
|
+
* flow — `dvm` (account 2) in the current Townhouse layout. Calling on
|
|
698
|
+
* `town` or `mill` will derive a valid AR key at the corresponding
|
|
699
|
+
* account index but those keys are not used by any current code path.
|
|
700
|
+
*
|
|
701
|
+
* Throws if the wallet is locked or RSA derivation fails (unsupported
|
|
702
|
+
* platform, etc.). On success the result is also reflected in subsequent
|
|
703
|
+
* `getAllKeys()` calls (arweaveAddress + arweaveDerivationPath fields).
|
|
704
|
+
*/
|
|
705
|
+
ensureArweaveKey(nodeType: NodeType, password?: string): Promise<ArweaveJwk>;
|
|
706
|
+
/**
|
|
707
|
+
* Derive keys for all node types from a mnemonic.
|
|
708
|
+
*/
|
|
709
|
+
private deriveAllKeys;
|
|
710
|
+
/**
|
|
711
|
+
* Derive Nostr + EVM keys for a specific node type.
|
|
712
|
+
* Accepts an optional `accountIndex` to override the default per-type index.
|
|
713
|
+
* When omitted, uses `NODE_ACCOUNT_INDEX[nodeType]` (existing behavior).
|
|
714
|
+
*/
|
|
715
|
+
private deriveNodeKeys;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Connector types for Townhouse (Story 21.3).
|
|
720
|
+
*
|
|
721
|
+
* Defines the runtime configuration shape for the standalone ILP connector,
|
|
722
|
+
* peer entries, and admin API response types.
|
|
723
|
+
*/
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* A peer entry representing a child node connected to the connector via BTP.
|
|
727
|
+
* Each active node becomes a child peer of the connector.
|
|
728
|
+
*/
|
|
729
|
+
interface PeerEntry {
|
|
730
|
+
/** Node type identifier (e.g., 'town', 'mill', 'dvm') */
|
|
731
|
+
id: string;
|
|
732
|
+
/** Relationship to connector — nodes are always children */
|
|
733
|
+
relation: 'child';
|
|
734
|
+
/** BTP WebSocket URL for Docker-internal communication */
|
|
735
|
+
btpUrl: string;
|
|
736
|
+
/** Asset code for ILP packets (e.g., 'USD') */
|
|
737
|
+
assetCode: string;
|
|
738
|
+
/** Asset scale (e.g., 6 for micro-units) */
|
|
739
|
+
assetScale: number;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
/**
|
|
743
|
+
* Hidden-service configuration for the connector's inbound BTP path.
|
|
744
|
+
*
|
|
745
|
+
* When set under transport.hiddenService, the connector boots the
|
|
746
|
+
* `@anyone-protocol/anyone-client` SDK in-process (Story 35.5 of the
|
|
747
|
+
* connector repo, "ManagedAnonClient") which spawns the `anon` binary
|
|
748
|
+
* and publishes a v3 hidden service. The .anyone hostname is read from
|
|
749
|
+
* `${dir}/hostname` after publish and substituted into the connector's
|
|
750
|
+
* externalUrl as `wss://<hostname>.anyone/btp`.
|
|
751
|
+
*
|
|
752
|
+
* Operator surface lives at transport.hiddenService.{dir, port}; the
|
|
753
|
+
* connector's wire format is more verbose (transport.managedOptions.*) —
|
|
754
|
+
* config-generator handles the translation.
|
|
755
|
+
*
|
|
756
|
+
* Per-townhouse-instance keypair: the secret key under `${dir}/` is the
|
|
757
|
+
* .anyone identity. Persist it across redeploys to keep the address stable;
|
|
758
|
+
* delete it to rotate.
|
|
759
|
+
*/
|
|
760
|
+
interface HiddenServiceConfig {
|
|
761
|
+
/**
|
|
762
|
+
* Absolute path to the v3 hidden-service key directory inside the
|
|
763
|
+
* connector container. The connector creates `hs_ed25519_secret_key`,
|
|
764
|
+
* `hs_ed25519_public_key`, and `hostname` here on first boot, then
|
|
765
|
+
* reuses them on subsequent boots if present.
|
|
766
|
+
*/
|
|
767
|
+
dir: string;
|
|
768
|
+
/**
|
|
769
|
+
* Hidden service port advertised on the .anyone address. Forwards to
|
|
770
|
+
* the connector's BTP server (typically same port as connector's
|
|
771
|
+
* btpServerPort, since the HS port = the port external peers dial).
|
|
772
|
+
*/
|
|
773
|
+
port: number;
|
|
774
|
+
/**
|
|
775
|
+
* Optional override for the externalUrl the connector advertises.
|
|
776
|
+
* Defaults to the literal "auto" when unset, which makes the connector
|
|
777
|
+
* resolve the .anyone hostname from `${dir}/hostname` at startup.
|
|
778
|
+
* Set explicitly only when forcing a specific .anyone address (rare).
|
|
779
|
+
*/
|
|
780
|
+
externalUrl?: string;
|
|
781
|
+
/** Overall deadline for SOCKS port readiness (ms). Default 60000. */
|
|
782
|
+
startupTimeoutMs?: number;
|
|
783
|
+
/** Overall deadline for `sdk.stop()` (ms). Default 10000. */
|
|
784
|
+
stopTimeoutMs?: number;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Runtime configuration for the standalone ILP connector.
|
|
788
|
+
* Generated by ConnectorConfigGenerator from TownhouseConfig.
|
|
789
|
+
*/
|
|
790
|
+
interface ConnectorRuntimeConfig {
|
|
791
|
+
/** Admin API listen port */
|
|
792
|
+
adminPort: number;
|
|
793
|
+
/** Base ILP address for this connector */
|
|
794
|
+
ilpAddress: string;
|
|
795
|
+
/** List of child peer entries (one per active node) */
|
|
796
|
+
peers: PeerEntry[];
|
|
797
|
+
/** Transport configuration */
|
|
798
|
+
transport: {
|
|
799
|
+
/**
|
|
800
|
+
* Operator-facing transport selection. Maps to the connector's
|
|
801
|
+
* discriminated union internally (mode='hs' → type='socks5').
|
|
802
|
+
*/
|
|
803
|
+
mode: 'hs' | 'direct';
|
|
804
|
+
/** SOCKS5 proxy URL (must be socks5h://) — required when mode='hs'. */
|
|
805
|
+
socksProxy?: string;
|
|
806
|
+
/**
|
|
807
|
+
* Externally reachable BTP URL the connector advertises to peers.
|
|
808
|
+
* Required when mode='hs' AND hiddenService is unset (operator runs
|
|
809
|
+
* their own anon binary externally). Ignored for mode='direct'.
|
|
810
|
+
* When hiddenService IS set and externalUrl is unset, the generator
|
|
811
|
+
* emits the literal "auto" so the connector resolves the .anyone
|
|
812
|
+
* hostname at startup.
|
|
813
|
+
*/
|
|
814
|
+
externalUrl?: string;
|
|
815
|
+
/** Inbound hidden-service publication (Story 35.5 path). */
|
|
816
|
+
hiddenService?: HiddenServiceConfig;
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Response from GET /health on the connector's healthCheckPort.
|
|
821
|
+
* Mirrors `HealthStatus` from `@toon-protocol/connector`.
|
|
822
|
+
*/
|
|
823
|
+
interface HealthResponse {
|
|
824
|
+
status: 'healthy' | 'unhealthy' | 'starting' | 'degraded';
|
|
825
|
+
uptime: number;
|
|
826
|
+
peersConnected: number;
|
|
827
|
+
totalPeers: number;
|
|
828
|
+
timestamp: string;
|
|
829
|
+
nodeId?: string;
|
|
830
|
+
version?: string;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Per-peer ILP counter snapshot from GET /admin/metrics.json.
|
|
834
|
+
* Mirrors `AdminMetricsJsonPeer` from `@toon-protocol/connector`.
|
|
835
|
+
*/
|
|
836
|
+
interface MetricsPeerEntry {
|
|
837
|
+
peerId: string;
|
|
838
|
+
connected: boolean;
|
|
839
|
+
packetsForwarded: number;
|
|
840
|
+
packetsRejected: number;
|
|
841
|
+
bytesSent: number;
|
|
842
|
+
/** Connector v3.7.0+ (toon-protocol/connector#73). Counts packets accepted
|
|
843
|
+
* via the self-delivery code path (route nextHop === connector's own
|
|
844
|
+
* nodeId). Older connectors omit this field — default to 0 at consumers. */
|
|
845
|
+
packetsLocallyDelivered?: number;
|
|
846
|
+
lastPacketAt: string | null;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Response from GET /admin/metrics.json on the connector's adminApi port.
|
|
850
|
+
* Mirrors `AdminMetricsJsonResponse` from `@toon-protocol/connector`.
|
|
851
|
+
*/
|
|
852
|
+
interface MetricsResponse {
|
|
853
|
+
uptimeSeconds: number;
|
|
854
|
+
aggregate: {
|
|
855
|
+
packetsForwarded: number;
|
|
856
|
+
packetsRejected: number;
|
|
857
|
+
bytesSent: number;
|
|
858
|
+
/** Connector v3.7.0+ — see MetricsPeerEntry.packetsLocallyDelivered. */
|
|
859
|
+
packetsLocallyDelivered?: number;
|
|
860
|
+
};
|
|
861
|
+
peers: MetricsPeerEntry[];
|
|
862
|
+
timestamp: string;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Peer entry from GET /admin/peers on the connector's adminApi port.
|
|
866
|
+
* Mirrors the response of the connector's `router.get('/peers', …)` handler.
|
|
867
|
+
* Note: per-peer packet counters live on /admin/metrics.json, not here.
|
|
868
|
+
*/
|
|
869
|
+
interface PeerStatus {
|
|
870
|
+
id: string;
|
|
871
|
+
connected: boolean;
|
|
872
|
+
ilpAddresses: string[];
|
|
873
|
+
routeCount: number;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Wrapper response from GET /admin/peers.
|
|
877
|
+
* Mirrors the connector's `router.get('/peers')` JSON envelope.
|
|
878
|
+
*/
|
|
879
|
+
interface PeersResponse {
|
|
880
|
+
nodeId: string;
|
|
881
|
+
peerCount: number;
|
|
882
|
+
connectedCount: number;
|
|
883
|
+
peers: PeerStatus[];
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Channel summary entry from GET /admin/channels on the connector's adminApi port.
|
|
887
|
+
* Mirrors `ChannelSummary` from `@toon-protocol/connector`
|
|
888
|
+
* (packages/connector/src/http/admin-api.ts ChannelSummary at v3.x).
|
|
889
|
+
*
|
|
890
|
+
* `status` is kept as `string` (not a union) because the connector's enum may
|
|
891
|
+
* grow without warning — the contract canary catches shape drift, not enum domain.
|
|
892
|
+
*/
|
|
893
|
+
interface ChannelSummary {
|
|
894
|
+
channelId: string;
|
|
895
|
+
peerId: string;
|
|
896
|
+
chain: string;
|
|
897
|
+
status: string;
|
|
898
|
+
deposit: string;
|
|
899
|
+
lastActivity: string;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Filter params for GET /packets on the connector admin API.
|
|
903
|
+
*
|
|
904
|
+
* Townhouse-Side Contract (§getPacketLog) — added in story 21.10.
|
|
905
|
+
* The connector must expose `GET /packets?ilpAddress=<>&since=<>&limit=<>`.
|
|
906
|
+
* If the running connector image does not expose this endpoint, the
|
|
907
|
+
* /nodes/:type/packets/timeseries route returns 503 and the contract canary
|
|
908
|
+
* will catch the drift. See packages/sdk/CONNECTOR_MIGRATION.md §Townhouse-Side Contract.
|
|
909
|
+
*/
|
|
910
|
+
interface PacketLogFilter {
|
|
911
|
+
ilpAddress?: string;
|
|
912
|
+
since?: number;
|
|
913
|
+
limit?: number;
|
|
914
|
+
}
|
|
915
|
+
/** A single packet log entry returned by GET /packets */
|
|
916
|
+
interface PacketLogEntry {
|
|
917
|
+
ts: number;
|
|
918
|
+
ilpAddressFrom: string;
|
|
919
|
+
ilpAddressTo: string;
|
|
920
|
+
amount: string;
|
|
921
|
+
result: 'fulfill' | 'reject' | 'timeout';
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Response from GET /admin/hs-hostname on the connector's adminApi port.
|
|
925
|
+
* Mirrors the connector v3.5.0+ `HsHostnameResponse` (Story 44.1 / AC FR35).
|
|
926
|
+
* Both fields are null while the .anyone hidden-service bootstrap is in
|
|
927
|
+
* progress; both become non-null once the descriptor is published.
|
|
928
|
+
*/
|
|
929
|
+
interface HsHostnameResponse {
|
|
930
|
+
hostname: string | null;
|
|
931
|
+
publishedAt: string | null;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Per-asset earnings row from GET /admin/earnings.json (endpoint added in connector v3.2.0).
|
|
936
|
+
* Mirrors `AdminEarningsByAsset` from `@toon-protocol/connector` admin-api.
|
|
937
|
+
*
|
|
938
|
+
* Cumulative amounts are decimal-string bigints (JSON-safe for any asset scale).
|
|
939
|
+
* `claimsReceivedTotal` tracks value received from the peer (they paid us);
|
|
940
|
+
* `claimsSentTotal` tracks value forwarded to the peer (they earned).
|
|
941
|
+
*/
|
|
942
|
+
interface AssetEarnings {
|
|
943
|
+
assetCode: string;
|
|
944
|
+
assetScale: number;
|
|
945
|
+
claimsReceivedTotal: string;
|
|
946
|
+
claimsSentTotal: string;
|
|
947
|
+
netBalance: string;
|
|
948
|
+
lastClaimAt: string | null;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Per-peer earnings entry from GET /admin/earnings.json.
|
|
952
|
+
* Mirrors `AdminEarningsJsonPeer` from
|
|
953
|
+
* `@toon-protocol/connector packages/connector/src/http/admin-api.ts:278-281`.
|
|
954
|
+
*/
|
|
955
|
+
interface PeerEarnings {
|
|
956
|
+
peerId: string;
|
|
957
|
+
byAsset: AssetEarnings[];
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Connector fee rollup from GET /admin/earnings.json.
|
|
961
|
+
* Mirrors `AdminEarningsConnectorFee` from
|
|
962
|
+
* `@toon-protocol/connector packages/connector/src/http/admin-api.ts:283-287`.
|
|
963
|
+
*/
|
|
964
|
+
interface ConnectorFeeEntry {
|
|
965
|
+
assetCode: string;
|
|
966
|
+
assetScale: number;
|
|
967
|
+
total: string;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* A single recent claim entry from GET /admin/earnings.json.
|
|
971
|
+
* Mirrors `AdminEarningsRecentClaim` from
|
|
972
|
+
* `@toon-protocol/connector packages/connector/src/http/admin-api.ts:289-296`.
|
|
973
|
+
*/
|
|
974
|
+
interface RecentClaim {
|
|
975
|
+
peerId: string;
|
|
976
|
+
assetCode: string;
|
|
977
|
+
assetScale: number;
|
|
978
|
+
amount: string;
|
|
979
|
+
direction: 'inbound' | 'outbound';
|
|
980
|
+
at: string;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Typed wrapper around the connector's wire-level `timestamp: string` field.
|
|
984
|
+
* The connector's `AdminEarningsJsonResponse` carries a plain ISO-8601 string
|
|
985
|
+
* (`packages/connector/src/http/admin-api.ts:298-304`). Townhouse wraps it as
|
|
986
|
+
* a value object so dashboard consumers have a typed handle and the field can
|
|
987
|
+
* be extended (`epoch?`, `timezone?`) without breaking callers.
|
|
988
|
+
*
|
|
989
|
+
* `ConnectorAdminClient.getEarnings()` performs the wrap on the way out:
|
|
990
|
+
* `timestamp: { iso: rawString }`.
|
|
991
|
+
*/
|
|
992
|
+
interface EarningsTimestamp {
|
|
993
|
+
iso: string;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Response from GET /admin/earnings.json on the connector's adminApi port.
|
|
997
|
+
* Mirrors `AdminEarningsJsonResponse` from
|
|
998
|
+
* `@toon-protocol/connector packages/connector/src/http/admin-api.ts:298-304`.
|
|
999
|
+
*
|
|
1000
|
+
* NOTE: The connector's wire shape carries `timestamp: string`. Townhouse's
|
|
1001
|
+
* `getEarnings()` adapts it to `timestamp: EarningsTimestamp` (value object).
|
|
1002
|
+
*
|
|
1003
|
+
* Returns HTTP 503 when the connector is started without settlement config
|
|
1004
|
+
* (accountManager / claimReceiver not wired). Townhouse's apex always wires
|
|
1005
|
+
* both; 503 in production indicates connector misconfiguration.
|
|
1006
|
+
*/
|
|
1007
|
+
interface EarningsResponse {
|
|
1008
|
+
uptimeSeconds: number;
|
|
1009
|
+
peers: PeerEarnings[];
|
|
1010
|
+
connectorFees: ConnectorFeeEntry[];
|
|
1011
|
+
recentClaims: RecentClaim[];
|
|
1012
|
+
timestamp: EarningsTimestamp;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Connector Admin Client for Townhouse (Story 21.3, contract aligned in 21.7.5).
|
|
1017
|
+
*
|
|
1018
|
+
* HTTP client for the connector's admin API endpoints. Paths and response
|
|
1019
|
+
* shapes mirror the connector source-of-truth — see
|
|
1020
|
+
* `@toon-protocol/connector` `packages/connector/src/http/{types,admin-api}.ts`.
|
|
1021
|
+
*
|
|
1022
|
+
* Uses Node.js native fetch (available in Node 20+).
|
|
1023
|
+
*
|
|
1024
|
+
* Two distinct HTTP servers live on the connector image:
|
|
1025
|
+
* - healthCheckPort serves /health and /health/{live,ready}
|
|
1026
|
+
* - adminApi.port serves /admin/* (peers, metrics.json, routes, channels, …)
|
|
1027
|
+
*
|
|
1028
|
+
* The base URL passed to this client must point at whichever server hosts
|
|
1029
|
+
* the endpoint being called: pass the healthCheckPort base for `getHealth`
|
|
1030
|
+
* and the adminApi.port base for `getPeers` / `getMetrics`. In practice
|
|
1031
|
+
* Townhouse currently runs both ports on the same host, so callers either
|
|
1032
|
+
* construct two clients or hit a shared base URL when the ports overlap.
|
|
1033
|
+
*/
|
|
1034
|
+
|
|
1035
|
+
declare class ConnectorAdminClient {
|
|
1036
|
+
private readonly baseUrl;
|
|
1037
|
+
private readonly timeoutMs;
|
|
1038
|
+
/**
|
|
1039
|
+
* @param baseUrl - Base URL for the connector admin API (e.g., 'http://localhost:9402')
|
|
1040
|
+
* @param timeoutMs - Request timeout in milliseconds (default: 5000)
|
|
1041
|
+
*/
|
|
1042
|
+
constructor(baseUrl: string, timeoutMs?: number);
|
|
1043
|
+
/** Public read of the configured base URL (used by drill-command probes to derive a sibling client). */
|
|
1044
|
+
getBaseUrl(): string;
|
|
1045
|
+
/**
|
|
1046
|
+
* GET /health on the admin-API port — checks HTTP reachability of the
|
|
1047
|
+
* connector without validating the rich HealthStatus shape. Use this from
|
|
1048
|
+
* the drill-command health probe when only the admin URL is available
|
|
1049
|
+
* (port 9401), not the healthCheckPort (8080). The admin server's /health
|
|
1050
|
+
* returns `{status:'healthy', service:'admin-api', nodeId, timestamp}` —
|
|
1051
|
+
* a different shape from `getHealth()`'s validator. This method returns
|
|
1052
|
+
* a coarse status from a 200 response and reads `nodeId` if present.
|
|
1053
|
+
*
|
|
1054
|
+
* @throws Error when connector is unreachable or returns non-2xx.
|
|
1055
|
+
*/
|
|
1056
|
+
pingAdminLive(): Promise<{
|
|
1057
|
+
status: 'healthy';
|
|
1058
|
+
nodeId?: string;
|
|
1059
|
+
}>;
|
|
1060
|
+
/**
|
|
1061
|
+
* GET /health — returns the connector's HealthStatus from the healthCheckPort server.
|
|
1062
|
+
*
|
|
1063
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
1064
|
+
*/
|
|
1065
|
+
getHealth(): Promise<HealthResponse>;
|
|
1066
|
+
/**
|
|
1067
|
+
* GET /admin/hs-hostname — returns the connector's published .anyone hidden-service
|
|
1068
|
+
* hostname (Epic 45 / Story 44.1). Returns 200 with {hostname, publishedAt} both
|
|
1069
|
+
* possibly null while bootstrap is in progress, both non-null once anon publishes.
|
|
1070
|
+
* Returns 503 when the connector is anon-disabled (anon.enabled: false in config).
|
|
1071
|
+
*
|
|
1072
|
+
* @throws Error('connector is anon-disabled (HTTP 503)') on 503 — caller can match
|
|
1073
|
+
* on this exact prefix for actionable diagnostics.
|
|
1074
|
+
* @throws Error on non-200/503 status, network error, or shape-validation failure.
|
|
1075
|
+
*/
|
|
1076
|
+
getHsHostname(): Promise<HsHostnameResponse>;
|
|
1077
|
+
/**
|
|
1078
|
+
* GET /admin/metrics.json — returns the connector's per-peer ILP counters
|
|
1079
|
+
* with an aggregate rollup, mirroring `AdminMetricsJsonResponse`.
|
|
1080
|
+
*
|
|
1081
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
1082
|
+
*/
|
|
1083
|
+
getMetrics(): Promise<MetricsResponse>;
|
|
1084
|
+
/**
|
|
1085
|
+
* GET /admin/earnings.json — returns the connector's per-peer per-asset
|
|
1086
|
+
* earnings projection, mirroring `AdminEarningsJsonResponse` (connector v3.2.0+).
|
|
1087
|
+
*
|
|
1088
|
+
* Source of truth: @toon-protocol/connector
|
|
1089
|
+
* packages/connector/src/http/admin-api.ts:1864-1945
|
|
1090
|
+
*
|
|
1091
|
+
* Returns HTTP 503 when the connector is started without settlement config
|
|
1092
|
+
* (accountManager / claimReceiver not wired). Townhouse's apex always wires
|
|
1093
|
+
* both; 503 in production indicates connector misconfiguration.
|
|
1094
|
+
*
|
|
1095
|
+
* Wire-shape adaptation: the connector's `timestamp: string` field is
|
|
1096
|
+
* wrapped into `{ iso: string }` on the way out (EarningsTimestamp).
|
|
1097
|
+
*
|
|
1098
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
1099
|
+
*/
|
|
1100
|
+
getEarnings(): Promise<EarningsResponse>;
|
|
1101
|
+
/**
|
|
1102
|
+
* GET /admin/peers — returns the connector's peer roster with route counts
|
|
1103
|
+
* and ILP addresses. Returns the unwrapped peers array (the wrapper's
|
|
1104
|
+
* nodeId / peerCount / connectedCount fields are dropped).
|
|
1105
|
+
*
|
|
1106
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
1107
|
+
*/
|
|
1108
|
+
getPeers(): Promise<PeerStatus[]>;
|
|
1109
|
+
/**
|
|
1110
|
+
* GET /admin/channels — returns the connector's payment-channel summaries
|
|
1111
|
+
* across all registered chain providers. Multi-chain: one entry per channel
|
|
1112
|
+
* regardless of chain.
|
|
1113
|
+
*
|
|
1114
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
1115
|
+
*/
|
|
1116
|
+
getChannels(): Promise<ChannelSummary[]>;
|
|
1117
|
+
/**
|
|
1118
|
+
* POST /admin/peers — register (or re-register, idempotent) a child peer
|
|
1119
|
+
* with the connector. Used by the boot reconciler (Story 46.1) to
|
|
1120
|
+
* re-register peers present in `nodes.yaml` but missing from the
|
|
1121
|
+
* connector's runtime peer roster (e.g., after a connector restart).
|
|
1122
|
+
*
|
|
1123
|
+
* The connector's POST /admin/peers handler treats a POST whose `id`
|
|
1124
|
+
* matches an existing peer as a re-registration (no-op for the peer
|
|
1125
|
+
* itself; routes are appended). A POST with a new `id` triggers
|
|
1126
|
+
* `addPeer()` and BTP connection setup.
|
|
1127
|
+
*
|
|
1128
|
+
* @param input.id - peer identifier (matches `nodes.yaml`'s `peerId` and
|
|
1129
|
+
* the connector's `PeerStatus.id`).
|
|
1130
|
+
* @param input.url - BTP WebSocket URL the connector dials. MUST start
|
|
1131
|
+
* with `ws://` or `wss://` (the connector validates this).
|
|
1132
|
+
* @param input.authToken - shared auth token; pass empty string for
|
|
1133
|
+
* internal Townhouse peers (no auth).
|
|
1134
|
+
* @param input.routes - optional ILP route prefixes to register against
|
|
1135
|
+
* this peer. The reconciler passes the peer's ilpAddress.
|
|
1136
|
+
* @param input.transport - optional per-peer transport selection
|
|
1137
|
+
* (connector >= 3.6.2). `'direct'` forces the connector to bypass the
|
|
1138
|
+
* global SOCKS5 transport for this peer, even when the apex itself
|
|
1139
|
+
* runs in `transport.type: socks5` mode. Required for Docker-sibling
|
|
1140
|
+
* peers in HS mode — the anon SOCKS5 proxy cannot resolve internal
|
|
1141
|
+
* Docker hostnames. When omitted, the peer inherits the connector's
|
|
1142
|
+
* global transport (back-compat with pre-3.6.2 connectors).
|
|
1143
|
+
*
|
|
1144
|
+
* @throws Error on non-2xx response, timeout, or connection refused.
|
|
1145
|
+
*/
|
|
1146
|
+
registerPeer(input: {
|
|
1147
|
+
id: string;
|
|
1148
|
+
url: string;
|
|
1149
|
+
authToken: string;
|
|
1150
|
+
routes?: {
|
|
1151
|
+
prefix: string;
|
|
1152
|
+
priority?: number;
|
|
1153
|
+
}[];
|
|
1154
|
+
transport?: 'direct' | 'socks5';
|
|
1155
|
+
relation?: 'parent' | 'peer' | 'child';
|
|
1156
|
+
}): Promise<void>;
|
|
1157
|
+
/**
|
|
1158
|
+
* DELETE /admin/peers/:peerId?removeRoutes=true — deregister a child peer.
|
|
1159
|
+
*
|
|
1160
|
+
* Idempotent: a 404 from the connector (peer already removed) is treated as
|
|
1161
|
+
* success so callers can safely use this as a rollback step without knowing
|
|
1162
|
+
* whether the peer was ever registered.
|
|
1163
|
+
*
|
|
1164
|
+
* `removeRoutes=true` is always sent so the connector drops the ILP routing
|
|
1165
|
+
* entries for this peer along with the BTP connection config.
|
|
1166
|
+
*
|
|
1167
|
+
* @throws Error on empty peerId (rejected at client, no network request made)
|
|
1168
|
+
* @throws Error on non-2xx/404 response, timeout, or connection refused
|
|
1169
|
+
*/
|
|
1170
|
+
removePeer(peerId: string): Promise<void>;
|
|
1171
|
+
/**
|
|
1172
|
+
* GET /packets — returns the connector's raw packet log filtered by the
|
|
1173
|
+
* given criteria. Used by the timeseries aggregation route (story 21.10).
|
|
1174
|
+
*
|
|
1175
|
+
* Townhouse-Side Contract: see packages/sdk/CONNECTOR_MIGRATION.md §getPacketLog.
|
|
1176
|
+
* If the connector image does not expose GET /packets, this method throws
|
|
1177
|
+
* with a `ConnectorEndpointNotFound` error code so the route can return 503.
|
|
1178
|
+
*
|
|
1179
|
+
* @throws Error with code='ConnectorEndpointNotFound' when connector returns 404
|
|
1180
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
1181
|
+
*/
|
|
1182
|
+
getPacketLog(filter?: PacketLogFilter): Promise<PacketLogEntry[]>;
|
|
1183
|
+
/**
|
|
1184
|
+
* Perform an HTTP GET request to the connector admin API.
|
|
1185
|
+
* Wraps fetch with error handling for connection refused and non-200 responses.
|
|
1186
|
+
*/
|
|
1187
|
+
private fetch;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Docker Orchestration Engine for Townhouse (Story 21.2).
|
|
1192
|
+
*
|
|
1193
|
+
* Manages the full container lifecycle: network creation, image pulling,
|
|
1194
|
+
* container creation/start/stop/removal, and health check polling.
|
|
1195
|
+
* Uses dockerode for programmatic Docker control with DI for testability.
|
|
1196
|
+
*/
|
|
1197
|
+
|
|
1198
|
+
interface RunDockerOptions {
|
|
1199
|
+
timeout?: number;
|
|
1200
|
+
maxBuffer?: number;
|
|
1201
|
+
inheritStdio?: boolean;
|
|
1202
|
+
/** Override the subprocess env. Defaults to process.env when omitted. */
|
|
1203
|
+
env?: NodeJS.ProcessEnv;
|
|
1204
|
+
}
|
|
1205
|
+
type ExecFileAsyncSignature = (file: string, args: readonly string[], options?: RunDockerOptions) => Promise<{
|
|
1206
|
+
stdout: string;
|
|
1207
|
+
stderr: string;
|
|
1208
|
+
}>;
|
|
1209
|
+
/**
|
|
1210
|
+
* Error thrown by DockerOrchestrator HS-path failures (Story 45.3).
|
|
1211
|
+
* Carries the failed-service name + subprocess diagnostics so CLI consumers
|
|
1212
|
+
* (Story 45.4) can render Sally's failure-state copy library (UX-DR5).
|
|
1213
|
+
*/
|
|
1214
|
+
declare class OrchestratorError extends Error {
|
|
1215
|
+
readonly service?: string;
|
|
1216
|
+
readonly exitCode?: number;
|
|
1217
|
+
readonly stderr?: string;
|
|
1218
|
+
constructor(message: string, options?: {
|
|
1219
|
+
service?: string;
|
|
1220
|
+
exitCode?: number;
|
|
1221
|
+
stderr?: string;
|
|
1222
|
+
cause?: Error;
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* DockerOrchestrator manages the lifecycle of Townhouse containers.
|
|
1227
|
+
*
|
|
1228
|
+
* Constructor accepts a dockerode instance (DI for testability) and config.
|
|
1229
|
+
* Emits typed events defined in OrchestratorEvents: pullProgress,
|
|
1230
|
+
* containerState, and healthCheck.
|
|
1231
|
+
*/
|
|
1232
|
+
declare class DockerOrchestrator extends EventEmitter {
|
|
1233
|
+
private readonly docker;
|
|
1234
|
+
private readonly config;
|
|
1235
|
+
private readonly configGenerator;
|
|
1236
|
+
private readonly walletManager;
|
|
1237
|
+
private activeNodes;
|
|
1238
|
+
private readonly statsCache;
|
|
1239
|
+
private readonly profile;
|
|
1240
|
+
private readonly composePath;
|
|
1241
|
+
private readonly execFileAsync;
|
|
1242
|
+
private readonly adminClientFactory;
|
|
1243
|
+
constructor(docker: Docker, config: TownhouseConfig, walletManager?: WalletManager, options?: {
|
|
1244
|
+
profile?: ComposeProfile;
|
|
1245
|
+
composePath?: string;
|
|
1246
|
+
execFileAsync?: ExecFileAsyncSignature;
|
|
1247
|
+
adminClientFactory?: (baseUrl: string, timeoutMs: number) => ConnectorAdminClient;
|
|
1248
|
+
});
|
|
1249
|
+
/**
|
|
1250
|
+
* Orchestrate full startup sequence. Branches on profile:
|
|
1251
|
+
* - 'dev' (default): dockerode-based, preserves existing dev-stack behavior
|
|
1252
|
+
* - 'hs': docker compose subprocess + HS hostname readiness gate
|
|
1253
|
+
* - 'direct': docker compose subprocess + connector /health readiness gate
|
|
1254
|
+
*/
|
|
1255
|
+
up(profiles: NodeType[]): Promise<void>;
|
|
1256
|
+
private upDev;
|
|
1257
|
+
/**
|
|
1258
|
+
* Narrow `this.composePath` to a definite string. The constructor enforces
|
|
1259
|
+
* this invariant for `profile: 'hs'`; this helper exists so the HS-path
|
|
1260
|
+
* methods don't need a non-null assertion (lint-clean) and so a constructor
|
|
1261
|
+
* regression surfaces as an `OrchestratorError` rather than a `TypeError`.
|
|
1262
|
+
*/
|
|
1263
|
+
private requireComposePath;
|
|
1264
|
+
/**
|
|
1265
|
+
* validate that composePath is absolute and exists on disk before
|
|
1266
|
+
* passing it to any subprocess call. Defence-in-depth — callers pass paths
|
|
1267
|
+
* from materializeComposeTemplate so this should never fire in normal use.
|
|
1268
|
+
*/
|
|
1269
|
+
private validateComposePath;
|
|
1270
|
+
/**
|
|
1271
|
+
* Shared compose `up -d` for the compose-driven profiles ('hs', 'direct').
|
|
1272
|
+
* Validates the composePath, builds the profile-gated args, and runs
|
|
1273
|
+
* `docker compose up -d`, surfacing failures the same way for both profiles.
|
|
1274
|
+
* Does NOT apply any readiness gate — the caller layers that on (HS hostname
|
|
1275
|
+
* for 'hs'; connector /health for 'direct').
|
|
1276
|
+
*/
|
|
1277
|
+
private composeUp;
|
|
1278
|
+
/** HS-mode startup: shell out to `docker compose up -d`, wait for HS hostname. */
|
|
1279
|
+
private upHs;
|
|
1280
|
+
/**
|
|
1281
|
+
* Direct-mode startup: shell out to `docker compose up -d`, then gate on the
|
|
1282
|
+
* connector's admin `/health` being reachable (no HS hostname to wait for).
|
|
1283
|
+
*
|
|
1284
|
+
* Readiness probe choice: a plain `:3000` BTP WebSocket-upgrade probe is
|
|
1285
|
+
* awkward to do portably (it needs a ws client + the BTP auth handshake), so
|
|
1286
|
+
* — as the plan permits — readiness here is the connector admin `/health`
|
|
1287
|
+
* returning 200 (via `ConnectorAdminClient.pingAdminLive`, the same admin
|
|
1288
|
+
* client the HS path uses). The compose healthcheck already gates the BTP
|
|
1289
|
+
* port: docker only reports the connector `healthy` once its admin `/health`
|
|
1290
|
+
* (port 9401) responds, and the connector binds its BTP server (:3000) and
|
|
1291
|
+
* admin server together at boot, so a healthy admin endpoint implies the BTP
|
|
1292
|
+
* listener is up. We poll the admin endpoint from the host (the host-mapped
|
|
1293
|
+
* 127.0.0.1:9401) rather than inspecting container health so the gate works
|
|
1294
|
+
* even when invoked against a remote daemon.
|
|
1295
|
+
*/
|
|
1296
|
+
private upDirect;
|
|
1297
|
+
/**
|
|
1298
|
+
* Poll the connector admin `/health` (host-mapped 127.0.0.1:<adminPort>) until
|
|
1299
|
+
* it returns 200 or the deadline elapses. Used as the direct-mode readiness
|
|
1300
|
+
* gate. Uses a monotonic clock so suspend/resume cannot stretch the timeout.
|
|
1301
|
+
*/
|
|
1302
|
+
private waitForConnectorHealth;
|
|
1303
|
+
/**
|
|
1304
|
+
* Parse Docker Compose stderr for failed-service names and emit a
|
|
1305
|
+
* containerState event per failed service so callers see the failure via
|
|
1306
|
+
* the same channel dev-mode uses (AC #6 — "for each failed service
|
|
1307
|
+
* identified, it emits..."). When no pattern matches, emit a single
|
|
1308
|
+
* fallback event with name `'compose-up'`.
|
|
1309
|
+
*/
|
|
1310
|
+
private surfaceComposeFailure;
|
|
1311
|
+
private waitForHsHostname;
|
|
1312
|
+
/**
|
|
1313
|
+
* Regenerate connector config and restart the connector container
|
|
1314
|
+
* with updated environment variables (peer list).
|
|
1315
|
+
*
|
|
1316
|
+
* Sequence: emit connectorRestarting -> stop -> remove -> create -> start -> health -> emit connectorRestarted
|
|
1317
|
+
*/
|
|
1318
|
+
regenerateConnectorConfig(activeNodes: NodeType[]): Promise<void>;
|
|
1319
|
+
/**
|
|
1320
|
+
* Hot-add a node after initial startup.
|
|
1321
|
+
* Starts the node container, then restarts the connector with updated peer list.
|
|
1322
|
+
*/
|
|
1323
|
+
addNode(type: NodeType): Promise<void>;
|
|
1324
|
+
/**
|
|
1325
|
+
* Hot-remove a node.
|
|
1326
|
+
* Stops the node container, then restarts the connector with updated peer list.
|
|
1327
|
+
*/
|
|
1328
|
+
removeNode(type: NodeType): Promise<void>;
|
|
1329
|
+
/**
|
|
1330
|
+
* Graceful shutdown. Branches on profile:
|
|
1331
|
+
* - 'dev' (default): dockerode-based teardown
|
|
1332
|
+
* - 'hs': docker compose subprocess
|
|
1333
|
+
*/
|
|
1334
|
+
down(): Promise<void>;
|
|
1335
|
+
private downDev;
|
|
1336
|
+
private downHs;
|
|
1337
|
+
/**
|
|
1338
|
+
* Resolve the Nostr relay WebSocket URL for a Town node instance.
|
|
1339
|
+
*
|
|
1340
|
+
* Inspects the container's port bindings to get the host-bound port for
|
|
1341
|
+
* the relay WebSocket (7100/tcp). Falls back to the Docker-internal URL
|
|
1342
|
+
* when the server is running inside the Docker network or bindings are absent.
|
|
1343
|
+
*
|
|
1344
|
+
* @param nodeId - The `NodeInfo.id` value (e.g. 'town', 'dev-town-01')
|
|
1345
|
+
*/
|
|
1346
|
+
getNodeRelayEndpoint(nodeId: string): Promise<string>;
|
|
1347
|
+
/**
|
|
1348
|
+
* Resolve the BLS health HTTP URL for a node instance.
|
|
1349
|
+
*
|
|
1350
|
+
* Inspects the container's port bindings to find the host-bound port for the
|
|
1351
|
+
* node's health endpoint. Falls back to Docker-internal URL when running
|
|
1352
|
+
* inside the Docker network or when bindings are absent.
|
|
1353
|
+
*
|
|
1354
|
+
* @param nodeId - The `NodeInfo.id` value (e.g. 'mill', 'dev-mill-01')
|
|
1355
|
+
* @param type - Node type (determines which internal port to use)
|
|
1356
|
+
*/
|
|
1357
|
+
getNodeHealthEndpoint(nodeId: string, type: 'town' | 'mill' | 'dvm'): Promise<string>;
|
|
1358
|
+
/**
|
|
1359
|
+
* Fetch network I/O stats for a container.
|
|
1360
|
+
* Results are cached for 5 seconds to avoid per-request Docker API overhead.
|
|
1361
|
+
*
|
|
1362
|
+
* @param containerName - Full container name (e.g. 'townhouse-town')
|
|
1363
|
+
* @returns Bandwidth stats or null when container is not running
|
|
1364
|
+
*/
|
|
1365
|
+
getContainerStats(containerName: string): Promise<BandwidthStats | null>;
|
|
1366
|
+
/**
|
|
1367
|
+
* Return status for all townhouse containers.
|
|
1368
|
+
*
|
|
1369
|
+
* Discovers both single-instance (townhouse-<type>) and multi-instance
|
|
1370
|
+
* (townhouse-<prefix>-<type>-<n>) containers. Multi-instance containers
|
|
1371
|
+
* are returned with a `name` matching their instance suffix so callers
|
|
1372
|
+
* can build per-instance NodeInfo entries (e.g. "dev-town-01").
|
|
1373
|
+
*/
|
|
1374
|
+
status(): Promise<{
|
|
1375
|
+
name: string;
|
|
1376
|
+
type: 'connector' | 'town' | 'mill' | 'dvm';
|
|
1377
|
+
state: string;
|
|
1378
|
+
health?: string;
|
|
1379
|
+
startedAt?: string;
|
|
1380
|
+
}[]>;
|
|
1381
|
+
/**
|
|
1382
|
+
* Pull required images before starting containers.
|
|
1383
|
+
* Skips images that already exist locally.
|
|
1384
|
+
* Emits pullProgress events during download.
|
|
1385
|
+
*/
|
|
1386
|
+
pullImages(profiles: NodeType[]): Promise<void>;
|
|
1387
|
+
/**
|
|
1388
|
+
* Pull a single image by its reference (tag or digest form).
|
|
1389
|
+
*
|
|
1390
|
+
* Skips the pull when the image already exists locally (matches against
|
|
1391
|
+
* both RepoTags and RepoDigests so digest-form refs like
|
|
1392
|
+
* `ghcr.io/toon-protocol/town@sha256:abc...` are found correctly).
|
|
1393
|
+
* Throws `OrchestratorError` on pull failure.
|
|
1394
|
+
*/
|
|
1395
|
+
pullImage(image: string): Promise<void>;
|
|
1396
|
+
/**
|
|
1397
|
+
* Start a child peer node via `docker compose --profile <type> up -d <type>`.
|
|
1398
|
+
*
|
|
1399
|
+
* HS-profile only — throws `OrchestratorError` when called on the dev profile.
|
|
1400
|
+
*
|
|
1401
|
+
* The `env` parameter supplies the per-node wallet secrets (e.g.
|
|
1402
|
+
* `TOWN_SECRET_KEY`, `MILL_MNEMONIC`). It is layered on top of `process.env`
|
|
1403
|
+
* so that PATH, HOME, and other process-level env vars are preserved for the
|
|
1404
|
+
* docker CLI subprocess.
|
|
1405
|
+
*
|
|
1406
|
+
* Logging guard: the caller (nodes-lifecycle route) must NOT log the `env`
|
|
1407
|
+
* argument — it contains secret keys and the wallet mnemonic.
|
|
1408
|
+
*/
|
|
1409
|
+
startNodeViaCompose(type: NodeType, env: Record<string, string>): Promise<void>;
|
|
1410
|
+
/**
|
|
1411
|
+
* Stop and remove a child peer node via `docker compose stop` + `rm -f`.
|
|
1412
|
+
*
|
|
1413
|
+
* HS-profile only — throws `OrchestratorError` when called on the dev profile.
|
|
1414
|
+
* Idempotent: stderr patterns indicating the service/container is already gone
|
|
1415
|
+
* (`'no such service'`, `'no containers to remove'`, `'No such container'`)
|
|
1416
|
+
* are treated as success so callers can run this as a rollback without
|
|
1417
|
+
* worrying about the container's prior state.
|
|
1418
|
+
*/
|
|
1419
|
+
stopNodeViaCompose(type: NodeType): Promise<void>;
|
|
1420
|
+
/**
|
|
1421
|
+
* Poll container health status via inspect().
|
|
1422
|
+
* Retries at configurable interval, throws on timeout.
|
|
1423
|
+
*/
|
|
1424
|
+
healthCheck(containerName: string, options?: HealthCheckOptions): Promise<string>;
|
|
1425
|
+
/**
|
|
1426
|
+
* Create the townhouse-net bridge network if it doesn't exist.
|
|
1427
|
+
*/
|
|
1428
|
+
private ensureNetwork;
|
|
1429
|
+
/**
|
|
1430
|
+
* Start the connector container — always runs first.
|
|
1431
|
+
*
|
|
1432
|
+
* The connector image at 3.3.x reads its config from a YAML file pointed
|
|
1433
|
+
* to by the `CONFIG_FILE` env var (default `./config.yaml`). We write the
|
|
1434
|
+
* generated YAML to `<configDir>/connector.yaml` (sibling to wallet.enc),
|
|
1435
|
+
* mount it as `/config/connector.yaml`, and set CONFIG_FILE accordingly.
|
|
1436
|
+
*
|
|
1437
|
+
* (Env-var-based config was set on the container historically but the
|
|
1438
|
+
* connector image silently ignored them — see the YAML fix landing with
|
|
1439
|
+
* this comment block.)
|
|
1440
|
+
*/
|
|
1441
|
+
private startConnector;
|
|
1442
|
+
/**
|
|
1443
|
+
* Start a node container (town, mill, or dvm).
|
|
1444
|
+
* Retries up to MAX_START_RETRIES on failure.
|
|
1445
|
+
*/
|
|
1446
|
+
private startNode;
|
|
1447
|
+
/**
|
|
1448
|
+
* Wait for a container's health check to pass.
|
|
1449
|
+
*/
|
|
1450
|
+
private waitForHealth;
|
|
1451
|
+
/**
|
|
1452
|
+
* Start the relay-side ator sidecar that publishes a v3 hidden service
|
|
1453
|
+
* forwarding inbound traffic to the town container's Nostr WebSocket port.
|
|
1454
|
+
*
|
|
1455
|
+
* The keypair directory is mounted read-write because the sidecar's
|
|
1456
|
+
* entrypoint writes the `hostname` file on first boot (see
|
|
1457
|
+
* docker/townhouse-ator-sidecar/Dockerfile). The town container picks up
|
|
1458
|
+
* the resulting .anyone URL via the operator-set externalUrl field.
|
|
1459
|
+
*/
|
|
1460
|
+
/**
|
|
1461
|
+
* Ensure the relay hidden-service sidecar is running (HS mode). Forwards the
|
|
1462
|
+
* relay `.anyone` HS to the town container's Nostr port (7100) so external
|
|
1463
|
+
* clients can READ over the hidden service. Idempotent: a no-op if already
|
|
1464
|
+
* running; recreates a stale/exited one. The keypair persists in a named
|
|
1465
|
+
* volume → stable address across `hs down`/`up`. Requires the town container
|
|
1466
|
+
* to exist (the sidecar resolves its DNS at boot), so call it AFTER the town
|
|
1467
|
+
* is (re)started.
|
|
1468
|
+
*/
|
|
1469
|
+
ensureRelaySidecar(): Promise<void>;
|
|
1470
|
+
/**
|
|
1471
|
+
* Read the relay sidecar's published `.anyone` hostname (it writes the file
|
|
1472
|
+
* once anon finishes bootstrapping). Polls until the file is non-empty or
|
|
1473
|
+
* `timeoutMs` elapses; returns null on timeout. The file already contains the
|
|
1474
|
+
* routable `.anyone` address (no scheme mapping needed).
|
|
1475
|
+
*/
|
|
1476
|
+
getRelayHsHostname(timeoutMs?: number): Promise<string | null>;
|
|
1477
|
+
/** Stop + remove the relay sidecar (called before `hs down`'s compose down). */
|
|
1478
|
+
removeRelaySidecar(): Promise<void>;
|
|
1479
|
+
/**
|
|
1480
|
+
* Stop and remove a single container.
|
|
1481
|
+
*/
|
|
1482
|
+
private stopAndRemove;
|
|
1483
|
+
/**
|
|
1484
|
+
* Remove the townhouse-net network if it exists.
|
|
1485
|
+
*/
|
|
1486
|
+
private removeNetwork;
|
|
1487
|
+
/**
|
|
1488
|
+
* Build environment variables for the connector container.
|
|
1489
|
+
* Delegates to ConnectorConfigGenerator for consistent config generation.
|
|
1490
|
+
*/
|
|
1491
|
+
private buildConnectorEnv;
|
|
1492
|
+
/**
|
|
1493
|
+
* Build environment variables for a node container.
|
|
1494
|
+
* If a WalletManager is provided, injects per-node identity keys.
|
|
1495
|
+
*
|
|
1496
|
+
* Async because the DVM path may need to derive an RSA-4096 Arweave key
|
|
1497
|
+
* via `walletManager.ensureArweaveKey('dvm')` — that derivation takes
|
|
1498
|
+
* 5–30s on first call per unlocked wallet (cached thereafter).
|
|
1499
|
+
*/
|
|
1500
|
+
private buildNodeEnv;
|
|
1501
|
+
/**
|
|
1502
|
+
* Follow a Docker pull stream and emit progress events.
|
|
1503
|
+
*/
|
|
1504
|
+
private followPullProgress;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
export { type ApiConfig as A, type BandwidthStats as B, type ComposeLoaderOptions as C, DockerOrchestrator as D, type EncryptedWallet as E, type WalletConfig as F, type WalletManagerConfig as G, type HealthCheckOptions as H, type WalletState as I, loadComposeTemplate as J, materializeComposeTemplate as K, type LoggingConfig as L, type MetricsPeerEntry as M, type NodeType as N, OrchestratorError as O, type PacketLogEntry as P, type RecentClaim as R, type SolanaChainProvider as S, type TownhouseConfig as T, WalletManager as W, type ConnectorRuntimeConfig as a, ConnectorAdminClient as b, type ChainProviderEntry as c, type ChainType as d, ComposeLoaderError as e, type ComposeProfile as f, type ConnectorConfig as g, type ContainerSpec as h, type DerivedNodeKeys as i, type DvmNodeConfig as j, type EvmChainProvider as k, type HealthResponse as l, type HsHostnameResponse as m, type MetricsResponse as n, type MillNodeConfig as o, type MinaChainProvider as p, type NodeKeyInfo as q, type NodeKeys as r, type NodesConfig as s, type OrchestratorEvents as t, type PacketLogFilter as u, type PeerEntry as v, type PeerStatus as w, type PeersResponse as x, type TownNodeConfig as y, type TransportConfig as z };
|