@toon-protocol/client-mcp 0.1.0

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.
Files changed (41) hide show
  1. package/README.md +302 -0
  2. package/dist/anon-proxy-W3KMM7GU-FN7ZJY7P.js +25 -0
  3. package/dist/anon-proxy-W3KMM7GU-FN7ZJY7P.js.map +1 -0
  4. package/dist/chunk-32QD72IL.js +83 -0
  5. package/dist/chunk-32QD72IL.js.map +1 -0
  6. package/dist/chunk-3NAWISI5.js +2245 -0
  7. package/dist/chunk-3NAWISI5.js.map +1 -0
  8. package/dist/chunk-5KFXUT5Q.js +11321 -0
  9. package/dist/chunk-5KFXUT5Q.js.map +1 -0
  10. package/dist/chunk-F22GNSF6.js +12 -0
  11. package/dist/chunk-F22GNSF6.js.map +1 -0
  12. package/dist/chunk-FSS45ZX3.js +2633 -0
  13. package/dist/chunk-FSS45ZX3.js.map +1 -0
  14. package/dist/chunk-LR7W2ISE.js +657 -0
  15. package/dist/chunk-LR7W2ISE.js.map +1 -0
  16. package/dist/chunk-SKQTKZIH.js +278 -0
  17. package/dist/chunk-SKQTKZIH.js.map +1 -0
  18. package/dist/chunk-VA7XC4FD.js +185 -0
  19. package/dist/chunk-VA7XC4FD.js.map +1 -0
  20. package/dist/chunk-ZQKYZJWT.js +359 -0
  21. package/dist/chunk-ZQKYZJWT.js.map +1 -0
  22. package/dist/daemon.d.ts +1 -0
  23. package/dist/daemon.js +141 -0
  24. package/dist/daemon.js.map +1 -0
  25. package/dist/ed25519-2QVPINLS.js +27 -0
  26. package/dist/ed25519-2QVPINLS.js.map +1 -0
  27. package/dist/gateway-QOK47RKS-SEGTXBR3.js +16 -0
  28. package/dist/gateway-QOK47RKS-SEGTXBR3.js.map +1 -0
  29. package/dist/hmac-26UC6YKX.js +12 -0
  30. package/dist/hmac-26UC6YKX.js.map +1 -0
  31. package/dist/index.d.ts +965 -0
  32. package/dist/index.js +68 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/mcp.d.ts +1 -0
  35. package/dist/mcp.js +81 -0
  36. package/dist/mcp.js.map +1 -0
  37. package/dist/sha512-WYC446ZM.js +34 -0
  38. package/dist/sha512-WYC446ZM.js.map +1 -0
  39. package/dist/socks5-WTJBYGME-6COK4LXW.js +139 -0
  40. package/dist/socks5-WTJBYGME-6COK4LXW.js.map +1 -0
  41. package/package.json +60 -0
@@ -0,0 +1,2245 @@
1
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
2
+ import {
3
+ DEFAULT_KEYSTORE_PASSWORD,
4
+ ILP_PEER_INFO_KIND,
5
+ ToonError,
6
+ buildIlpPrepare,
7
+ configDir,
8
+ decodeEventFromToon,
9
+ defaultConfigPath,
10
+ deriveFullIdentity,
11
+ encodeEventToToon,
12
+ generateKeystore,
13
+ isEventExpired,
14
+ parseIlpPeerInfo,
15
+ readConfigFile
16
+ } from "./chunk-5KFXUT5Q.js";
17
+ import {
18
+ startManagedAnonProxy
19
+ } from "./chunk-SKQTKZIH.js";
20
+ import {
21
+ __require
22
+ } from "./chunk-F22GNSF6.js";
23
+
24
+ // src/daemon/first-run.ts
25
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
26
+ import { dirname, join } from "path";
27
+ function defaultKeystorePath() {
28
+ return join(configDir(), "keystore.json");
29
+ }
30
+ function hasConfiguredIdentity(file) {
31
+ return Boolean(
32
+ process.env["TOON_CLIENT_MNEMONIC"]?.trim() || file.keystorePath || file.mnemonic
33
+ );
34
+ }
35
+ var CONFIG_HELP = {
36
+ transport: "Point btpUrl/relayUrl at your apex. For an .anyone hidden service use ws://<host>.anyone:3000/btp + ws://<host>.anyone:7100 \u2014 the daemon auto-routes through a managed anon proxy. For a direct apex use ws://<host>:3000/btp + ws://<host>:7100.",
37
+ btpUrl: "REQUIRED. BTP WebSocket URL of the apex/connector for paid writes.",
38
+ relayUrl: "Town relay WebSocket URL for FREE reads. Default ws://localhost:7100.",
39
+ managedAnonProxy: "Optional override \u2014 leave unset to auto-infer from btpUrl (.anyone host \u2192 managed anon proxy, anything else \u2192 direct).",
40
+ keystorePath: "Auto-generated encrypted identity. Do not hand-edit; back up your seed phrase."
41
+ };
42
+ async function scaffoldFirstRun(opts = {}) {
43
+ const log = opts.log ?? ((m) => console.error(m));
44
+ const configPath = opts.configPath ?? process.env["TOON_CLIENT_CONFIG"] ?? defaultConfigPath();
45
+ mkdirSync(dirname(configPath), { recursive: true });
46
+ const file = readConfigFile(configPath);
47
+ let updated = { ...file };
48
+ let changed = false;
49
+ if (!hasConfiguredIdentity(file)) {
50
+ const keystorePath = defaultKeystorePath();
51
+ const envPassword = process.env["TOON_CLIENT_KEYSTORE_PASSWORD"];
52
+ const password = envPassword ?? DEFAULT_KEYSTORE_PASSWORD;
53
+ let mnemonic;
54
+ if (existsSync(keystorePath)) {
55
+ mnemonic = "";
56
+ log(
57
+ `[toon-clientd] first run: relinking existing keystore at ${keystorePath}`
58
+ );
59
+ } else {
60
+ const generated = generateKeystore(keystorePath, password);
61
+ mnemonic = generated.mnemonic;
62
+ }
63
+ updated = {
64
+ ...updated,
65
+ keystorePath,
66
+ // Only flag auto-password when WE chose it, so a user-imported keystore
67
+ // (custom password) still requires the env var.
68
+ ...envPassword ? {} : { keystoreAutoPassword: true }
69
+ };
70
+ changed = true;
71
+ if (mnemonic) {
72
+ await printNewIdentity(mnemonic, keystorePath, Boolean(envPassword), log);
73
+ }
74
+ }
75
+ if (!existsSync(configPath)) {
76
+ updated = {
77
+ _help: CONFIG_HELP,
78
+ btpUrl: "",
79
+ relayUrl: "ws://localhost:7100",
80
+ ...updated
81
+ };
82
+ changed = true;
83
+ log(
84
+ `[toon-clientd] wrote starter config at ${configPath} \u2014 set "btpUrl" to your apex before publishing.`
85
+ );
86
+ }
87
+ if (changed) writeConfigFile(configPath, updated);
88
+ }
89
+ async function printNewIdentity(mnemonic, keystorePath, hasEnvPassword, log) {
90
+ const id = await deriveFullIdentity(mnemonic);
91
+ const lines = [
92
+ "",
93
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
94
+ " TOON client: generated a new identity (first run)",
95
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
96
+ ` Nostr pubkey : ${id.nostr.pubkey}`,
97
+ ` EVM address : ${id.evm.address}`,
98
+ ...id.solana.publicKey ? [` Solana : ${id.solana.publicKey}`] : [],
99
+ ...id.mina.publicKey ? [` Mina : ${id.mina.publicKey}`] : [],
100
+ "",
101
+ " Seed phrase (BACK THIS UP \u2014 shown only once):",
102
+ ` ${mnemonic}`,
103
+ "",
104
+ ` Encrypted keystore: ${keystorePath}`,
105
+ hasEnvPassword ? " Encrypted with TOON_CLIENT_KEYSTORE_PASSWORD." : " Encrypted with the default password (set TOON_CLIENT_KEYSTORE_PASSWORD\n + re-import to use your own). Identity reloads automatically on restart.",
106
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
107
+ ""
108
+ ];
109
+ log(lines.join("\n"));
110
+ }
111
+ function writeConfigFile(path, file) {
112
+ writeFileSync(path, JSON.stringify(file, null, 2) + "\n", {
113
+ encoding: "utf8",
114
+ mode: 384
115
+ });
116
+ }
117
+
118
+ // src/relay-subscription.ts
119
+ import { createRequire } from "module";
120
+ var nodeRequire = createRequire(import.meta.url);
121
+ var DEFAULT_BUFFER = 5e3;
122
+ var DEFAULT_BASE_MS = 1e3;
123
+ var DEFAULT_MAX_MS = 3e4;
124
+ var noop = () => void 0;
125
+ var RelaySubscription = class {
126
+ relayUrl;
127
+ socksProxy;
128
+ bufferSize;
129
+ reconnectBaseMs;
130
+ reconnectMaxMs;
131
+ log;
132
+ wsFactory;
133
+ decodeEvent;
134
+ onEvent;
135
+ /** Active subscriptions: subId -> filters (re-sent on every (re)connect). */
136
+ subscriptions = /* @__PURE__ */ new Map();
137
+ /** Ring buffer of received events, ordered by ascending `seq`. */
138
+ buffer = [];
139
+ /** De-dup index: event.id -> seq (kept in lockstep with the buffer). */
140
+ seen = /* @__PURE__ */ new Set();
141
+ seqCounter = 0;
142
+ subIdCounter = 0;
143
+ ws = null;
144
+ connected = false;
145
+ closing = false;
146
+ reconnectAttempts = 0;
147
+ reconnectTimer = null;
148
+ constructor(opts) {
149
+ this.relayUrl = opts.relayUrl;
150
+ this.socksProxy = opts.socksProxy;
151
+ this.bufferSize = opts.bufferSize ?? DEFAULT_BUFFER;
152
+ this.reconnectBaseMs = opts.reconnectBaseMs ?? DEFAULT_BASE_MS;
153
+ this.reconnectMaxMs = opts.reconnectMaxMs ?? DEFAULT_MAX_MS;
154
+ this.log = opts.logger ?? noop;
155
+ this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory(this.socksProxy);
156
+ this.decodeEvent = opts.decodeEvent;
157
+ this.onEvent = opts.onEvent;
158
+ }
159
+ /** Whether the underlying socket is currently open. */
160
+ isConnected() {
161
+ return this.connected;
162
+ }
163
+ /** Number of events currently held in the buffer. */
164
+ bufferedCount() {
165
+ return this.buffer.length;
166
+ }
167
+ /** Active subscription ids. */
168
+ activeSubscriptions() {
169
+ return [...this.subscriptions.keys()];
170
+ }
171
+ /** Open the connection (idempotent). */
172
+ start() {
173
+ this.closing = false;
174
+ if (this.ws) return;
175
+ this.open();
176
+ }
177
+ /**
178
+ * Register a persistent subscription and (if connected) send the REQ.
179
+ * Returns the subscription id (caller-supplied or generated).
180
+ */
181
+ subscribe(filters, subId) {
182
+ const id = subId ?? `sub-${++this.subIdCounter}`;
183
+ const list = Array.isArray(filters) ? filters : [filters];
184
+ this.subscriptions.set(id, list);
185
+ if (this.connected) this.sendReq(id, list);
186
+ return id;
187
+ }
188
+ /** Cancel a subscription and send CLOSE if connected. */
189
+ unsubscribe(subId) {
190
+ if (!this.subscriptions.delete(subId)) return;
191
+ if (this.connected) this.sendRaw(["CLOSE", subId]);
192
+ }
193
+ /**
194
+ * Drain events newer than `cursor`. The cursor is the highest `seq` returned;
195
+ * pass it back to fetch only events received since. Filtering by `subId`
196
+ * restricts to one subscription.
197
+ */
198
+ getEvents(opts = {}) {
199
+ const after = opts.cursor ?? 0;
200
+ const limit = opts.limit ?? 200;
201
+ const matches = this.buffer.filter(
202
+ (b) => b.seq > after && (opts.subId === void 0 || b.subId === opts.subId)
203
+ );
204
+ const page = matches.slice(0, limit);
205
+ const hasMore = matches.length > page.length;
206
+ const last = page.at(-1);
207
+ const cursor = last ? last.seq : after;
208
+ return { events: page.map((b) => b.event), cursor, hasMore };
209
+ }
210
+ /** Close the connection permanently and stop reconnecting. */
211
+ close() {
212
+ this.closing = true;
213
+ if (this.reconnectTimer) {
214
+ clearTimeout(this.reconnectTimer);
215
+ this.reconnectTimer = null;
216
+ }
217
+ if (this.ws) {
218
+ try {
219
+ this.ws.close();
220
+ } catch {
221
+ }
222
+ this.ws = null;
223
+ }
224
+ this.connected = false;
225
+ }
226
+ // ── internals ────────────────────────────────────────────────────────────
227
+ open() {
228
+ let ws;
229
+ try {
230
+ ws = this.wsFactory(this.relayUrl);
231
+ } catch (err) {
232
+ this.log(`[relay] connect failed: ${errMsg(err)}`);
233
+ this.scheduleReconnect();
234
+ return;
235
+ }
236
+ this.ws = ws;
237
+ ws.on("open", () => {
238
+ this.connected = true;
239
+ this.reconnectAttempts = 0;
240
+ this.log(`[relay] connected to ${this.relayUrl}`);
241
+ for (const [id, filters] of this.subscriptions) this.sendReq(id, filters);
242
+ });
243
+ ws.on("message", (data) => this.handleMessage(data));
244
+ ws.on("error", (err) => {
245
+ this.log(`[relay] socket error: ${errMsg(err)}`);
246
+ });
247
+ ws.on("close", () => {
248
+ this.connected = false;
249
+ this.ws = null;
250
+ if (!this.closing) {
251
+ this.log("[relay] disconnected; scheduling reconnect");
252
+ this.scheduleReconnect();
253
+ }
254
+ });
255
+ }
256
+ scheduleReconnect() {
257
+ if (this.closing || this.reconnectTimer) return;
258
+ const delay2 = Math.min(
259
+ this.reconnectMaxMs,
260
+ this.reconnectBaseMs * 2 ** this.reconnectAttempts
261
+ );
262
+ this.reconnectAttempts += 1;
263
+ this.reconnectTimer = setTimeout(() => {
264
+ this.reconnectTimer = null;
265
+ if (!this.closing) this.open();
266
+ }, delay2);
267
+ this.reconnectTimer.unref?.();
268
+ }
269
+ sendReq(subId, filters) {
270
+ this.sendRaw(["REQ", subId, ...filters]);
271
+ }
272
+ sendRaw(message) {
273
+ if (!this.ws || !this.connected) return;
274
+ try {
275
+ this.ws.send(JSON.stringify(message));
276
+ } catch (err) {
277
+ this.log(`[relay] send failed: ${errMsg(err)}`);
278
+ }
279
+ }
280
+ handleMessage(data) {
281
+ let parsed;
282
+ try {
283
+ parsed = JSON.parse(toText(data));
284
+ } catch {
285
+ return;
286
+ }
287
+ if (!Array.isArray(parsed) || parsed.length === 0) return;
288
+ const type = parsed[0];
289
+ switch (type) {
290
+ case "EVENT": {
291
+ const subId = typeof parsed[1] === "string" ? parsed[1] : "";
292
+ const event = this.parseEventPayload(parsed[2]);
293
+ if (event && typeof event.id === "string")
294
+ this.bufferEvent(subId, event);
295
+ break;
296
+ }
297
+ case "EOSE":
298
+ break;
299
+ case "CLOSED":
300
+ this.log(
301
+ `[relay] subscription closed by relay: ${String(parsed[2] ?? "")}`
302
+ );
303
+ break;
304
+ case "NOTICE":
305
+ this.log(`[relay] NOTICE: ${String(parsed[1] ?? "")}`);
306
+ break;
307
+ default:
308
+ break;
309
+ }
310
+ }
311
+ /**
312
+ * Normalise an `EVENT` payload to a NostrEvent. Standard relays send a JSON
313
+ * object; the TOON relay sends a TOON-encoded string, decoded via the injected
314
+ * `decodeEvent`. Returns undefined when it can't be parsed.
315
+ */
316
+ parseEventPayload(raw) {
317
+ if (raw && typeof raw === "object" && typeof raw.id === "string") {
318
+ return raw;
319
+ }
320
+ if (typeof raw === "string" && this.decodeEvent) {
321
+ try {
322
+ return this.decodeEvent(raw);
323
+ } catch (err) {
324
+ this.log(`[relay] event decode failed: ${errMsg(err)}`);
325
+ return void 0;
326
+ }
327
+ }
328
+ return void 0;
329
+ }
330
+ bufferEvent(subId, event) {
331
+ if (this.seen.has(event.id)) return;
332
+ this.seen.add(event.id);
333
+ this.buffer.push({ seq: ++this.seqCounter, subId, event });
334
+ if (this.buffer.length > this.bufferSize) {
335
+ const evicted = this.buffer.shift();
336
+ if (evicted) this.seen.delete(evicted.event.id);
337
+ }
338
+ this.onEvent?.(subId, event);
339
+ }
340
+ };
341
+ function toText(data) {
342
+ if (typeof data === "string") return data;
343
+ if (data instanceof Uint8Array) return Buffer.from(data).toString("utf8");
344
+ if (Buffer.isBuffer(data)) return data.toString("utf8");
345
+ return String(data);
346
+ }
347
+ function errMsg(err) {
348
+ return err instanceof Error ? err.message : String(err);
349
+ }
350
+ function defaultWebSocketFactory(socksProxy) {
351
+ return (url) => {
352
+ const WebSocketImpl = nodeRequire("ws");
353
+ let agent;
354
+ if (socksProxy) {
355
+ const { SocksProxyAgent } = nodeRequire("socks-proxy-agent");
356
+ agent = new SocksProxyAgent(socksProxy);
357
+ }
358
+ return new WebSocketImpl(url, agent ? { agent } : void 0);
359
+ };
360
+ }
361
+
362
+ // src/daemon/client-runner.ts
363
+ import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
364
+
365
+ // ../../node_modules/.pnpm/@toon-protocol+sdk@0.5.1_@toon-protocol+connector@3.13.0_mina-signer@3.1.0_typescript@5.9.3/node_modules/@toon-protocol/sdk/dist/chunk-UP2VWCW5.js
366
+ var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __require : typeof Proxy !== "undefined" ? new Proxy(x, {
367
+ get: (a, b) => (typeof __require !== "undefined" ? __require : a)[b]
368
+ }) : x)(function(x) {
369
+ if (typeof __require !== "undefined") return __require.apply(this, arguments);
370
+ throw Error('Dynamic require of "' + x + '" is not supported');
371
+ });
372
+
373
+ // ../../node_modules/.pnpm/@toon-protocol+sdk@0.5.1_@toon-protocol+connector@3.13.0_mina-signer@3.1.0_typescript@5.9.3/node_modules/@toon-protocol/sdk/dist/chunk-XTHUXP63.js
374
+ import { getPublicKey } from "nostr-tools/pure";
375
+ import { generateSecretKey, getPublicKey as getPublicKey2 } from "nostr-tools/pure";
376
+ import { createRumor, createSeal, createWrap } from "nostr-tools/nip59";
377
+ import {
378
+ encrypt as nip44Encrypt,
379
+ decrypt as nip44Decrypt,
380
+ getConversationKey
381
+ } from "nostr-tools/nip44";
382
+ import { getPublicKey as getPublicKey3 } from "nostr-tools/pure";
383
+ var IdentityError = class extends ToonError {
384
+ constructor(message, cause) {
385
+ super(message, "IDENTITY_ERROR", cause);
386
+ this.name = "IdentityError";
387
+ }
388
+ };
389
+ var GiftWrapError = class extends ToonError {
390
+ constructor(message, cause) {
391
+ super(message, "GIFT_WRAP_ERROR", cause);
392
+ this.name = "GiftWrapError";
393
+ }
394
+ };
395
+ var SwapHandlerError = class extends ToonError {
396
+ constructor(message, cause) {
397
+ super(message, "SWAP_HANDLER_ERROR", cause);
398
+ this.name = "SwapHandlerError";
399
+ }
400
+ };
401
+ var StreamSwapError = class extends Error {
402
+ code;
403
+ constructor(code, message, options) {
404
+ super(message);
405
+ this.name = "StreamSwapError";
406
+ this.code = code;
407
+ if (options && "cause" in options) {
408
+ Object.defineProperty(this, "cause", {
409
+ value: options.cause,
410
+ enumerable: false,
411
+ writable: true,
412
+ configurable: true
413
+ });
414
+ }
415
+ }
416
+ };
417
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
418
+ function base58Decode(str) {
419
+ let zeros = 0;
420
+ for (let i = 0; i < str.length && str[i] === "1"; i++) zeros++;
421
+ let value = 0n;
422
+ for (const ch of str) {
423
+ const idx = BASE58_ALPHABET.indexOf(ch);
424
+ if (idx === -1) throw new IdentityError(`Invalid base58 character: ${ch}`);
425
+ value = value * 58n + BigInt(idx);
426
+ }
427
+ const hex = value === 0n ? "" : value.toString(16);
428
+ const hexPadded = hex.length % 2 ? "0" + hex : hex;
429
+ const rawBytes = [];
430
+ for (let i = 0; i < hexPadded.length; i += 2) {
431
+ rawBytes.push(parseInt(hexPadded.slice(i, i + 2), 16));
432
+ }
433
+ const result = new Uint8Array(zeros + rawBytes.length);
434
+ result.set(rawBytes, zeros);
435
+ return result;
436
+ }
437
+ function validateSecretKey(key2, paramName) {
438
+ if (!(key2 instanceof Uint8Array) || key2.length !== 32) {
439
+ throw new GiftWrapError(
440
+ `${paramName} must be a 32-byte Uint8Array, got ${key2 instanceof Uint8Array ? `${key2.length} bytes` : typeof key2}`
441
+ );
442
+ }
443
+ }
444
+ function validatePubkey(pubkey, paramName) {
445
+ if (typeof pubkey !== "string" || !/^[0-9a-f]{64}$/.test(pubkey)) {
446
+ throw new GiftWrapError(
447
+ `${paramName} must be a 64-character lowercase hex string`
448
+ );
449
+ }
450
+ }
451
+ function wrapSwapPacket(params) {
452
+ const { rumor, senderSecretKey, recipientPubkey } = params;
453
+ validateSecretKey(senderSecretKey, "senderSecretKey");
454
+ validatePubkey(recipientPubkey, "recipientPubkey");
455
+ try {
456
+ const rumorEvent = createRumor(rumor, senderSecretKey);
457
+ const seal = createSeal(rumorEvent, senderSecretKey, recipientPubkey);
458
+ const giftWrap = createWrap(seal, recipientPubkey);
459
+ const ephemeralPubkey = giftWrap.pubkey;
460
+ return { giftWrap, ephemeralPubkey };
461
+ } catch (error) {
462
+ throw new GiftWrapError(
463
+ `Failed to wrap swap packet: ${error instanceof Error ? error.message : String(error)}`,
464
+ error instanceof Error ? error : void 0
465
+ );
466
+ }
467
+ }
468
+ function wrapSwapPacketToToon(params) {
469
+ const {
470
+ rumor,
471
+ senderSecretKey,
472
+ recipientPubkey,
473
+ destination,
474
+ amount,
475
+ expiresAt
476
+ } = params;
477
+ const { giftWrap, ephemeralPubkey } = wrapSwapPacket({
478
+ rumor,
479
+ senderSecretKey,
480
+ recipientPubkey
481
+ });
482
+ const toonBinary = encodeEventToToon(giftWrap);
483
+ const ilpPrepare = buildIlpPrepare({
484
+ destination,
485
+ amount,
486
+ data: toonBinary,
487
+ expiresAt
488
+ });
489
+ return { ilpPrepare, ephemeralPubkey };
490
+ }
491
+ function decryptFulfillClaim(params) {
492
+ const { ciphertext, ephemeralPubkey, recipientSecretKey } = params;
493
+ validateSecretKey(recipientSecretKey, "recipientSecretKey");
494
+ validatePubkey(ephemeralPubkey, "ephemeralPubkey");
495
+ if (!(ciphertext instanceof Uint8Array) || ciphertext.length === 0) {
496
+ throw new GiftWrapError("ciphertext must be a non-empty Uint8Array");
497
+ }
498
+ let conversationKey = null;
499
+ try {
500
+ conversationKey = getConversationKey(recipientSecretKey, ephemeralPubkey);
501
+ const ciphertextString = new TextDecoder().decode(ciphertext);
502
+ const claimString = nip44Decrypt(ciphertextString, conversationKey);
503
+ return new Uint8Array(Buffer.from(claimString, "base64"));
504
+ } catch (error) {
505
+ if (error instanceof GiftWrapError) {
506
+ throw error;
507
+ }
508
+ throw new GiftWrapError(
509
+ `Failed to decrypt FULFILL claim: ${error instanceof Error ? error.message : String(error)}`,
510
+ error instanceof Error ? error : void 0
511
+ );
512
+ } finally {
513
+ if (conversationKey) {
514
+ conversationKey.fill(0);
515
+ conversationKey = null;
516
+ }
517
+ }
518
+ }
519
+ var RATE_REGEX = /^(0|[1-9]\d*)(\.\d+)?$/;
520
+ function applyRate(params) {
521
+ const { sourceAmount, fromScale, toScale, rate } = params;
522
+ if (!RATE_REGEX.test(rate)) {
523
+ throw new SwapHandlerError(`Invalid rate format: ${rate}`);
524
+ }
525
+ if (/^0(\.0+)?$/.test(rate)) {
526
+ throw new SwapHandlerError("Rate is zero (pair not quoting)");
527
+ }
528
+ if (sourceAmount <= 0n) {
529
+ throw new SwapHandlerError(
530
+ `sourceAmount must be positive, got ${sourceAmount}`
531
+ );
532
+ }
533
+ const dotIdx = rate.indexOf(".");
534
+ const integerPart = dotIdx === -1 ? rate : rate.slice(0, dotIdx);
535
+ const fractionalPart = dotIdx === -1 ? "" : rate.slice(dotIdx + 1);
536
+ const rateNumerator = BigInt(integerPart + fractionalPart);
537
+ const rateDenominator = 10n ** BigInt(fractionalPart.length);
538
+ const scaleUp = 10n ** BigInt(toScale);
539
+ const scaleDown = 10n ** BigInt(fromScale);
540
+ return sourceAmount * rateNumerator * scaleUp / (rateDenominator * scaleDown);
541
+ }
542
+ var RATE_REGEX2 = /^(0|[1-9]\d*)(\.\d+)?$/;
543
+ var HEX64_REGEX = /^[0-9a-f]{64}$/;
544
+ var BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;
545
+ function isBase64(s) {
546
+ if (s.length === 0 || s.length % 4 !== 0) return false;
547
+ return BASE64_REGEX.test(s);
548
+ }
549
+ function chunkAmount(total, count) {
550
+ if (!Number.isInteger(count) || count <= 0 || count > Number.MAX_SAFE_INTEGER) {
551
+ throw new StreamSwapError(
552
+ "INVALID_CHUNKING",
553
+ `packetCount must be a positive integer, got ${count}`
554
+ );
555
+ }
556
+ if (total < BigInt(count)) {
557
+ throw new StreamSwapError(
558
+ "INVALID_CHUNKING",
559
+ `totalAmount (${total}) must be >= packetCount (${count}) so per-packet amount >= 1`
560
+ );
561
+ }
562
+ const base = total / BigInt(count);
563
+ const remainder = total - base * BigInt(count);
564
+ const out = new Array(count);
565
+ for (let i = 0; i < count; i++) out[i] = base;
566
+ const last = out[count - 1] ?? 0n;
567
+ out[count - 1] = last + remainder;
568
+ return out;
569
+ }
570
+ function buildSwapRumor(input) {
571
+ const {
572
+ senderPubkey,
573
+ pair,
574
+ sourceAmount,
575
+ packetIndex,
576
+ totalPackets,
577
+ nonce,
578
+ createdAt,
579
+ chainRecipient
580
+ } = input;
581
+ return {
582
+ kind: 20032,
583
+ pubkey: senderPubkey,
584
+ content: "",
585
+ created_at: createdAt,
586
+ tags: [
587
+ ["swap-from", `${pair.from.assetCode}:${pair.from.chain}`],
588
+ ["swap-to", `${pair.to.assetCode}:${pair.to.chain}`],
589
+ ["amount", sourceAmount.toString()],
590
+ ["seq", String(packetIndex), String(totalPackets)],
591
+ ["nonce", Buffer.from(nonce).toString("hex")],
592
+ ["chain-recipient", chainRecipient]
593
+ ]
594
+ };
595
+ }
596
+ var EVM_CHANNEL_ID_REGEX = /^0x[0-9a-f]{64}$/;
597
+ var EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;
598
+ var DECIMAL_UINT_REGEX = /^(0|[1-9]\d*)$/;
599
+ var BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
600
+ function validateChainAddress(value, chain, kind) {
601
+ if (chain.startsWith("evm:")) {
602
+ const normalized = value.toLowerCase();
603
+ if (kind === "channelId") return EVM_CHANNEL_ID_REGEX.test(normalized);
604
+ return EVM_ADDRESS_REGEX.test(normalized);
605
+ }
606
+ if (chain.startsWith("solana:")) {
607
+ if (!BASE58_REGEX.test(value)) return false;
608
+ if (value.length < 32 || value.length > 44) return false;
609
+ try {
610
+ return base58Decode(value).length === 32;
611
+ } catch {
612
+ return false;
613
+ }
614
+ }
615
+ if (chain.startsWith("mina:")) {
616
+ return BASE58_REGEX.test(value) && value.length >= 32;
617
+ }
618
+ return value.length > 0;
619
+ }
620
+ function decodeFulfillMetadata(data, chain) {
621
+ if (data === void 0 || data === null || data === "") {
622
+ throw new StreamSwapError("FULFILL_DECODE_FAILED", "FULFILL data missing");
623
+ }
624
+ if (!isBase64(data)) {
625
+ throw new StreamSwapError(
626
+ "FULFILL_DECODE_FAILED",
627
+ "FULFILL data is not valid base64"
628
+ );
629
+ }
630
+ let jsonBytes;
631
+ try {
632
+ jsonBytes = Buffer.from(data, "base64");
633
+ } catch (err) {
634
+ throw new StreamSwapError(
635
+ "FULFILL_DECODE_FAILED",
636
+ `FULFILL data base64 decode failed: ${err instanceof Error ? err.message : String(err)}`,
637
+ { cause: err }
638
+ );
639
+ }
640
+ let parsed;
641
+ try {
642
+ parsed = JSON.parse(jsonBytes.toString("utf8"));
643
+ } catch (err) {
644
+ throw new StreamSwapError(
645
+ "FULFILL_DECODE_FAILED",
646
+ `FULFILL data JSON parse failed: ${err instanceof Error ? err.message : String(err)}`,
647
+ { cause: err }
648
+ );
649
+ }
650
+ if (!parsed || typeof parsed !== "object") {
651
+ throw new StreamSwapError(
652
+ "FULFILL_DECODE_FAILED",
653
+ "FULFILL metadata is not an object"
654
+ );
655
+ }
656
+ const obj = parsed;
657
+ const claim = obj["claim"];
658
+ const ephemeralPubkey = obj["ephemeralPubkey"];
659
+ if (typeof claim !== "string" || !isBase64(claim)) {
660
+ throw new StreamSwapError(
661
+ "FULFILL_DECODE_FAILED",
662
+ "FULFILL metadata.claim is missing or not base64 string"
663
+ );
664
+ }
665
+ if (typeof ephemeralPubkey !== "string" || !HEX64_REGEX.test(ephemeralPubkey)) {
666
+ throw new StreamSwapError(
667
+ "FULFILL_DECODE_FAILED",
668
+ "FULFILL metadata.ephemeralPubkey is missing or not 64-char hex"
669
+ );
670
+ }
671
+ const result = {
672
+ claim,
673
+ ephemeralPubkey
674
+ };
675
+ if (typeof obj["claimId"] === "string") {
676
+ result.claimId = obj["claimId"];
677
+ }
678
+ if (obj["targetAmount"] !== void 0) {
679
+ const ta = obj["targetAmount"];
680
+ if (typeof ta !== "string" || !/^(0|[1-9]\d*)$/.test(ta)) {
681
+ throw new StreamSwapError(
682
+ "FULFILL_DECODE_FAILED",
683
+ "FULFILL metadata.targetAmount must be a non-negative integer decimal string"
684
+ );
685
+ }
686
+ result.targetAmount = ta;
687
+ }
688
+ const channelId = obj["channelId"];
689
+ if (typeof channelId === "string" && (!chain || validateChainAddress(channelId, chain, "channelId"))) {
690
+ result.channelId = channelId;
691
+ }
692
+ const nonce = obj["nonce"];
693
+ if (typeof nonce === "string" && DECIMAL_UINT_REGEX.test(nonce)) {
694
+ result.nonce = nonce;
695
+ }
696
+ const cumulativeAmount = obj["cumulativeAmount"];
697
+ if (typeof cumulativeAmount === "string" && DECIMAL_UINT_REGEX.test(cumulativeAmount)) {
698
+ result.cumulativeAmount = cumulativeAmount;
699
+ }
700
+ const recipient = obj["recipient"];
701
+ if (typeof recipient === "string" && recipient.length > 0) {
702
+ result.recipient = recipient;
703
+ }
704
+ const millSignerAddress = obj["millSignerAddress"];
705
+ if (typeof millSignerAddress === "string" && (!chain || validateChainAddress(millSignerAddress, chain, "address"))) {
706
+ result.millSignerAddress = millSignerAddress;
707
+ }
708
+ return result;
709
+ }
710
+ var Deferred = class {
711
+ promise;
712
+ resolve;
713
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirror Promise constructor
714
+ reject;
715
+ constructor() {
716
+ this.promise = new Promise((res, rej) => {
717
+ this.resolve = res;
718
+ this.reject = rej;
719
+ });
720
+ }
721
+ };
722
+ var noop2 = () => void 0;
723
+ var NOOP_LOGGER2 = {
724
+ debug: noop2,
725
+ info: noop2,
726
+ warn: noop2,
727
+ error: noop2
728
+ };
729
+ function validateParams(params) {
730
+ if (typeof params.totalAmount !== "bigint" || params.totalAmount <= 0n) {
731
+ throw new StreamSwapError(
732
+ "INVALID_AMOUNT",
733
+ `totalAmount must be a positive bigint, got ${String(params.totalAmount)}`
734
+ );
735
+ }
736
+ const hasCount = params.packetCount !== void 0;
737
+ const hasAmounts = params.packetAmounts !== void 0;
738
+ if (hasCount === hasAmounts) {
739
+ throw new StreamSwapError(
740
+ "INVALID_CHUNKING",
741
+ "Exactly one of packetCount or packetAmounts must be provided"
742
+ );
743
+ }
744
+ if (hasCount) {
745
+ const c = params.packetCount;
746
+ if (!Number.isInteger(c) || c <= 0) {
747
+ throw new StreamSwapError(
748
+ "INVALID_CHUNKING",
749
+ `packetCount must be a positive integer, got ${c}`
750
+ );
751
+ }
752
+ if (BigInt(c) > params.totalAmount) {
753
+ throw new StreamSwapError(
754
+ "INVALID_CHUNKING",
755
+ `packetCount (${c}) exceeds totalAmount (${params.totalAmount}); per-packet amount would be < 1 micro-unit`
756
+ );
757
+ }
758
+ } else {
759
+ const arr = params.packetAmounts;
760
+ if (!Array.isArray(arr) || arr.length === 0) {
761
+ throw new StreamSwapError(
762
+ "INVALID_CHUNKING",
763
+ "packetAmounts must be a non-empty array"
764
+ );
765
+ }
766
+ let sum = 0n;
767
+ for (const a of arr) {
768
+ if (typeof a !== "bigint" || a <= 0n) {
769
+ throw new StreamSwapError(
770
+ "INVALID_CHUNKING",
771
+ `packetAmounts entries must be positive bigint, got ${String(a)}`
772
+ );
773
+ }
774
+ sum += a;
775
+ }
776
+ if (sum !== params.totalAmount) {
777
+ throw new StreamSwapError(
778
+ "INVALID_CHUNKING",
779
+ `sum(packetAmounts) (${sum}) !== totalAmount (${params.totalAmount})`
780
+ );
781
+ }
782
+ }
783
+ if (!(params.senderSecretKey instanceof Uint8Array) || params.senderSecretKey.length !== 32) {
784
+ throw new StreamSwapError(
785
+ "INVALID_STATE",
786
+ "senderSecretKey must be a 32-byte Uint8Array"
787
+ );
788
+ }
789
+ if (typeof params.millPubkey !== "string" || !HEX64_REGEX.test(params.millPubkey)) {
790
+ throw new StreamSwapError(
791
+ "INVALID_STATE",
792
+ "millPubkey must be a 64-char lowercase hex string"
793
+ );
794
+ }
795
+ if (!params.pair || typeof params.pair !== "object") {
796
+ throw new StreamSwapError("INVALID_PAIR", "pair is required");
797
+ }
798
+ if (!params.pair.from || typeof params.pair.from !== "object" || typeof params.pair.from.assetCode !== "string" || typeof params.pair.from.assetScale !== "number" || typeof params.pair.from.chain !== "string") {
799
+ throw new StreamSwapError(
800
+ "INVALID_PAIR",
801
+ "pair.from must have { assetCode: string, assetScale: number, chain: string }"
802
+ );
803
+ }
804
+ if (!params.pair.to || typeof params.pair.to !== "object" || typeof params.pair.to.assetCode !== "string" || typeof params.pair.to.assetScale !== "number" || typeof params.pair.to.chain !== "string") {
805
+ throw new StreamSwapError(
806
+ "INVALID_PAIR",
807
+ "pair.to must have { assetCode: string, assetScale: number, chain: string }"
808
+ );
809
+ }
810
+ if (typeof params.pair.rate !== "string" || !RATE_REGEX2.test(params.pair.rate)) {
811
+ throw new StreamSwapError(
812
+ "INVALID_PAIR",
813
+ `pair.rate must match ${RATE_REGEX2}, got ${params.pair.rate}`
814
+ );
815
+ }
816
+ try {
817
+ applyRate({
818
+ sourceAmount: 1n,
819
+ fromScale: params.pair.from.assetScale,
820
+ toScale: params.pair.to.assetScale,
821
+ rate: params.pair.rate
822
+ });
823
+ } catch (err) {
824
+ throw new StreamSwapError(
825
+ "INVALID_PAIR",
826
+ `pair failed applyRate sanity check: ${err instanceof Error ? err.message : String(err)}`,
827
+ { cause: err }
828
+ );
829
+ }
830
+ if (params.rateDeviationThreshold !== void 0 && (typeof params.rateDeviationThreshold !== "number" || !Number.isFinite(params.rateDeviationThreshold) || params.rateDeviationThreshold < 0)) {
831
+ throw new StreamSwapError(
832
+ "INVALID_STATE",
833
+ `rateDeviationThreshold must be a non-negative finite number, got ${params.rateDeviationThreshold}`
834
+ );
835
+ }
836
+ if (typeof params.chainRecipient !== "string" || params.chainRecipient.length === 0) {
837
+ throw new StreamSwapError(
838
+ "INVALID_STATE",
839
+ "chainRecipient must be a non-empty string (sender payout address for pair.to.chain)"
840
+ );
841
+ }
842
+ if (!validateChainAddress(
843
+ params.chainRecipient,
844
+ params.pair.to.chain,
845
+ "address"
846
+ )) {
847
+ throw new StreamSwapError(
848
+ "INVALID_CHAIN_RECIPIENT",
849
+ `chainRecipient ${params.chainRecipient} is malformed for chain ${params.pair.to.chain}`
850
+ );
851
+ }
852
+ }
853
+ async function streamSwap(params) {
854
+ return streamSwapControlled(params).result;
855
+ }
856
+ function streamSwapControlled(params) {
857
+ validateParams(params);
858
+ const logger = params.logger ?? NOOP_LOGGER2;
859
+ const schedule = params.packetAmounts ? [...params.packetAmounts] : chunkAmount(params.totalAmount, params.packetCount);
860
+ const frozenPair = Object.freeze({
861
+ from: Object.freeze({ ...params.pair.from }),
862
+ to: Object.freeze({ ...params.pair.to }),
863
+ rate: params.pair.rate
864
+ });
865
+ const senderPubkey = getPublicKey3(params.senderSecretKey);
866
+ let streamState = "running";
867
+ let resumeDeferred = null;
868
+ const controller = {
869
+ pause() {
870
+ if (streamState === "running") {
871
+ streamState = "paused";
872
+ }
873
+ },
874
+ resume() {
875
+ if (streamState === "paused") {
876
+ streamState = "running";
877
+ if (resumeDeferred) {
878
+ resumeDeferred.resolve("resume");
879
+ resumeDeferred = null;
880
+ }
881
+ } else if (streamState === "running") {
882
+ } else {
883
+ throw new StreamSwapError(
884
+ "INVALID_STATE",
885
+ `Cannot resume from state "${streamState}"`
886
+ );
887
+ }
888
+ },
889
+ stop() {
890
+ if (streamState === "completed" || streamState === "failed") return;
891
+ const prev = streamState;
892
+ streamState = "stopped";
893
+ if (prev === "paused" && resumeDeferred) {
894
+ resumeDeferred.resolve("stop");
895
+ resumeDeferred = null;
896
+ }
897
+ },
898
+ get state() {
899
+ return streamState;
900
+ }
901
+ };
902
+ const result = runLoop(
903
+ params,
904
+ frozenPair,
905
+ schedule,
906
+ senderPubkey,
907
+ logger,
908
+ () => streamState,
909
+ (v) => {
910
+ streamState = v;
911
+ },
912
+ () => {
913
+ if (streamState !== "paused") return Promise.resolve("resume");
914
+ if (!resumeDeferred) resumeDeferred = new Deferred();
915
+ return resumeDeferred.promise;
916
+ }
917
+ );
918
+ return { result, controller };
919
+ }
920
+ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, setState, waitForResumeOrStop) {
921
+ const claims = [];
922
+ const rejections = [];
923
+ const errors = [];
924
+ let cumulativeSource = 0n;
925
+ let cumulativeTarget = 0n;
926
+ let packetsSent = 0;
927
+ let abortReason = "complete";
928
+ const totalPackets = schedule.length;
929
+ const isAborted = () => params.signal?.aborted === true;
930
+ packetLoop: for (let packetIndex = 0; packetIndex < totalPackets; packetIndex++) {
931
+ if (isAborted()) {
932
+ abortReason = "aborted";
933
+ break;
934
+ }
935
+ if (getState() === "stopped") {
936
+ abortReason = "stopped";
937
+ break;
938
+ }
939
+ if (getState() === "paused") {
940
+ const resumedBy = await waitForResumeOrStop();
941
+ if (resumedBy === "stop" || getState() === "stopped") {
942
+ abortReason = "stopped";
943
+ break;
944
+ }
945
+ if (isAborted()) {
946
+ abortReason = "aborted";
947
+ break;
948
+ }
949
+ }
950
+ const sourceAmount = schedule[packetIndex];
951
+ if (sourceAmount === void 0) {
952
+ throw new StreamSwapError(
953
+ "INVALID_STATE",
954
+ `schedule[${packetIndex}] is undefined; schedule was mutated mid-stream`
955
+ );
956
+ }
957
+ const nonce = new Uint8Array(16);
958
+ getRandomValues(nonce);
959
+ const rumor = buildSwapRumor({
960
+ senderPubkey,
961
+ pair,
962
+ sourceAmount,
963
+ packetIndex: packetIndex + 1,
964
+ totalPackets,
965
+ nonce,
966
+ createdAt: Math.floor(Date.now() / 1e3),
967
+ chainRecipient: params.chainRecipient
968
+ });
969
+ let toonData;
970
+ try {
971
+ const wrapped = wrapSwapPacketToToon({
972
+ rumor,
973
+ senderSecretKey: params.senderSecretKey,
974
+ recipientPubkey: params.millPubkey,
975
+ destination: params.millIlpAddress,
976
+ amount: sourceAmount
977
+ });
978
+ toonData = new Uint8Array(Buffer.from(wrapped.ilpPrepare.data, "base64"));
979
+ } catch (err) {
980
+ logger.error({
981
+ event: "stream_swap.wrap_failed",
982
+ packetIndex,
983
+ error: err instanceof Error ? err.message : String(err)
984
+ });
985
+ errors.push({
986
+ packetIndex,
987
+ cause: err instanceof Error ? err : new Error(String(err))
988
+ });
989
+ continue;
990
+ }
991
+ let sendResult;
992
+ try {
993
+ sendResult = await params.client.sendSwapPacket({
994
+ destination: params.millIlpAddress,
995
+ amount: sourceAmount,
996
+ toonData,
997
+ timeout: params.packetTimeoutMs ?? 3e4,
998
+ claim: params.claim
999
+ });
1000
+ packetsSent += 1;
1001
+ } catch (err) {
1002
+ logger.error({
1003
+ event: "stream_swap.send_failed",
1004
+ packetIndex,
1005
+ error: err instanceof Error ? err.message : String(err)
1006
+ });
1007
+ errors.push({
1008
+ packetIndex,
1009
+ cause: err instanceof Error ? err : new Error(String(err))
1010
+ });
1011
+ continue;
1012
+ }
1013
+ if (!sendResult.accepted) {
1014
+ const code = sendResult.code ?? "F00";
1015
+ const message = sendResult.message ?? "rejected";
1016
+ logger.warn({
1017
+ event: "stream_swap.packet_rejected",
1018
+ packetIndex,
1019
+ code,
1020
+ message
1021
+ });
1022
+ rejections.push({ packetIndex, sourceAmount, code, message });
1023
+ continue;
1024
+ }
1025
+ let metadata;
1026
+ try {
1027
+ metadata = decodeFulfillMetadata(sendResult.data, pair.to.chain);
1028
+ } catch (err) {
1029
+ logger.error({
1030
+ event: "stream_swap.fulfill_decode_failed",
1031
+ packetIndex,
1032
+ error: err instanceof Error ? err.message : String(err)
1033
+ });
1034
+ errors.push({
1035
+ packetIndex,
1036
+ cause: err instanceof Error ? err : new Error(String(err))
1037
+ });
1038
+ continue;
1039
+ }
1040
+ const isEvmTarget = pair.to.chain.startsWith("evm:");
1041
+ const recipientMatches = metadata.recipient === void 0 || (isEvmTarget ? metadata.recipient.toLowerCase() === params.chainRecipient.toLowerCase() : metadata.recipient === params.chainRecipient);
1042
+ if (!recipientMatches) {
1043
+ logger.warn({
1044
+ event: "stream_swap.recipient_mismatch",
1045
+ packetIndex,
1046
+ expected: params.chainRecipient,
1047
+ actual: metadata.recipient
1048
+ });
1049
+ rejections.push({
1050
+ packetIndex,
1051
+ sourceAmount,
1052
+ code: "MILL_RECIPIENT_MISMATCH",
1053
+ message: `Mill echoed recipient ${metadata.recipient} but sender expected ${params.chainRecipient}`
1054
+ });
1055
+ continue;
1056
+ }
1057
+ let claimBytes;
1058
+ try {
1059
+ const ciphertext = new Uint8Array(Buffer.from(metadata.claim, "base64"));
1060
+ claimBytes = decryptFulfillClaim({
1061
+ ciphertext,
1062
+ ephemeralPubkey: metadata.ephemeralPubkey,
1063
+ recipientSecretKey: params.senderSecretKey
1064
+ });
1065
+ } catch (err) {
1066
+ logger.error({
1067
+ event: "stream_swap.decrypt_failed",
1068
+ packetIndex,
1069
+ error: err instanceof Error ? err.message : String(err)
1070
+ });
1071
+ errors.push({
1072
+ packetIndex,
1073
+ cause: err instanceof Error ? err : new Error(String(err))
1074
+ });
1075
+ continue;
1076
+ }
1077
+ if (claimBytes.length === 0) {
1078
+ logger.warn({
1079
+ event: "stream_swap.empty_claim_bytes",
1080
+ packetIndex
1081
+ });
1082
+ }
1083
+ const expectedTargetAmount = applyRate({
1084
+ sourceAmount,
1085
+ fromScale: pair.from.assetScale,
1086
+ toScale: pair.to.assetScale,
1087
+ rate: pair.rate
1088
+ });
1089
+ const targetAmount = metadata.targetAmount !== void 0 ? BigInt(metadata.targetAmount) : expectedTargetAmount;
1090
+ let rateDeviation = 0;
1091
+ if (expectedTargetAmount > 0n) {
1092
+ const diff = targetAmount >= expectedTargetAmount ? targetAmount - expectedTargetAmount : expectedTargetAmount - targetAmount;
1093
+ const scaled = diff * 1000000n / expectedTargetAmount;
1094
+ rateDeviation = Number(scaled) / 1e6;
1095
+ }
1096
+ const advertisedRate = parseFloat(pair.rate);
1097
+ let effectiveRate;
1098
+ if (targetAmount === expectedTargetAmount) {
1099
+ effectiveRate = advertisedRate;
1100
+ } else {
1101
+ const signedDeviation = targetAmount >= expectedTargetAmount ? rateDeviation : -rateDeviation;
1102
+ effectiveRate = advertisedRate * (1 + signedDeviation);
1103
+ }
1104
+ if (!Number.isFinite(effectiveRate)) {
1105
+ effectiveRate = advertisedRate;
1106
+ }
1107
+ cumulativeSource += sourceAmount;
1108
+ cumulativeTarget += targetAmount;
1109
+ const accumulated = {
1110
+ packetIndex,
1111
+ sourceAmount,
1112
+ targetAmount,
1113
+ claimBytes,
1114
+ millEphemeralPubkey: metadata.ephemeralPubkey,
1115
+ pair,
1116
+ receivedAt: Date.now()
1117
+ };
1118
+ if (metadata.claimId !== void 0) accumulated.claimId = metadata.claimId;
1119
+ if (metadata.channelId !== void 0)
1120
+ accumulated.channelId = metadata.channelId;
1121
+ if (metadata.nonce !== void 0) accumulated.nonce = metadata.nonce;
1122
+ if (metadata.cumulativeAmount !== void 0)
1123
+ accumulated.cumulativeAmount = metadata.cumulativeAmount;
1124
+ if (metadata.recipient !== void 0)
1125
+ accumulated.recipient = metadata.recipient;
1126
+ if (metadata.millSignerAddress !== void 0)
1127
+ accumulated.millSignerAddress = metadata.millSignerAddress;
1128
+ claims.push(accumulated);
1129
+ logger.debug({
1130
+ event: "stream_swap.packet_accepted",
1131
+ packetIndex,
1132
+ sourceAmount: sourceAmount.toString(),
1133
+ targetAmount: targetAmount.toString()
1134
+ });
1135
+ if (params.onPacket) {
1136
+ const progress = Object.freeze({
1137
+ index: packetIndex,
1138
+ total: totalPackets,
1139
+ sourceAmount,
1140
+ targetAmount,
1141
+ advertisedRate: pair.rate,
1142
+ effectiveRate,
1143
+ rateDeviation,
1144
+ cumulativeSource,
1145
+ cumulativeTarget,
1146
+ state: getState() === "paused" ? "paused" : getState() === "stopped" ? "stopped" : "running"
1147
+ });
1148
+ try {
1149
+ const maybePromise = params.onPacket(progress);
1150
+ if (maybePromise && typeof maybePromise.then === "function") {
1151
+ await maybePromise;
1152
+ }
1153
+ } catch (err) {
1154
+ logger.warn({
1155
+ event: "stream_swap.callback_threw",
1156
+ packetIndex,
1157
+ error: err instanceof Error ? err.message : String(err)
1158
+ });
1159
+ errors.push({
1160
+ packetIndex,
1161
+ cause: err instanceof Error ? err : new Error(String(err))
1162
+ });
1163
+ abortReason = "callback-throw";
1164
+ break packetLoop;
1165
+ }
1166
+ }
1167
+ if (isAborted()) {
1168
+ abortReason = "aborted";
1169
+ break;
1170
+ }
1171
+ if (getState() === "stopped") {
1172
+ abortReason = "stopped";
1173
+ break;
1174
+ }
1175
+ if (params.rateDeviationThreshold !== void 0 && rateDeviation > params.rateDeviationThreshold) {
1176
+ abortReason = "rate-deviation";
1177
+ break;
1178
+ }
1179
+ }
1180
+ let finalState;
1181
+ if (abortReason === "aborted" || abortReason === "stopped") {
1182
+ finalState = "stopped";
1183
+ } else if (claims.length === 0 && (rejections.length > 0 || errors.length > 0)) {
1184
+ finalState = "failed";
1185
+ if (abortReason === "complete" && rejections.length > 0 && errors.length === 0) {
1186
+ abortReason = "all-rejected";
1187
+ }
1188
+ } else {
1189
+ finalState = "completed";
1190
+ }
1191
+ setState(finalState);
1192
+ return {
1193
+ state: finalState,
1194
+ claims,
1195
+ rejections,
1196
+ errors,
1197
+ abortReason,
1198
+ cumulativeSource,
1199
+ cumulativeTarget,
1200
+ packetsSent,
1201
+ packetsScheduled: totalPackets
1202
+ };
1203
+ }
1204
+ function getRandomValues(buf) {
1205
+ const g = globalThis;
1206
+ if (g.crypto && typeof g.crypto.getRandomValues === "function") {
1207
+ g.crypto.getRandomValues(buf);
1208
+ return buf;
1209
+ }
1210
+ const nodeCrypto = __require2("crypto");
1211
+ if (nodeCrypto.webcrypto?.getRandomValues) {
1212
+ nodeCrypto.webcrypto.getRandomValues(buf);
1213
+ return buf;
1214
+ }
1215
+ if (nodeCrypto.randomFillSync) {
1216
+ nodeCrypto.randomFillSync(buf);
1217
+ return buf;
1218
+ }
1219
+ throw new StreamSwapError(
1220
+ "INVALID_STATE",
1221
+ "No crypto.getRandomValues available in this environment"
1222
+ );
1223
+ }
1224
+
1225
+ // src/daemon/apex-channel-store.ts
1226
+ import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
1227
+ import { dirname as dirname2 } from "path";
1228
+ function key(destination, chain) {
1229
+ return `${destination}|${chain}`;
1230
+ }
1231
+ function readStore(path) {
1232
+ try {
1233
+ return JSON.parse(readFileSync(path, "utf8"));
1234
+ } catch {
1235
+ return {};
1236
+ }
1237
+ }
1238
+ function loadApexChannel(path, destination, chain) {
1239
+ return readStore(path)[key(destination, chain)] ?? null;
1240
+ }
1241
+ function saveApexChannel(path, destination, chain, record) {
1242
+ const store = readStore(path);
1243
+ store[key(destination, chain)] = record;
1244
+ mkdirSync2(dirname2(path), { recursive: true });
1245
+ writeFileSync2(path, JSON.stringify(store, null, 2), { mode: 384 });
1246
+ }
1247
+
1248
+ // src/daemon/targets-store.ts
1249
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
1250
+ import { dirname as dirname3, join as join2 } from "path";
1251
+ function defaultTargetsPath() {
1252
+ return join2(configDir(), "targets.json");
1253
+ }
1254
+ function loadTargets(path = defaultTargetsPath()) {
1255
+ let parsed;
1256
+ try {
1257
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
1258
+ } catch {
1259
+ return { relays: [], apexes: [] };
1260
+ }
1261
+ return {
1262
+ relays: Array.isArray(parsed.relays) ? parsed.relays : [],
1263
+ apexes: Array.isArray(parsed.apexes) ? parsed.apexes : []
1264
+ };
1265
+ }
1266
+ function write(path, data) {
1267
+ mkdirSync3(dirname3(path), { recursive: true });
1268
+ writeFileSync3(path, JSON.stringify(data, null, 2), { mode: 384 });
1269
+ }
1270
+ function saveRelayTarget(relayUrl, path = defaultTargetsPath()) {
1271
+ const store = loadTargets(path);
1272
+ if (!store.relays.some((r) => r.relayUrl === relayUrl)) {
1273
+ store.relays.push({ relayUrl });
1274
+ write(path, store);
1275
+ }
1276
+ }
1277
+ function removeRelayTarget(relayUrl, path = defaultTargetsPath()) {
1278
+ const store = loadTargets(path);
1279
+ const next = store.relays.filter((r) => r.relayUrl !== relayUrl);
1280
+ if (next.length === store.relays.length) return false;
1281
+ store.relays = next;
1282
+ write(path, store);
1283
+ return true;
1284
+ }
1285
+ function saveApexTarget(target, path = defaultTargetsPath()) {
1286
+ const store = loadTargets(path);
1287
+ store.apexes = store.apexes.filter((a) => a.btpUrl !== target.btpUrl);
1288
+ store.apexes.push(target);
1289
+ write(path, store);
1290
+ }
1291
+ function removeApexTarget(btpUrl, path = defaultTargetsPath()) {
1292
+ const store = loadTargets(path);
1293
+ const next = store.apexes.filter((a) => a.btpUrl !== btpUrl);
1294
+ if (next.length === store.apexes.length) return false;
1295
+ store.apexes = next;
1296
+ write(path, store);
1297
+ return true;
1298
+ }
1299
+
1300
+ // src/daemon/apex-discovery.ts
1301
+ var ApexDiscoveryError = class extends Error {
1302
+ constructor(message, retryable = false) {
1303
+ super(message);
1304
+ this.retryable = retryable;
1305
+ this.name = "ApexDiscoveryError";
1306
+ }
1307
+ };
1308
+ async function discoverApex(params) {
1309
+ const { relay, ilpAddress, pubkey, chain, childPeers } = params;
1310
+ const timeoutMs = params.timeoutMs ?? 15e3;
1311
+ const pollMs = params.pollMs ?? 250;
1312
+ const subId = relay.subscribe(
1313
+ [
1314
+ {
1315
+ kinds: [ILP_PEER_INFO_KIND],
1316
+ ...pubkey ? { authors: [pubkey] } : {}
1317
+ }
1318
+ ],
1319
+ `apex-discovery-${ilpAddress}`
1320
+ );
1321
+ try {
1322
+ const deadline = Date.now() + timeoutMs;
1323
+ let cursor = 0;
1324
+ while (Date.now() < deadline) {
1325
+ const { events, cursor: next } = relay.getEvents({ cursor });
1326
+ cursor = next;
1327
+ const match = events.find((e) => matchesApex(e, ilpAddress, pubkey));
1328
+ if (match) return mapAnnouncement(match, { chain, childPeers });
1329
+ await delay(pollMs);
1330
+ }
1331
+ throw new ApexDiscoveryError(
1332
+ `Timed out after ${timeoutMs}ms waiting for the apex kind:${ILP_PEER_INFO_KIND} announcement for "${ilpAddress}" on the relay. Is the relay reachable and the apex online?`,
1333
+ true
1334
+ // retryable: the apex may just be slow/offline
1335
+ );
1336
+ } finally {
1337
+ relay.unsubscribe(subId);
1338
+ }
1339
+ }
1340
+ function matchesApex(event, ilpAddress, pubkey) {
1341
+ if (event.kind !== ILP_PEER_INFO_KIND) return false;
1342
+ if (pubkey && event.pubkey !== pubkey) return false;
1343
+ if (isEventExpired(event)) return false;
1344
+ try {
1345
+ const info = parseIlpPeerInfo(event);
1346
+ const addrs = info.ilpAddresses ?? [info.ilpAddress];
1347
+ return addrs.includes(ilpAddress) || info.ilpAddress === ilpAddress;
1348
+ } catch {
1349
+ return false;
1350
+ }
1351
+ }
1352
+ function mapAnnouncement(event, opts) {
1353
+ const info = parseIlpPeerInfo(event);
1354
+ const chains = info.supportedChains ?? [];
1355
+ if (chains.length === 0) {
1356
+ throw new ApexDiscoveryError(
1357
+ `Apex "${info.ilpAddress}" announced no supportedChains \u2014 cannot settle.`
1358
+ );
1359
+ }
1360
+ const chainKey = (opts.chain ? chains.find((c) => c.split(":")[0] === opts.chain) : void 0) ?? chains[0];
1361
+ if (!chainKey) {
1362
+ throw new ApexDiscoveryError(
1363
+ `Apex "${info.ilpAddress}" announced no usable settlement chain.`
1364
+ );
1365
+ }
1366
+ const family = chainKey.split(":")[0];
1367
+ const settlementAddress = info.settlementAddresses?.[chainKey];
1368
+ if (!settlementAddress) {
1369
+ throw new ApexDiscoveryError(
1370
+ `Apex "${info.ilpAddress}" announced no settlementAddress for chain "${chainKey}".`
1371
+ );
1372
+ }
1373
+ const btpUrl = info.btpEndpoint;
1374
+ if (!btpUrl) {
1375
+ throw new ApexDiscoveryError(
1376
+ `Apex "${info.ilpAddress}" announced an empty btpEndpoint \u2014 cannot open a BTP session.`
1377
+ );
1378
+ }
1379
+ const negotiation = {
1380
+ destination: info.ilpAddress,
1381
+ peerId: info.ilpAddress.split(".").at(-1) ?? info.ilpAddress,
1382
+ chain: family,
1383
+ chainKey,
1384
+ // EVM chainKeys are `evm:<network>:<chainId>`; non-EVM carry no numeric id.
1385
+ chainId: family === "evm" ? Number(chainKey.split(":")[2] ?? 0) : 0,
1386
+ settlementAddress,
1387
+ ...info.preferredTokens?.[chainKey] ? { tokenAddress: info.preferredTokens[chainKey] } : {},
1388
+ ...info.tokenNetworks?.[chainKey] ? { tokenNetwork: info.tokenNetworks[chainKey] } : {}
1389
+ };
1390
+ return {
1391
+ btpUrl,
1392
+ negotiation,
1393
+ ...opts.childPeers && opts.childPeers.length > 0 ? { apexChildPeers: opts.childPeers } : {}
1394
+ };
1395
+ }
1396
+ function delay(ms) {
1397
+ return new Promise((r) => setTimeout(r, ms));
1398
+ }
1399
+
1400
+ // src/daemon/client-runner.ts
1401
+ var MERGED_BUFFER = 5e3;
1402
+ var ClientRunner = class {
1403
+ config;
1404
+ createClient;
1405
+ createRelay;
1406
+ startReadProxy;
1407
+ log;
1408
+ targetsPath;
1409
+ startedAt = Date.now();
1410
+ /** Apex write targets, keyed by btpUrl. */
1411
+ apexes = /* @__PURE__ */ new Map();
1412
+ /** Relay read targets, keyed by relayUrl. */
1413
+ relays = /* @__PURE__ */ new Map();
1414
+ /** Runner-level merged read buffer across all relays (de-duped by event.id). */
1415
+ merged = [];
1416
+ mergedSeen = /* @__PURE__ */ new Set();
1417
+ mergedSeq = 0;
1418
+ /**
1419
+ * Fan-out subscriptions (no relayUrl restriction): replayed onto relays added
1420
+ * later so a new relay immediately participates in existing reads.
1421
+ */
1422
+ fanoutSubs = /* @__PURE__ */ new Map();
1423
+ subIdCounter = 0;
1424
+ defaultBtpUrl;
1425
+ defaultRelayUrl;
1426
+ /** Teardown for the daemon-managed read proxy (btp-direct + relay-`.anyone`). */
1427
+ stopReadProxy;
1428
+ readProxyError;
1429
+ stopped = false;
1430
+ started = false;
1431
+ constructor(deps) {
1432
+ this.config = deps.config;
1433
+ this.createClient = deps.createClient;
1434
+ this.log = deps.logger ?? (() => void 0);
1435
+ if (deps.targetsPath !== void 0) this.targetsPath = deps.targetsPath;
1436
+ this.defaultBtpUrl = deps.config.toonClientConfig.btpUrl ?? "";
1437
+ this.defaultRelayUrl = deps.config.relayUrl;
1438
+ this.createRelay = deps.createRelay ?? ((opts) => new RelaySubscription({
1439
+ relayUrl: opts.relayUrl,
1440
+ ...opts.socksProxy ? { socksProxy: opts.socksProxy } : {},
1441
+ ...opts.logger ? { logger: opts.logger } : {},
1442
+ onEvent: opts.onEvent,
1443
+ // The TOON relay sends events TOON-encoded (text) on reads, not JSON.
1444
+ decodeEvent: (raw) => decodeEventFromToon(new TextEncoder().encode(raw))
1445
+ }));
1446
+ this.startReadProxy = deps.startReadProxy ?? ((opts) => startManagedAnonProxy({
1447
+ socksPort: opts.socksPort,
1448
+ ...opts.log ? { log: opts.log } : {}
1449
+ }));
1450
+ this.registerRelay(this.defaultRelayUrl, this.config.socksProxy);
1451
+ const defaultApex = this.makeApex({
1452
+ btpUrl: this.defaultBtpUrl,
1453
+ client: this.createClient(this.config.toonClientConfig),
1454
+ ...this.config.apex ? { negotiation: this.config.apex } : {},
1455
+ childPeers: this.config.apexChildPeers ?? [],
1456
+ destination: this.config.destination,
1457
+ chain: this.config.chain,
1458
+ channelStorePath: this.config.toonClientConfig.channelStorePath ?? this.apexChannelStorePathFor(this.defaultBtpUrl),
1459
+ feePerEvent: this.config.feePerEvent,
1460
+ isDefault: true
1461
+ });
1462
+ this.apexes.set(defaultApex.btpUrl, defaultApex);
1463
+ }
1464
+ /**
1465
+ * Start the live connections: the shared read proxy, every relay socket, the
1466
+ * default apex bootstrap (non-blocking), then replay persisted dynamic
1467
+ * targets. Returns immediately; apexes become ready asynchronously.
1468
+ */
1469
+ start() {
1470
+ if (this.started) return;
1471
+ this.started = true;
1472
+ if (this.config.manageReadProxy) void this.bringUpReadProxy();
1473
+ for (const relay of this.relays.values()) relay.start();
1474
+ void this.bootstrap();
1475
+ this.replayPersistedTargets();
1476
+ }
1477
+ /** Await the default apex's bootstrap (kicking it off if not already running). */
1478
+ bootstrap() {
1479
+ const apex = this.apexes.get(this.defaultBtpUrl);
1480
+ if (!apex) return Promise.resolve();
1481
+ return this.bootstrapApex(apex);
1482
+ }
1483
+ // ── Relays (reads) ─────────────────────────────────────────────────────────
1484
+ /**
1485
+ * Build + register a relay (idempotent by URL), wiring its events into the
1486
+ * merged buffer and replaying active fan-out subscriptions. Does NOT start the
1487
+ * socket — callers start it (so construction stays side-effect-free for tests).
1488
+ */
1489
+ registerRelay(relayUrl, socksProxy) {
1490
+ const existing = this.relays.get(relayUrl);
1491
+ if (existing) return existing;
1492
+ const relay = this.createRelay({
1493
+ relayUrl,
1494
+ ...socksProxy ? { socksProxy } : {},
1495
+ logger: this.log,
1496
+ onEvent: (subId, event) => this.pushMerged(relayUrl, subId, event)
1497
+ });
1498
+ this.relays.set(relayUrl, relay);
1499
+ for (const [subId, filters] of this.fanoutSubs)
1500
+ relay.subscribe(filters, subId);
1501
+ return relay;
1502
+ }
1503
+ /**
1504
+ * Add a relay read target at runtime. `.anyone` relays reuse the managed read
1505
+ * proxy (started here if needed). Persisted unless `persist` is false.
1506
+ */
1507
+ async addRelay(relayUrl, persist = true) {
1508
+ if (this.relays.has(relayUrl)) return;
1509
+ let socksProxy = this.config.socksProxy;
1510
+ if (isAnyoneHost(relayUrl) && !socksProxy) {
1511
+ await this.ensureReadProxy();
1512
+ socksProxy = `socks5h://127.0.0.1:${this.config.readProxySocksPort ?? 9050}`;
1513
+ }
1514
+ const relay = this.registerRelay(relayUrl, socksProxy);
1515
+ relay.start();
1516
+ if (persist) saveRelayTarget(relayUrl, this.targetsPath);
1517
+ }
1518
+ /** Remove a relay read target. The config-seeded default cannot be removed. */
1519
+ removeRelay(relayUrl) {
1520
+ if (relayUrl === this.defaultRelayUrl) {
1521
+ throw new TargetError("Cannot remove the default (config-seeded) relay.");
1522
+ }
1523
+ const relay = this.relays.get(relayUrl);
1524
+ if (!relay) throw new TargetError(`No such relay: ${relayUrl}`);
1525
+ relay.close();
1526
+ this.relays.delete(relayUrl);
1527
+ this.merged = this.merged.filter((m) => {
1528
+ if (m.relayUrl === relayUrl) {
1529
+ this.mergedSeen.delete(m.event.id);
1530
+ return false;
1531
+ }
1532
+ return true;
1533
+ });
1534
+ removeRelayTarget(relayUrl, this.targetsPath);
1535
+ }
1536
+ /** Mirror a newly-buffered relay event into the merged cross-relay buffer. */
1537
+ pushMerged(relayUrl, subId, event) {
1538
+ if (this.mergedSeen.has(event.id)) return;
1539
+ this.mergedSeen.add(event.id);
1540
+ this.merged.push({ seq: ++this.mergedSeq, relayUrl, subId, event });
1541
+ if (this.merged.length > MERGED_BUFFER) {
1542
+ const evicted = this.merged.shift();
1543
+ if (evicted) this.mergedSeen.delete(evicted.event.id);
1544
+ }
1545
+ }
1546
+ /**
1547
+ * Register a free-read subscription. With no `relayUrl` it FANS OUT across
1548
+ * every relay (and onto relays added later); with one it targets that relay.
1549
+ */
1550
+ subscribe(req) {
1551
+ const subId = req.subId ?? `sub-${++this.subIdCounter}`;
1552
+ const filters = Array.isArray(req.filters) ? req.filters : [req.filters];
1553
+ const targets = req.relayUrl ? [req.relayUrl] : [...this.relays.keys()];
1554
+ if (req.relayUrl && !this.relays.has(req.relayUrl)) {
1555
+ throw new TargetError(`No such relay: ${req.relayUrl}`);
1556
+ }
1557
+ if (!req.relayUrl) this.fanoutSubs.set(subId, filters);
1558
+ for (const url of targets) this.relays.get(url)?.subscribe(filters, subId);
1559
+ return { subId, relays: targets };
1560
+ }
1561
+ /** Drain merged events newer than the cursor (free read), optionally scoped. */
1562
+ getEvents(query) {
1563
+ const after = query.cursor ?? 0;
1564
+ const limit = query.limit ?? 200;
1565
+ const matches = this.merged.filter(
1566
+ (m) => m.seq > after && (query.subId === void 0 || m.subId === query.subId) && (query.relayUrl === void 0 || m.relayUrl === query.relayUrl)
1567
+ );
1568
+ const page = matches.slice(0, limit);
1569
+ const hasMore = matches.length > page.length;
1570
+ const last = page.at(-1);
1571
+ return {
1572
+ events: page.map((m) => m.event),
1573
+ cursor: last ? last.seq : after,
1574
+ hasMore
1575
+ };
1576
+ }
1577
+ // ── Apexes (writes) ──────────────────────────────────────────────────────
1578
+ makeApex(init) {
1579
+ return {
1580
+ ...init,
1581
+ ready: false,
1582
+ bootstrapping: false
1583
+ };
1584
+ }
1585
+ /**
1586
+ * Bootstrap one apex (memoized): start, inject negotiation, open/resume the
1587
+ * channel, route child peers. Concurrent callers await the same in-flight
1588
+ * work rather than re-running it.
1589
+ */
1590
+ bootstrapApex(apex) {
1591
+ if (apex.ready) return Promise.resolve();
1592
+ if (!apex.bootstrapPromise) {
1593
+ apex.bootstrapPromise = this.doBootstrapApex(apex);
1594
+ }
1595
+ return apex.bootstrapPromise;
1596
+ }
1597
+ async doBootstrapApex(apex) {
1598
+ apex.bootstrapping = true;
1599
+ try {
1600
+ await apex.client.start();
1601
+ this.injectApexNegotiation(apex);
1602
+ apex.apexChannelId = await this.openOrResumeApexChannel(apex);
1603
+ this.routeChildPeersThroughApexChannel(apex);
1604
+ apex.ready = true;
1605
+ apex.lastError = void 0;
1606
+ this.log(
1607
+ `[runner] apex ${apex.btpUrl} ready; channel ${apex.apexChannelId}`
1608
+ );
1609
+ } catch (err) {
1610
+ apex.lastError = err instanceof Error ? err.message : String(err);
1611
+ this.log(
1612
+ `[runner] apex ${apex.btpUrl} bootstrap failed: ${apex.lastError}`
1613
+ );
1614
+ } finally {
1615
+ apex.bootstrapping = false;
1616
+ }
1617
+ }
1618
+ /**
1619
+ * Add an apex write target. Settlement params are discovered by reading the
1620
+ * apex's kind:10032 off the given relay (added first if unknown). Persisted.
1621
+ */
1622
+ async addApex(req) {
1623
+ await this.addRelay(req.relayUrl);
1624
+ const relay = this.relays.get(req.relayUrl);
1625
+ if (!relay) throw new TargetError(`Relay unavailable: ${req.relayUrl}`);
1626
+ const discovered = await discoverApex({
1627
+ relay,
1628
+ ilpAddress: req.ilpAddress,
1629
+ ...req.pubkey ? { pubkey: req.pubkey } : {},
1630
+ ...req.chain ? { chain: req.chain } : {},
1631
+ ...req.childPeers ? { childPeers: req.childPeers } : {}
1632
+ });
1633
+ const feePerEvent = req.feePerEvent !== void 0 ? BigInt(req.feePerEvent) : this.config.feePerEvent;
1634
+ await this.instantiateApex(
1635
+ {
1636
+ btpUrl: discovered.btpUrl,
1637
+ negotiation: discovered.negotiation,
1638
+ ...discovered.apexChildPeers ? { apexChildPeers: discovered.apexChildPeers } : {},
1639
+ feePerEvent: req.feePerEvent ?? feePerEvent.toString(),
1640
+ discoveredFrom: req.relayUrl
1641
+ },
1642
+ true
1643
+ );
1644
+ const apex = this.apexes.get(discovered.btpUrl);
1645
+ if (!apex) {
1646
+ throw new TargetError(
1647
+ `Apex ${discovered.btpUrl} failed to register after discovery.`
1648
+ );
1649
+ }
1650
+ return {
1651
+ btpUrl: apex.btpUrl,
1652
+ destination: apex.destination,
1653
+ chain: apex.chain,
1654
+ ready: apex.ready
1655
+ };
1656
+ }
1657
+ /** Build + register + bootstrap an apex from a (persisted) target record. */
1658
+ async instantiateApex(target, persist) {
1659
+ if (this.apexes.has(target.btpUrl)) return;
1660
+ const clientConfig = this.deriveApexClientConfig(
1661
+ target.btpUrl,
1662
+ target.negotiation.destination
1663
+ );
1664
+ const apex = this.makeApex({
1665
+ btpUrl: target.btpUrl,
1666
+ client: this.createClient(clientConfig),
1667
+ negotiation: target.negotiation,
1668
+ childPeers: target.apexChildPeers ?? [],
1669
+ destination: target.negotiation.destination,
1670
+ chain: target.negotiation.chain,
1671
+ channelStorePath: this.apexChannelStorePathFor(target.btpUrl),
1672
+ feePerEvent: BigInt(target.feePerEvent ?? this.config.feePerEvent),
1673
+ isDefault: false
1674
+ });
1675
+ this.apexes.set(apex.btpUrl, apex);
1676
+ if (persist) saveApexTarget(target, this.targetsPath);
1677
+ await this.bootstrapApex(apex);
1678
+ }
1679
+ /** Remove an apex write target. The config-seeded default cannot be removed. */
1680
+ async removeApex(btpUrl) {
1681
+ if (btpUrl === this.defaultBtpUrl) {
1682
+ throw new TargetError("Cannot remove the default (config-seeded) apex.");
1683
+ }
1684
+ const apex = this.apexes.get(btpUrl);
1685
+ if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);
1686
+ try {
1687
+ await apex.client.stop();
1688
+ } catch (err) {
1689
+ this.log(
1690
+ `[runner] apex ${btpUrl} stop error: ${err instanceof Error ? err.message : String(err)}`
1691
+ );
1692
+ }
1693
+ this.apexes.delete(btpUrl);
1694
+ removeApexTarget(btpUrl, this.targetsPath);
1695
+ }
1696
+ /** Derive a per-apex ToonClientConfig from the default (shared identity/transport). */
1697
+ deriveApexClientConfig(btpUrl, destination) {
1698
+ const base = this.config.toonClientConfig;
1699
+ return {
1700
+ ...base,
1701
+ btpUrl,
1702
+ destinationAddress: destination,
1703
+ // Distinct nonce-watermark store per apex so parallel ChannelManagers in
1704
+ // this process never race a shared channels.json.
1705
+ channelStorePath: this.apexChannelStorePathFor(btpUrl),
1706
+ ilpInfo: { ...base.ilpInfo, btpEndpoint: btpUrl }
1707
+ };
1708
+ }
1709
+ apexChannelStorePathFor(btpUrl) {
1710
+ return `${configDir()}/channels-${sanitize(btpUrl)}.json`;
1711
+ }
1712
+ // ── Persisted-target replay ────────────────────────────────────────────────
1713
+ replayPersistedTargets() {
1714
+ let store;
1715
+ try {
1716
+ store = loadTargets(this.targetsPath);
1717
+ } catch (err) {
1718
+ this.log(
1719
+ `[runner] failed to load targets store: ${err instanceof Error ? err.message : String(err)}`
1720
+ );
1721
+ return;
1722
+ }
1723
+ for (const r of store.relays) {
1724
+ if (r.relayUrl === this.defaultRelayUrl) continue;
1725
+ void this.addRelay(r.relayUrl, false).catch(
1726
+ (err) => this.log(`[runner] replay relay ${r.relayUrl} failed: ${errMsg2(err)}`)
1727
+ );
1728
+ }
1729
+ for (const a of store.apexes) {
1730
+ if (a.btpUrl === this.defaultBtpUrl) continue;
1731
+ void this.instantiateApex(a, false).catch(
1732
+ (err) => this.log(`[runner] replay apex ${a.btpUrl} failed: ${errMsg2(err)}`)
1733
+ );
1734
+ }
1735
+ }
1736
+ // ── Shared read proxy ──────────────────────────────────────────────────────
1737
+ async ensureReadProxy() {
1738
+ if (this.stopReadProxy) return;
1739
+ await this.bringUpReadProxy();
1740
+ }
1741
+ async bringUpReadProxy() {
1742
+ if (this.stopReadProxy) return;
1743
+ const socksPort = this.config.readProxySocksPort ?? 9050;
1744
+ try {
1745
+ this.log(
1746
+ `[runner] starting managed read proxy on 127.0.0.1:${socksPort}`
1747
+ );
1748
+ const proxy = await this.startReadProxy({
1749
+ socksPort,
1750
+ log: (m) => this.log(`[anon] ${m}`)
1751
+ });
1752
+ if (this.stopped) {
1753
+ await proxy.stop();
1754
+ return;
1755
+ }
1756
+ this.stopReadProxy = () => proxy.stop();
1757
+ this.readProxyError = void 0;
1758
+ this.log("[runner] managed read proxy ready");
1759
+ } catch (err) {
1760
+ this.readProxyError = err instanceof Error ? err.message : String(err);
1761
+ this.log(`[runner] managed read proxy failed: ${this.readProxyError}`);
1762
+ }
1763
+ }
1764
+ // ── Channel / negotiation helpers (per-apex) ───────────────────────────────
1765
+ /** Open the apex channel — or, on a restart, RESUME the existing one. */
1766
+ async openOrResumeApexChannel(apex) {
1767
+ const { destination, chain } = apex;
1768
+ const { apexChannelStorePath } = this.config;
1769
+ const saved = loadApexChannel(apexChannelStorePath, destination, chain);
1770
+ const cm = apex.client.channelManager;
1771
+ if (saved && cm && typeof cm.trackChannel === "function") {
1772
+ cm.trackChannel(saved.channelId, saved.context);
1773
+ this.log(
1774
+ `[runner] resumed apex channel ${saved.channelId} (tracked, no re-deposit)`
1775
+ );
1776
+ return saved.channelId;
1777
+ }
1778
+ const channelId = await apex.client.openChannel(destination);
1779
+ if (apex.negotiation) {
1780
+ const a = apex.negotiation;
1781
+ saveApexChannel(apexChannelStorePath, destination, chain, {
1782
+ channelId,
1783
+ context: {
1784
+ chainType: a.chain,
1785
+ chainId: a.chainId,
1786
+ tokenNetworkAddress: a.tokenNetwork ?? "",
1787
+ ...a.tokenAddress ? { tokenAddress: a.tokenAddress } : {},
1788
+ recipient: a.settlementAddress
1789
+ }
1790
+ });
1791
+ }
1792
+ return channelId;
1793
+ }
1794
+ /** Inject the apex settlement negotiation directly into its ToonClient. */
1795
+ injectApexNegotiation(apex) {
1796
+ const a = apex.negotiation;
1797
+ if (!a) return;
1798
+ const negotiations = apex.client.peerNegotiations;
1799
+ if (!(negotiations instanceof Map)) {
1800
+ throw new Error(
1801
+ "ToonClient.peerNegotiations layout changed \u2014 cannot inject apex negotiation"
1802
+ );
1803
+ }
1804
+ negotiations.set(a.peerId, {
1805
+ chain: a.chainKey,
1806
+ chainType: a.chain,
1807
+ chainId: a.chainId,
1808
+ settlementAddress: a.settlementAddress,
1809
+ tokenAddress: a.tokenAddress,
1810
+ tokenNetwork: a.tokenNetwork
1811
+ });
1812
+ this.log(`[runner] injected apex negotiation for peer "${a.peerId}"`);
1813
+ }
1814
+ /** Route apex CHILD peers (dvm/mill) through the SAME apex payment channel. */
1815
+ routeChildPeersThroughApexChannel(apex) {
1816
+ const a = apex.negotiation;
1817
+ if (!a || !apex.apexChannelId || apex.childPeers.length === 0) return;
1818
+ const client = apex.client;
1819
+ const negotiations = client.peerNegotiations;
1820
+ const peerChannels = client.channelManager?.peerChannels;
1821
+ if (!(negotiations instanceof Map) || !(peerChannels instanceof Map)) {
1822
+ this.log(
1823
+ "[runner] cannot route child peers \u2014 ToonClient internals layout changed"
1824
+ );
1825
+ return;
1826
+ }
1827
+ for (const peer of apex.childPeers) {
1828
+ if (peer === a.peerId) continue;
1829
+ negotiations.set(peer, {
1830
+ chain: a.chainKey,
1831
+ chainType: a.chain,
1832
+ chainId: a.chainId,
1833
+ settlementAddress: a.settlementAddress,
1834
+ tokenAddress: a.tokenAddress,
1835
+ tokenNetwork: a.tokenNetwork
1836
+ });
1837
+ peerChannels.set(peer, apex.apexChannelId);
1838
+ this.log(
1839
+ `[runner] routed child peer "${peer}" through apex channel ${apex.apexChannelId}`
1840
+ );
1841
+ }
1842
+ }
1843
+ // ── Status ─────────────────────────────────────────────────────────────────
1844
+ defaultApex() {
1845
+ return this.apexes.get(this.defaultBtpUrl);
1846
+ }
1847
+ /** Whether any apex has finished bootstrapping. */
1848
+ isReady() {
1849
+ return [...this.apexes.values()].some((a) => a.ready);
1850
+ }
1851
+ isBootstrapping() {
1852
+ return [...this.apexes.values()].some((a) => a.bootstrapping);
1853
+ }
1854
+ getStatus() {
1855
+ const apex = this.defaultApex();
1856
+ const client = apex?.client;
1857
+ const net = client?.getNetworkStatus();
1858
+ const network = net ? ["evm", "solana", "mina"].map((c) => ({
1859
+ chain: c,
1860
+ ready: net[c] === "configured",
1861
+ detail: net[c]
1862
+ })) : void 0;
1863
+ const relay = this.relays.get(this.defaultRelayUrl);
1864
+ return {
1865
+ uptimeMs: Date.now() - this.startedAt,
1866
+ bootstrapping: apex?.bootstrapping ?? false,
1867
+ ready: apex?.ready ?? false,
1868
+ settlementChain: this.config.chain,
1869
+ identity: {
1870
+ nostrPubkey: safe(() => client?.getPublicKey()) ?? "",
1871
+ evmAddress: safe(() => client?.getEvmAddress()),
1872
+ solanaAddress: safe(() => client?.getSolanaAddress()),
1873
+ minaAddress: safe(() => client?.getMinaAddress())
1874
+ },
1875
+ transport: {
1876
+ type: this.config.socksProxy ? "socks5" : "direct",
1877
+ ...this.config.socksProxy ? { socksProxy: this.config.socksProxy } : {},
1878
+ ...apex ? { btpUrl: apex.btpUrl } : {}
1879
+ },
1880
+ relay: {
1881
+ url: this.defaultRelayUrl,
1882
+ connected: relay?.isConnected() ?? false,
1883
+ buffered: relay?.bufferedCount() ?? 0,
1884
+ subscriptions: relay?.activeSubscriptions() ?? [],
1885
+ ...this.readProxyError ? { proxyError: this.readProxyError } : {}
1886
+ },
1887
+ ...network ? { network } : {},
1888
+ ...apex?.lastError ? { lastError: apex.lastError } : {}
1889
+ };
1890
+ }
1891
+ /** Full registry of relay + apex targets with per-target status. */
1892
+ getTargets() {
1893
+ const relays = [...this.relays.entries()].map(
1894
+ ([relayUrl, r]) => ({
1895
+ relayUrl,
1896
+ connected: r.isConnected(),
1897
+ buffered: r.bufferedCount(),
1898
+ subscriptions: r.activeSubscriptions(),
1899
+ isDefault: relayUrl === this.defaultRelayUrl
1900
+ })
1901
+ );
1902
+ const apexes = [...this.apexes.values()].map((a) => ({
1903
+ btpUrl: a.btpUrl,
1904
+ destination: a.destination,
1905
+ chain: a.chain,
1906
+ ready: a.ready,
1907
+ bootstrapping: a.bootstrapping,
1908
+ ...a.apexChannelId ? { channelId: a.apexChannelId } : {},
1909
+ ...a.lastError ? { lastError: a.lastError } : {},
1910
+ isDefault: a.isDefault
1911
+ }));
1912
+ return { relays, apexes };
1913
+ }
1914
+ // ── Paid operations ──────────────────────────────────────────────────────
1915
+ /** Pay-to-write a single event through the selected (or default) apex. */
1916
+ async publish(req) {
1917
+ const apex = this.selectApex(req.btpUrl);
1918
+ this.assertApexReady(apex);
1919
+ const channelId = apex.apexChannelId ?? await apex.client.openChannel(req.destination);
1920
+ const fee = req.fee !== void 0 ? BigInt(req.fee) : apex.feePerEvent;
1921
+ const claim = await apex.client.signBalanceProof(channelId, fee);
1922
+ const result = await apex.client.publishEvent(req.event, {
1923
+ ...req.destination ? { destination: req.destination } : {},
1924
+ claim,
1925
+ ilpAmount: fee
1926
+ });
1927
+ if (!result.success) {
1928
+ throw new PublishRejectedError(result.error ?? "relay rejected event");
1929
+ }
1930
+ return {
1931
+ eventId: result.eventId ?? req.event.id,
1932
+ ...result.data !== void 0 ? { data: result.data } : {},
1933
+ channelId,
1934
+ nonce: apex.client.getChannelNonce(channelId)
1935
+ };
1936
+ }
1937
+ /** Open (or return) a payment channel on the selected (or default) apex. */
1938
+ async openChannel(destination, btpUrl) {
1939
+ const apex = this.selectApex(btpUrl);
1940
+ this.assertApexReady(apex);
1941
+ const channelId = await apex.client.openChannel(
1942
+ destination ?? apex.destination
1943
+ );
1944
+ if (!destination || destination === apex.destination) {
1945
+ apex.apexChannelId = channelId;
1946
+ }
1947
+ return { channelId };
1948
+ }
1949
+ /** List tracked channels across ALL apexes with nonce + cumulative amount. */
1950
+ getChannels() {
1951
+ const seen = /* @__PURE__ */ new Set();
1952
+ const channels = [];
1953
+ for (const apex of this.apexes.values()) {
1954
+ for (const channelId of apex.client.getTrackedChannels()) {
1955
+ if (seen.has(channelId)) continue;
1956
+ seen.add(channelId);
1957
+ channels.push({
1958
+ channelId,
1959
+ nonce: apex.client.getChannelNonce(channelId),
1960
+ cumulativeAmount: apex.client.getChannelCumulativeAmount(channelId).toString()
1961
+ });
1962
+ }
1963
+ }
1964
+ return { channels };
1965
+ }
1966
+ /** Swap source→target asset against a mill peer via the selected apex. */
1967
+ async swap(req) {
1968
+ const apex = this.selectApex(req.btpUrl);
1969
+ this.assertApexReady(apex);
1970
+ const senderSecretKey = generateSecretKey2();
1971
+ const result = await streamSwap({
1972
+ client: apex.client,
1973
+ millPubkey: req.millPubkey,
1974
+ millIlpAddress: req.destination,
1975
+ pair: req.pair,
1976
+ senderSecretKey,
1977
+ chainRecipient: req.chainRecipient,
1978
+ totalAmount: BigInt(req.amount),
1979
+ packetCount: req.packetCount ?? 1
1980
+ });
1981
+ const firstReject = result.rejections[0];
1982
+ return {
1983
+ accepted: result.claims.length > 0,
1984
+ packetsAccepted: result.claims.length,
1985
+ claims: result.claims.map((c) => ({
1986
+ sourceAmount: c.sourceAmount.toString(),
1987
+ targetAmount: c.targetAmount.toString(),
1988
+ claim: Buffer.from(c.claimBytes).toString("base64"),
1989
+ ...c.channelId ? { channelId: c.channelId } : {},
1990
+ ...c.recipient ? { recipient: c.recipient } : {},
1991
+ ...c.millSignerAddress ? { millSignerAddress: c.millSignerAddress } : {},
1992
+ ...c.claimId ? { claimId: c.claimId } : {},
1993
+ ...c.nonce ? { nonce: c.nonce } : {},
1994
+ ...c.cumulativeAmount ? { cumulativeAmount: c.cumulativeAmount } : {}
1995
+ })),
1996
+ cumulativeSource: result.cumulativeSource.toString(),
1997
+ cumulativeTarget: result.cumulativeTarget.toString(),
1998
+ state: result.state,
1999
+ ...firstReject ? { code: firstReject.code, message: firstReject.message } : {}
2000
+ };
2001
+ }
2002
+ /** Graceful teardown: close every relay + stop every apex client + read proxy. */
2003
+ async stop() {
2004
+ if (this.stopped) return;
2005
+ this.stopped = true;
2006
+ for (const relay of this.relays.values()) relay.close();
2007
+ if (this.stopReadProxy) {
2008
+ try {
2009
+ await this.stopReadProxy();
2010
+ } catch (err) {
2011
+ this.log(`[runner] read proxy stop error: ${errMsg2(err)}`);
2012
+ }
2013
+ this.stopReadProxy = void 0;
2014
+ }
2015
+ for (const apex of this.apexes.values()) {
2016
+ try {
2017
+ await apex.client.stop();
2018
+ } catch (err) {
2019
+ this.log(`[runner] client stop error (${apex.btpUrl}): ${errMsg2(err)}`);
2020
+ }
2021
+ }
2022
+ }
2023
+ // ── internals ────────────────────────────────────────────────────────────
2024
+ selectApex(btpUrl) {
2025
+ if (btpUrl) {
2026
+ const apex = this.apexes.get(btpUrl);
2027
+ if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);
2028
+ return apex;
2029
+ }
2030
+ const def = this.defaultApex();
2031
+ if (!def) throw new NotReadyError("No apex configured.");
2032
+ return def;
2033
+ }
2034
+ assertApexReady(apex) {
2035
+ if (!apex.ready) {
2036
+ throw new NotReadyError(
2037
+ apex.bootstrapping ? "Apex is still bootstrapping (BTP/anon coming up) \u2014 retry shortly." : apex.lastError ?? "Apex is not ready."
2038
+ );
2039
+ }
2040
+ }
2041
+ };
2042
+ var NotReadyError = class extends Error {
2043
+ retryable = true;
2044
+ constructor(message) {
2045
+ super(message);
2046
+ this.name = "NotReadyError";
2047
+ }
2048
+ };
2049
+ var PublishRejectedError = class extends Error {
2050
+ constructor(message) {
2051
+ super(message);
2052
+ this.name = "PublishRejectedError";
2053
+ }
2054
+ };
2055
+ var TargetError = class extends Error {
2056
+ constructor(message) {
2057
+ super(message);
2058
+ this.name = "TargetError";
2059
+ }
2060
+ };
2061
+ function safe(fn) {
2062
+ try {
2063
+ return fn();
2064
+ } catch {
2065
+ return void 0;
2066
+ }
2067
+ }
2068
+ function errMsg2(err) {
2069
+ return err instanceof Error ? err.message : String(err);
2070
+ }
2071
+ function sanitize(s) {
2072
+ return s.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
2073
+ }
2074
+ function isAnyoneHost(url) {
2075
+ try {
2076
+ return new URL(url).hostname.endsWith(".anyone");
2077
+ } catch {
2078
+ return false;
2079
+ }
2080
+ }
2081
+
2082
+ // src/daemon/routes.ts
2083
+ function registerRoutes(app, runner) {
2084
+ app.get("/status", async () => runner.getStatus());
2085
+ app.post("/publish", async (req, reply) => {
2086
+ const body = req.body;
2087
+ if (!body || !isSignedEvent(body.event)) {
2088
+ return sendError(reply, 400, "invalid_event", {
2089
+ detail: "body.event must be a fully-signed Nostr event (id + sig)."
2090
+ });
2091
+ }
2092
+ try {
2093
+ return await runner.publish(body);
2094
+ } catch (err) {
2095
+ return mapError(reply, err);
2096
+ }
2097
+ });
2098
+ app.post("/subscribe", async (req, reply) => {
2099
+ const body = req.body;
2100
+ if (!body || body.filters === void 0) {
2101
+ return sendError(reply, 400, "invalid_filters", {
2102
+ detail: "body.filters is required (a NIP-01 filter or array of filters)."
2103
+ });
2104
+ }
2105
+ try {
2106
+ return runner.subscribe(body);
2107
+ } catch (err) {
2108
+ return mapError(reply, err);
2109
+ }
2110
+ });
2111
+ app.get("/events", async (req) => {
2112
+ const q = req.query;
2113
+ const query = {};
2114
+ if (q.subId) query.subId = q.subId;
2115
+ if (q.cursor !== void 0) query.cursor = Number(q.cursor);
2116
+ if (q.limit !== void 0) query.limit = Number(q.limit);
2117
+ if (q.relayUrl) query.relayUrl = q.relayUrl;
2118
+ return runner.getEvents(query);
2119
+ });
2120
+ app.post("/channels", async (req, reply) => {
2121
+ try {
2122
+ return await runner.openChannel(req.body?.destination);
2123
+ } catch (err) {
2124
+ return mapError(reply, err);
2125
+ }
2126
+ });
2127
+ app.get("/channels", async () => runner.getChannels());
2128
+ app.post("/swap", async (req, reply) => {
2129
+ const body = req.body;
2130
+ if (!body || !body.destination || body.amount === void 0 || !body.millPubkey || !body.pair || !body.chainRecipient) {
2131
+ return sendError(reply, 400, "invalid_swap", {
2132
+ detail: "body.destination, amount, millPubkey, pair, and chainRecipient are required."
2133
+ });
2134
+ }
2135
+ try {
2136
+ return await runner.swap(body);
2137
+ } catch (err) {
2138
+ return mapError(reply, err);
2139
+ }
2140
+ });
2141
+ app.get("/targets", async () => runner.getTargets());
2142
+ app.post("/relays", async (req, reply) => {
2143
+ const url = req.body?.relayUrl;
2144
+ if (!url) {
2145
+ return sendError(reply, 400, "invalid_relay", {
2146
+ detail: "body.relayUrl is required."
2147
+ });
2148
+ }
2149
+ try {
2150
+ await runner.addRelay(url);
2151
+ return runner.getTargets();
2152
+ } catch (err) {
2153
+ return mapError(reply, err);
2154
+ }
2155
+ });
2156
+ app.delete("/relays", async (req, reply) => {
2157
+ const url = req.body?.relayUrl;
2158
+ if (!url) {
2159
+ return sendError(reply, 400, "invalid_relay", {
2160
+ detail: "body.relayUrl is required."
2161
+ });
2162
+ }
2163
+ try {
2164
+ runner.removeRelay(url);
2165
+ return runner.getTargets();
2166
+ } catch (err) {
2167
+ return mapError(reply, err);
2168
+ }
2169
+ });
2170
+ app.post("/apex", async (req, reply) => {
2171
+ const body = req.body;
2172
+ if (!body || !body.ilpAddress || !body.relayUrl) {
2173
+ return sendError(reply, 400, "invalid_apex", {
2174
+ detail: "body.ilpAddress and body.relayUrl are required."
2175
+ });
2176
+ }
2177
+ try {
2178
+ return await runner.addApex(body);
2179
+ } catch (err) {
2180
+ return mapError(reply, err);
2181
+ }
2182
+ });
2183
+ app.delete("/apex", async (req, reply) => {
2184
+ const url = req.body?.btpUrl;
2185
+ if (!url) {
2186
+ return sendError(reply, 400, "invalid_apex", {
2187
+ detail: "body.btpUrl is required."
2188
+ });
2189
+ }
2190
+ try {
2191
+ await runner.removeApex(url);
2192
+ return runner.getTargets();
2193
+ } catch (err) {
2194
+ return mapError(reply, err);
2195
+ }
2196
+ });
2197
+ }
2198
+ function isSignedEvent(event) {
2199
+ if (typeof event !== "object" || event === null) return false;
2200
+ const e = event;
2201
+ return typeof e["id"] === "string" && typeof e["sig"] === "string" && typeof e["pubkey"] === "string" && typeof e["kind"] === "number";
2202
+ }
2203
+ function mapError(reply, err) {
2204
+ if (err instanceof NotReadyError) {
2205
+ return sendError(reply, 503, "bootstrapping", {
2206
+ detail: err.message,
2207
+ retryable: true
2208
+ });
2209
+ }
2210
+ if (err instanceof PublishRejectedError) {
2211
+ return sendError(reply, 502, "rejected", { detail: err.message });
2212
+ }
2213
+ if (err instanceof TargetError) {
2214
+ const status = /no such/i.test(err.message) ? 404 : 400;
2215
+ return sendError(reply, status, "invalid_target", { detail: err.message });
2216
+ }
2217
+ if (err instanceof ApexDiscoveryError) {
2218
+ return err.retryable ? sendError(reply, 504, "discovery_timeout", {
2219
+ detail: err.message,
2220
+ retryable: true
2221
+ }) : sendError(reply, 502, "discovery_failed", { detail: err.message });
2222
+ }
2223
+ return sendError(reply, 500, "internal_error", {
2224
+ detail: err instanceof Error ? err.message : String(err)
2225
+ });
2226
+ }
2227
+ function sendError(reply, status, error, extra = {}) {
2228
+ return reply.status(status).send({
2229
+ error,
2230
+ ...extra.detail ? { detail: extra.detail } : {},
2231
+ ...extra.retryable ? { retryable: true } : {}
2232
+ });
2233
+ }
2234
+
2235
+ export {
2236
+ defaultKeystorePath,
2237
+ hasConfiguredIdentity,
2238
+ scaffoldFirstRun,
2239
+ RelaySubscription,
2240
+ ClientRunner,
2241
+ NotReadyError,
2242
+ PublishRejectedError,
2243
+ registerRoutes
2244
+ };
2245
+ //# sourceMappingURL=chunk-3NAWISI5.js.map