@toon-protocol/client-mcp 0.29.6 → 0.31.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 (30) hide show
  1. package/dist/{anon-proxy-W3KMM7GU-L5IBCUAX.js → anon-proxy-W3KMM7GU-FN7ZJY7P.js} +2 -1
  2. package/dist/chunk-4K6S3VPM.js +1567 -0
  3. package/dist/chunk-4K6S3VPM.js.map +1 -0
  4. package/dist/chunk-F22GNSF6.js +12 -0
  5. package/dist/{chunk-OMEKDTJ4.js → chunk-QHNSETFB.js} +19 -8
  6. package/dist/{chunk-OMEKDTJ4.js.map → chunk-QHNSETFB.js.map} +1 -1
  7. package/dist/{chunk-WYWUAKYM.js → chunk-U227QSQG.js} +33 -9
  8. package/dist/chunk-U227QSQG.js.map +1 -0
  9. package/dist/daemon.js +3 -2
  10. package/dist/daemon.js.map +1 -1
  11. package/dist/{ed25519-OFFWPWRE.js → ed25519-UB2JDJJK.js} +2 -1
  12. package/dist/{gateway-QOK47RKS-HB65KIKC.js → gateway-QOK47RKS-SEGTXBR3.js} +2 -1
  13. package/dist/{gateway-QOK47RKS-HB65KIKC.js.map → gateway-QOK47RKS-SEGTXBR3.js.map} +1 -1
  14. package/dist/{hmac-7WSXTWW4.js → hmac-26UC6YKX.js} +2 -1
  15. package/dist/hmac-26UC6YKX.js.map +1 -0
  16. package/dist/index.d.ts +95 -9
  17. package/dist/index.js +4 -3
  18. package/dist/mcp.js +3 -2
  19. package/dist/mcp.js.map +1 -1
  20. package/dist/{sha512-LMOIUNFJ.js → sha512-WYC446ZM.js} +2 -1
  21. package/dist/{sha512-LMOIUNFJ.js.map → sha512-WYC446ZM.js.map} +1 -1
  22. package/dist/{socks5-WTJBYGME-IXWLQDE7.js → socks5-WTJBYGME-6COK4LXW.js} +2 -1
  23. package/dist/{socks5-WTJBYGME-IXWLQDE7.js.map → socks5-WTJBYGME-6COK4LXW.js.map} +1 -1
  24. package/package.json +3 -2
  25. package/dist/chunk-CY5GWIH5.js +0 -625
  26. package/dist/chunk-CY5GWIH5.js.map +0 -1
  27. package/dist/chunk-WYWUAKYM.js.map +0 -1
  28. /package/dist/{anon-proxy-W3KMM7GU-L5IBCUAX.js.map → anon-proxy-W3KMM7GU-FN7ZJY7P.js.map} +0 -0
  29. /package/dist/{ed25519-OFFWPWRE.js.map → chunk-F22GNSF6.js.map} +0 -0
  30. /package/dist/{hmac-7WSXTWW4.js.map → ed25519-UB2JDJJK.js.map} +0 -0
@@ -0,0 +1,1567 @@
1
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
2
+ import {
3
+ ToonError,
4
+ buildIlpPrepare,
5
+ decodeEventFromToon,
6
+ encodeEventToToon
7
+ } from "./chunk-QHNSETFB.js";
8
+ import {
9
+ __require
10
+ } from "./chunk-F22GNSF6.js";
11
+
12
+ // src/relay-subscription.ts
13
+ import { createRequire } from "module";
14
+ var nodeRequire = createRequire(import.meta.url);
15
+ var DEFAULT_BUFFER = 5e3;
16
+ var DEFAULT_BASE_MS = 1e3;
17
+ var DEFAULT_MAX_MS = 3e4;
18
+ var noop = () => void 0;
19
+ var RelaySubscription = class {
20
+ relayUrl;
21
+ socksProxy;
22
+ bufferSize;
23
+ reconnectBaseMs;
24
+ reconnectMaxMs;
25
+ log;
26
+ wsFactory;
27
+ decodeEvent;
28
+ /** Active subscriptions: subId -> filters (re-sent on every (re)connect). */
29
+ subscriptions = /* @__PURE__ */ new Map();
30
+ /** Ring buffer of received events, ordered by ascending `seq`. */
31
+ buffer = [];
32
+ /** De-dup index: event.id -> seq (kept in lockstep with the buffer). */
33
+ seen = /* @__PURE__ */ new Set();
34
+ seqCounter = 0;
35
+ subIdCounter = 0;
36
+ ws = null;
37
+ connected = false;
38
+ closing = false;
39
+ reconnectAttempts = 0;
40
+ reconnectTimer = null;
41
+ constructor(opts) {
42
+ this.relayUrl = opts.relayUrl;
43
+ this.socksProxy = opts.socksProxy;
44
+ this.bufferSize = opts.bufferSize ?? DEFAULT_BUFFER;
45
+ this.reconnectBaseMs = opts.reconnectBaseMs ?? DEFAULT_BASE_MS;
46
+ this.reconnectMaxMs = opts.reconnectMaxMs ?? DEFAULT_MAX_MS;
47
+ this.log = opts.logger ?? noop;
48
+ this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory(this.socksProxy);
49
+ this.decodeEvent = opts.decodeEvent;
50
+ }
51
+ /** Whether the underlying socket is currently open. */
52
+ isConnected() {
53
+ return this.connected;
54
+ }
55
+ /** Number of events currently held in the buffer. */
56
+ bufferedCount() {
57
+ return this.buffer.length;
58
+ }
59
+ /** Active subscription ids. */
60
+ activeSubscriptions() {
61
+ return [...this.subscriptions.keys()];
62
+ }
63
+ /** Open the connection (idempotent). */
64
+ start() {
65
+ this.closing = false;
66
+ if (this.ws) return;
67
+ this.open();
68
+ }
69
+ /**
70
+ * Register a persistent subscription and (if connected) send the REQ.
71
+ * Returns the subscription id (caller-supplied or generated).
72
+ */
73
+ subscribe(filters, subId) {
74
+ const id = subId ?? `sub-${++this.subIdCounter}`;
75
+ const list = Array.isArray(filters) ? filters : [filters];
76
+ this.subscriptions.set(id, list);
77
+ if (this.connected) this.sendReq(id, list);
78
+ return id;
79
+ }
80
+ /** Cancel a subscription and send CLOSE if connected. */
81
+ unsubscribe(subId) {
82
+ if (!this.subscriptions.delete(subId)) return;
83
+ if (this.connected) this.sendRaw(["CLOSE", subId]);
84
+ }
85
+ /**
86
+ * Drain events newer than `cursor`. The cursor is the highest `seq` returned;
87
+ * pass it back to fetch only events received since. Filtering by `subId`
88
+ * restricts to one subscription.
89
+ */
90
+ getEvents(opts = {}) {
91
+ const after = opts.cursor ?? 0;
92
+ const limit = opts.limit ?? 200;
93
+ const matches = this.buffer.filter(
94
+ (b) => b.seq > after && (opts.subId === void 0 || b.subId === opts.subId)
95
+ );
96
+ const page = matches.slice(0, limit);
97
+ const hasMore = matches.length > page.length;
98
+ const last = page.at(-1);
99
+ const cursor = last ? last.seq : after;
100
+ return { events: page.map((b) => b.event), cursor, hasMore };
101
+ }
102
+ /** Close the connection permanently and stop reconnecting. */
103
+ close() {
104
+ this.closing = true;
105
+ if (this.reconnectTimer) {
106
+ clearTimeout(this.reconnectTimer);
107
+ this.reconnectTimer = null;
108
+ }
109
+ if (this.ws) {
110
+ try {
111
+ this.ws.close();
112
+ } catch {
113
+ }
114
+ this.ws = null;
115
+ }
116
+ this.connected = false;
117
+ }
118
+ // ── internals ────────────────────────────────────────────────────────────
119
+ open() {
120
+ let ws;
121
+ try {
122
+ ws = this.wsFactory(this.relayUrl);
123
+ } catch (err) {
124
+ this.log(`[relay] connect failed: ${errMsg(err)}`);
125
+ this.scheduleReconnect();
126
+ return;
127
+ }
128
+ this.ws = ws;
129
+ ws.on("open", () => {
130
+ this.connected = true;
131
+ this.reconnectAttempts = 0;
132
+ this.log(`[relay] connected to ${this.relayUrl}`);
133
+ for (const [id, filters] of this.subscriptions) this.sendReq(id, filters);
134
+ });
135
+ ws.on("message", (data) => this.handleMessage(data));
136
+ ws.on("error", (err) => {
137
+ this.log(`[relay] socket error: ${errMsg(err)}`);
138
+ });
139
+ ws.on("close", () => {
140
+ this.connected = false;
141
+ this.ws = null;
142
+ if (!this.closing) {
143
+ this.log("[relay] disconnected; scheduling reconnect");
144
+ this.scheduleReconnect();
145
+ }
146
+ });
147
+ }
148
+ scheduleReconnect() {
149
+ if (this.closing || this.reconnectTimer) return;
150
+ const delay = Math.min(
151
+ this.reconnectMaxMs,
152
+ this.reconnectBaseMs * 2 ** this.reconnectAttempts
153
+ );
154
+ this.reconnectAttempts += 1;
155
+ this.reconnectTimer = setTimeout(() => {
156
+ this.reconnectTimer = null;
157
+ if (!this.closing) this.open();
158
+ }, delay);
159
+ this.reconnectTimer.unref?.();
160
+ }
161
+ sendReq(subId, filters) {
162
+ this.sendRaw(["REQ", subId, ...filters]);
163
+ }
164
+ sendRaw(message) {
165
+ if (!this.ws || !this.connected) return;
166
+ try {
167
+ this.ws.send(JSON.stringify(message));
168
+ } catch (err) {
169
+ this.log(`[relay] send failed: ${errMsg(err)}`);
170
+ }
171
+ }
172
+ handleMessage(data) {
173
+ let parsed;
174
+ try {
175
+ parsed = JSON.parse(toText(data));
176
+ } catch {
177
+ return;
178
+ }
179
+ if (!Array.isArray(parsed) || parsed.length === 0) return;
180
+ const type = parsed[0];
181
+ switch (type) {
182
+ case "EVENT": {
183
+ const subId = typeof parsed[1] === "string" ? parsed[1] : "";
184
+ const event = this.parseEventPayload(parsed[2]);
185
+ if (event && typeof event.id === "string")
186
+ this.bufferEvent(subId, event);
187
+ break;
188
+ }
189
+ case "EOSE":
190
+ break;
191
+ case "CLOSED":
192
+ this.log(
193
+ `[relay] subscription closed by relay: ${String(parsed[2] ?? "")}`
194
+ );
195
+ break;
196
+ case "NOTICE":
197
+ this.log(`[relay] NOTICE: ${String(parsed[1] ?? "")}`);
198
+ break;
199
+ default:
200
+ break;
201
+ }
202
+ }
203
+ /**
204
+ * Normalise an `EVENT` payload to a NostrEvent. Standard relays send a JSON
205
+ * object; the TOON relay sends a TOON-encoded string, decoded via the injected
206
+ * `decodeEvent`. Returns undefined when it can't be parsed.
207
+ */
208
+ parseEventPayload(raw) {
209
+ if (raw && typeof raw === "object" && typeof raw.id === "string") {
210
+ return raw;
211
+ }
212
+ if (typeof raw === "string" && this.decodeEvent) {
213
+ try {
214
+ return this.decodeEvent(raw);
215
+ } catch (err) {
216
+ this.log(`[relay] event decode failed: ${errMsg(err)}`);
217
+ return void 0;
218
+ }
219
+ }
220
+ return void 0;
221
+ }
222
+ bufferEvent(subId, event) {
223
+ if (this.seen.has(event.id)) return;
224
+ this.seen.add(event.id);
225
+ this.buffer.push({ seq: ++this.seqCounter, subId, event });
226
+ if (this.buffer.length > this.bufferSize) {
227
+ const evicted = this.buffer.shift();
228
+ if (evicted) this.seen.delete(evicted.event.id);
229
+ }
230
+ }
231
+ };
232
+ function toText(data) {
233
+ if (typeof data === "string") return data;
234
+ if (data instanceof Uint8Array) return Buffer.from(data).toString("utf8");
235
+ if (Buffer.isBuffer(data)) return data.toString("utf8");
236
+ return String(data);
237
+ }
238
+ function errMsg(err) {
239
+ return err instanceof Error ? err.message : String(err);
240
+ }
241
+ function defaultWebSocketFactory(socksProxy) {
242
+ return (url) => {
243
+ const WebSocketImpl = nodeRequire("ws");
244
+ let agent;
245
+ if (socksProxy) {
246
+ const { SocksProxyAgent } = nodeRequire("socks-proxy-agent");
247
+ agent = new SocksProxyAgent(socksProxy);
248
+ }
249
+ return new WebSocketImpl(url, agent ? { agent } : void 0);
250
+ };
251
+ }
252
+
253
+ // src/daemon/client-runner.ts
254
+ import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
255
+
256
+ // ../sdk/dist/chunk-UP2VWCW5.js
257
+ var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __require : typeof Proxy !== "undefined" ? new Proxy(x, {
258
+ get: (a, b) => (typeof __require !== "undefined" ? __require : a)[b]
259
+ }) : x)(function(x) {
260
+ if (typeof __require !== "undefined") return __require.apply(this, arguments);
261
+ throw Error('Dynamic require of "' + x + '" is not supported');
262
+ });
263
+
264
+ // ../sdk/dist/chunk-XTHUXP63.js
265
+ import { getPublicKey } from "nostr-tools/pure";
266
+ import { generateSecretKey, getPublicKey as getPublicKey2 } from "nostr-tools/pure";
267
+ import { createRumor, createSeal, createWrap } from "nostr-tools/nip59";
268
+ import {
269
+ encrypt as nip44Encrypt,
270
+ decrypt as nip44Decrypt,
271
+ getConversationKey
272
+ } from "nostr-tools/nip44";
273
+ import { getPublicKey as getPublicKey3 } from "nostr-tools/pure";
274
+ var IdentityError = class extends ToonError {
275
+ constructor(message, cause) {
276
+ super(message, "IDENTITY_ERROR", cause);
277
+ this.name = "IdentityError";
278
+ }
279
+ };
280
+ var GiftWrapError = class extends ToonError {
281
+ constructor(message, cause) {
282
+ super(message, "GIFT_WRAP_ERROR", cause);
283
+ this.name = "GiftWrapError";
284
+ }
285
+ };
286
+ var SwapHandlerError = class extends ToonError {
287
+ constructor(message, cause) {
288
+ super(message, "SWAP_HANDLER_ERROR", cause);
289
+ this.name = "SwapHandlerError";
290
+ }
291
+ };
292
+ var StreamSwapError = class extends Error {
293
+ code;
294
+ constructor(code, message, options) {
295
+ super(message);
296
+ this.name = "StreamSwapError";
297
+ this.code = code;
298
+ if (options && "cause" in options) {
299
+ Object.defineProperty(this, "cause", {
300
+ value: options.cause,
301
+ enumerable: false,
302
+ writable: true,
303
+ configurable: true
304
+ });
305
+ }
306
+ }
307
+ };
308
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
309
+ function base58Decode(str) {
310
+ let zeros = 0;
311
+ for (let i = 0; i < str.length && str[i] === "1"; i++) zeros++;
312
+ let value = 0n;
313
+ for (const ch of str) {
314
+ const idx = BASE58_ALPHABET.indexOf(ch);
315
+ if (idx === -1) throw new IdentityError(`Invalid base58 character: ${ch}`);
316
+ value = value * 58n + BigInt(idx);
317
+ }
318
+ const hex = value === 0n ? "" : value.toString(16);
319
+ const hexPadded = hex.length % 2 ? "0" + hex : hex;
320
+ const rawBytes = [];
321
+ for (let i = 0; i < hexPadded.length; i += 2) {
322
+ rawBytes.push(parseInt(hexPadded.slice(i, i + 2), 16));
323
+ }
324
+ const result = new Uint8Array(zeros + rawBytes.length);
325
+ result.set(rawBytes, zeros);
326
+ return result;
327
+ }
328
+ function validateSecretKey(key2, paramName) {
329
+ if (!(key2 instanceof Uint8Array) || key2.length !== 32) {
330
+ throw new GiftWrapError(
331
+ `${paramName} must be a 32-byte Uint8Array, got ${key2 instanceof Uint8Array ? `${key2.length} bytes` : typeof key2}`
332
+ );
333
+ }
334
+ }
335
+ function validatePubkey(pubkey, paramName) {
336
+ if (typeof pubkey !== "string" || !/^[0-9a-f]{64}$/.test(pubkey)) {
337
+ throw new GiftWrapError(
338
+ `${paramName} must be a 64-character lowercase hex string`
339
+ );
340
+ }
341
+ }
342
+ function wrapSwapPacket(params) {
343
+ const { rumor, senderSecretKey, recipientPubkey } = params;
344
+ validateSecretKey(senderSecretKey, "senderSecretKey");
345
+ validatePubkey(recipientPubkey, "recipientPubkey");
346
+ try {
347
+ const rumorEvent = createRumor(rumor, senderSecretKey);
348
+ const seal = createSeal(rumorEvent, senderSecretKey, recipientPubkey);
349
+ const giftWrap = createWrap(seal, recipientPubkey);
350
+ const ephemeralPubkey = giftWrap.pubkey;
351
+ return { giftWrap, ephemeralPubkey };
352
+ } catch (error) {
353
+ throw new GiftWrapError(
354
+ `Failed to wrap swap packet: ${error instanceof Error ? error.message : String(error)}`,
355
+ error instanceof Error ? error : void 0
356
+ );
357
+ }
358
+ }
359
+ function wrapSwapPacketToToon(params) {
360
+ const {
361
+ rumor,
362
+ senderSecretKey,
363
+ recipientPubkey,
364
+ destination,
365
+ amount,
366
+ expiresAt
367
+ } = params;
368
+ const { giftWrap, ephemeralPubkey } = wrapSwapPacket({
369
+ rumor,
370
+ senderSecretKey,
371
+ recipientPubkey
372
+ });
373
+ const toonBinary = encodeEventToToon(giftWrap);
374
+ const ilpPrepare = buildIlpPrepare({
375
+ destination,
376
+ amount,
377
+ data: toonBinary,
378
+ expiresAt
379
+ });
380
+ return { ilpPrepare, ephemeralPubkey };
381
+ }
382
+ function decryptFulfillClaim(params) {
383
+ const { ciphertext, ephemeralPubkey, recipientSecretKey } = params;
384
+ validateSecretKey(recipientSecretKey, "recipientSecretKey");
385
+ validatePubkey(ephemeralPubkey, "ephemeralPubkey");
386
+ if (!(ciphertext instanceof Uint8Array) || ciphertext.length === 0) {
387
+ throw new GiftWrapError("ciphertext must be a non-empty Uint8Array");
388
+ }
389
+ let conversationKey = null;
390
+ try {
391
+ conversationKey = getConversationKey(recipientSecretKey, ephemeralPubkey);
392
+ const ciphertextString = new TextDecoder().decode(ciphertext);
393
+ const claimString = nip44Decrypt(ciphertextString, conversationKey);
394
+ return new Uint8Array(Buffer.from(claimString, "base64"));
395
+ } catch (error) {
396
+ if (error instanceof GiftWrapError) {
397
+ throw error;
398
+ }
399
+ throw new GiftWrapError(
400
+ `Failed to decrypt FULFILL claim: ${error instanceof Error ? error.message : String(error)}`,
401
+ error instanceof Error ? error : void 0
402
+ );
403
+ } finally {
404
+ if (conversationKey) {
405
+ conversationKey.fill(0);
406
+ conversationKey = null;
407
+ }
408
+ }
409
+ }
410
+ var RATE_REGEX = /^(0|[1-9]\d*)(\.\d+)?$/;
411
+ function applyRate(params) {
412
+ const { sourceAmount, fromScale, toScale, rate } = params;
413
+ if (!RATE_REGEX.test(rate)) {
414
+ throw new SwapHandlerError(`Invalid rate format: ${rate}`);
415
+ }
416
+ if (/^0(\.0+)?$/.test(rate)) {
417
+ throw new SwapHandlerError("Rate is zero (pair not quoting)");
418
+ }
419
+ if (sourceAmount <= 0n) {
420
+ throw new SwapHandlerError(
421
+ `sourceAmount must be positive, got ${sourceAmount}`
422
+ );
423
+ }
424
+ const dotIdx = rate.indexOf(".");
425
+ const integerPart = dotIdx === -1 ? rate : rate.slice(0, dotIdx);
426
+ const fractionalPart = dotIdx === -1 ? "" : rate.slice(dotIdx + 1);
427
+ const rateNumerator = BigInt(integerPart + fractionalPart);
428
+ const rateDenominator = 10n ** BigInt(fractionalPart.length);
429
+ const scaleUp = 10n ** BigInt(toScale);
430
+ const scaleDown = 10n ** BigInt(fromScale);
431
+ return sourceAmount * rateNumerator * scaleUp / (rateDenominator * scaleDown);
432
+ }
433
+ var RATE_REGEX2 = /^(0|[1-9]\d*)(\.\d+)?$/;
434
+ var HEX64_REGEX = /^[0-9a-f]{64}$/;
435
+ var BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;
436
+ function isBase64(s) {
437
+ if (s.length === 0 || s.length % 4 !== 0) return false;
438
+ return BASE64_REGEX.test(s);
439
+ }
440
+ function chunkAmount(total, count) {
441
+ if (!Number.isInteger(count) || count <= 0 || count > Number.MAX_SAFE_INTEGER) {
442
+ throw new StreamSwapError(
443
+ "INVALID_CHUNKING",
444
+ `packetCount must be a positive integer, got ${count}`
445
+ );
446
+ }
447
+ if (total < BigInt(count)) {
448
+ throw new StreamSwapError(
449
+ "INVALID_CHUNKING",
450
+ `totalAmount (${total}) must be >= packetCount (${count}) so per-packet amount >= 1`
451
+ );
452
+ }
453
+ const base = total / BigInt(count);
454
+ const remainder = total - base * BigInt(count);
455
+ const out = new Array(count);
456
+ for (let i = 0; i < count; i++) out[i] = base;
457
+ const last = out[count - 1] ?? 0n;
458
+ out[count - 1] = last + remainder;
459
+ return out;
460
+ }
461
+ function buildSwapRumor(input) {
462
+ const {
463
+ senderPubkey,
464
+ pair,
465
+ sourceAmount,
466
+ packetIndex,
467
+ totalPackets,
468
+ nonce,
469
+ createdAt,
470
+ chainRecipient
471
+ } = input;
472
+ return {
473
+ kind: 20032,
474
+ pubkey: senderPubkey,
475
+ content: "",
476
+ created_at: createdAt,
477
+ tags: [
478
+ ["swap-from", `${pair.from.assetCode}:${pair.from.chain}`],
479
+ ["swap-to", `${pair.to.assetCode}:${pair.to.chain}`],
480
+ ["amount", sourceAmount.toString()],
481
+ ["seq", String(packetIndex), String(totalPackets)],
482
+ ["nonce", Buffer.from(nonce).toString("hex")],
483
+ ["chain-recipient", chainRecipient]
484
+ ]
485
+ };
486
+ }
487
+ var EVM_CHANNEL_ID_REGEX = /^0x[0-9a-f]{64}$/;
488
+ var EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;
489
+ var DECIMAL_UINT_REGEX = /^(0|[1-9]\d*)$/;
490
+ var BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
491
+ function validateChainAddress(value, chain, kind) {
492
+ if (chain.startsWith("evm:")) {
493
+ const normalized = value.toLowerCase();
494
+ if (kind === "channelId") return EVM_CHANNEL_ID_REGEX.test(normalized);
495
+ return EVM_ADDRESS_REGEX.test(normalized);
496
+ }
497
+ if (chain.startsWith("solana:")) {
498
+ if (!BASE58_REGEX.test(value)) return false;
499
+ if (value.length < 32 || value.length > 44) return false;
500
+ try {
501
+ return base58Decode(value).length === 32;
502
+ } catch {
503
+ return false;
504
+ }
505
+ }
506
+ if (chain.startsWith("mina:")) {
507
+ return BASE58_REGEX.test(value) && value.length >= 32;
508
+ }
509
+ return value.length > 0;
510
+ }
511
+ function decodeFulfillMetadata(data, chain) {
512
+ if (data === void 0 || data === null || data === "") {
513
+ throw new StreamSwapError("FULFILL_DECODE_FAILED", "FULFILL data missing");
514
+ }
515
+ if (!isBase64(data)) {
516
+ throw new StreamSwapError(
517
+ "FULFILL_DECODE_FAILED",
518
+ "FULFILL data is not valid base64"
519
+ );
520
+ }
521
+ let jsonBytes;
522
+ try {
523
+ jsonBytes = Buffer.from(data, "base64");
524
+ } catch (err) {
525
+ throw new StreamSwapError(
526
+ "FULFILL_DECODE_FAILED",
527
+ `FULFILL data base64 decode failed: ${err instanceof Error ? err.message : String(err)}`,
528
+ { cause: err }
529
+ );
530
+ }
531
+ let parsed;
532
+ try {
533
+ parsed = JSON.parse(jsonBytes.toString("utf8"));
534
+ } catch (err) {
535
+ throw new StreamSwapError(
536
+ "FULFILL_DECODE_FAILED",
537
+ `FULFILL data JSON parse failed: ${err instanceof Error ? err.message : String(err)}`,
538
+ { cause: err }
539
+ );
540
+ }
541
+ if (!parsed || typeof parsed !== "object") {
542
+ throw new StreamSwapError(
543
+ "FULFILL_DECODE_FAILED",
544
+ "FULFILL metadata is not an object"
545
+ );
546
+ }
547
+ const obj = parsed;
548
+ const claim = obj["claim"];
549
+ const ephemeralPubkey = obj["ephemeralPubkey"];
550
+ if (typeof claim !== "string" || !isBase64(claim)) {
551
+ throw new StreamSwapError(
552
+ "FULFILL_DECODE_FAILED",
553
+ "FULFILL metadata.claim is missing or not base64 string"
554
+ );
555
+ }
556
+ if (typeof ephemeralPubkey !== "string" || !HEX64_REGEX.test(ephemeralPubkey)) {
557
+ throw new StreamSwapError(
558
+ "FULFILL_DECODE_FAILED",
559
+ "FULFILL metadata.ephemeralPubkey is missing or not 64-char hex"
560
+ );
561
+ }
562
+ const result = {
563
+ claim,
564
+ ephemeralPubkey
565
+ };
566
+ if (typeof obj["claimId"] === "string") {
567
+ result.claimId = obj["claimId"];
568
+ }
569
+ if (obj["targetAmount"] !== void 0) {
570
+ const ta = obj["targetAmount"];
571
+ if (typeof ta !== "string" || !/^(0|[1-9]\d*)$/.test(ta)) {
572
+ throw new StreamSwapError(
573
+ "FULFILL_DECODE_FAILED",
574
+ "FULFILL metadata.targetAmount must be a non-negative integer decimal string"
575
+ );
576
+ }
577
+ result.targetAmount = ta;
578
+ }
579
+ const channelId = obj["channelId"];
580
+ if (typeof channelId === "string" && (!chain || validateChainAddress(channelId, chain, "channelId"))) {
581
+ result.channelId = channelId;
582
+ }
583
+ const nonce = obj["nonce"];
584
+ if (typeof nonce === "string" && DECIMAL_UINT_REGEX.test(nonce)) {
585
+ result.nonce = nonce;
586
+ }
587
+ const cumulativeAmount = obj["cumulativeAmount"];
588
+ if (typeof cumulativeAmount === "string" && DECIMAL_UINT_REGEX.test(cumulativeAmount)) {
589
+ result.cumulativeAmount = cumulativeAmount;
590
+ }
591
+ const recipient = obj["recipient"];
592
+ if (typeof recipient === "string" && recipient.length > 0) {
593
+ result.recipient = recipient;
594
+ }
595
+ const millSignerAddress = obj["millSignerAddress"];
596
+ if (typeof millSignerAddress === "string" && (!chain || validateChainAddress(millSignerAddress, chain, "address"))) {
597
+ result.millSignerAddress = millSignerAddress;
598
+ }
599
+ return result;
600
+ }
601
+ var Deferred = class {
602
+ promise;
603
+ resolve;
604
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirror Promise constructor
605
+ reject;
606
+ constructor() {
607
+ this.promise = new Promise((res, rej) => {
608
+ this.resolve = res;
609
+ this.reject = rej;
610
+ });
611
+ }
612
+ };
613
+ var noop2 = () => void 0;
614
+ var NOOP_LOGGER2 = {
615
+ debug: noop2,
616
+ info: noop2,
617
+ warn: noop2,
618
+ error: noop2
619
+ };
620
+ function validateParams(params) {
621
+ if (typeof params.totalAmount !== "bigint" || params.totalAmount <= 0n) {
622
+ throw new StreamSwapError(
623
+ "INVALID_AMOUNT",
624
+ `totalAmount must be a positive bigint, got ${String(params.totalAmount)}`
625
+ );
626
+ }
627
+ const hasCount = params.packetCount !== void 0;
628
+ const hasAmounts = params.packetAmounts !== void 0;
629
+ if (hasCount === hasAmounts) {
630
+ throw new StreamSwapError(
631
+ "INVALID_CHUNKING",
632
+ "Exactly one of packetCount or packetAmounts must be provided"
633
+ );
634
+ }
635
+ if (hasCount) {
636
+ const c = params.packetCount;
637
+ if (!Number.isInteger(c) || c <= 0) {
638
+ throw new StreamSwapError(
639
+ "INVALID_CHUNKING",
640
+ `packetCount must be a positive integer, got ${c}`
641
+ );
642
+ }
643
+ if (BigInt(c) > params.totalAmount) {
644
+ throw new StreamSwapError(
645
+ "INVALID_CHUNKING",
646
+ `packetCount (${c}) exceeds totalAmount (${params.totalAmount}); per-packet amount would be < 1 micro-unit`
647
+ );
648
+ }
649
+ } else {
650
+ const arr = params.packetAmounts;
651
+ if (!Array.isArray(arr) || arr.length === 0) {
652
+ throw new StreamSwapError(
653
+ "INVALID_CHUNKING",
654
+ "packetAmounts must be a non-empty array"
655
+ );
656
+ }
657
+ let sum = 0n;
658
+ for (const a of arr) {
659
+ if (typeof a !== "bigint" || a <= 0n) {
660
+ throw new StreamSwapError(
661
+ "INVALID_CHUNKING",
662
+ `packetAmounts entries must be positive bigint, got ${String(a)}`
663
+ );
664
+ }
665
+ sum += a;
666
+ }
667
+ if (sum !== params.totalAmount) {
668
+ throw new StreamSwapError(
669
+ "INVALID_CHUNKING",
670
+ `sum(packetAmounts) (${sum}) !== totalAmount (${params.totalAmount})`
671
+ );
672
+ }
673
+ }
674
+ if (!(params.senderSecretKey instanceof Uint8Array) || params.senderSecretKey.length !== 32) {
675
+ throw new StreamSwapError(
676
+ "INVALID_STATE",
677
+ "senderSecretKey must be a 32-byte Uint8Array"
678
+ );
679
+ }
680
+ if (typeof params.millPubkey !== "string" || !HEX64_REGEX.test(params.millPubkey)) {
681
+ throw new StreamSwapError(
682
+ "INVALID_STATE",
683
+ "millPubkey must be a 64-char lowercase hex string"
684
+ );
685
+ }
686
+ if (!params.pair || typeof params.pair !== "object") {
687
+ throw new StreamSwapError("INVALID_PAIR", "pair is required");
688
+ }
689
+ 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") {
690
+ throw new StreamSwapError(
691
+ "INVALID_PAIR",
692
+ "pair.from must have { assetCode: string, assetScale: number, chain: string }"
693
+ );
694
+ }
695
+ 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") {
696
+ throw new StreamSwapError(
697
+ "INVALID_PAIR",
698
+ "pair.to must have { assetCode: string, assetScale: number, chain: string }"
699
+ );
700
+ }
701
+ if (typeof params.pair.rate !== "string" || !RATE_REGEX2.test(params.pair.rate)) {
702
+ throw new StreamSwapError(
703
+ "INVALID_PAIR",
704
+ `pair.rate must match ${RATE_REGEX2}, got ${params.pair.rate}`
705
+ );
706
+ }
707
+ try {
708
+ applyRate({
709
+ sourceAmount: 1n,
710
+ fromScale: params.pair.from.assetScale,
711
+ toScale: params.pair.to.assetScale,
712
+ rate: params.pair.rate
713
+ });
714
+ } catch (err) {
715
+ throw new StreamSwapError(
716
+ "INVALID_PAIR",
717
+ `pair failed applyRate sanity check: ${err instanceof Error ? err.message : String(err)}`,
718
+ { cause: err }
719
+ );
720
+ }
721
+ if (params.rateDeviationThreshold !== void 0 && (typeof params.rateDeviationThreshold !== "number" || !Number.isFinite(params.rateDeviationThreshold) || params.rateDeviationThreshold < 0)) {
722
+ throw new StreamSwapError(
723
+ "INVALID_STATE",
724
+ `rateDeviationThreshold must be a non-negative finite number, got ${params.rateDeviationThreshold}`
725
+ );
726
+ }
727
+ if (typeof params.chainRecipient !== "string" || params.chainRecipient.length === 0) {
728
+ throw new StreamSwapError(
729
+ "INVALID_STATE",
730
+ "chainRecipient must be a non-empty string (sender payout address for pair.to.chain)"
731
+ );
732
+ }
733
+ if (!validateChainAddress(
734
+ params.chainRecipient,
735
+ params.pair.to.chain,
736
+ "address"
737
+ )) {
738
+ throw new StreamSwapError(
739
+ "INVALID_CHAIN_RECIPIENT",
740
+ `chainRecipient ${params.chainRecipient} is malformed for chain ${params.pair.to.chain}`
741
+ );
742
+ }
743
+ }
744
+ async function streamSwap(params) {
745
+ return streamSwapControlled(params).result;
746
+ }
747
+ function streamSwapControlled(params) {
748
+ validateParams(params);
749
+ const logger = params.logger ?? NOOP_LOGGER2;
750
+ const schedule = params.packetAmounts ? [...params.packetAmounts] : chunkAmount(params.totalAmount, params.packetCount);
751
+ const frozenPair = Object.freeze({
752
+ from: Object.freeze({ ...params.pair.from }),
753
+ to: Object.freeze({ ...params.pair.to }),
754
+ rate: params.pair.rate
755
+ });
756
+ const senderPubkey = getPublicKey3(params.senderSecretKey);
757
+ let streamState = "running";
758
+ let resumeDeferred = null;
759
+ const controller = {
760
+ pause() {
761
+ if (streamState === "running") {
762
+ streamState = "paused";
763
+ }
764
+ },
765
+ resume() {
766
+ if (streamState === "paused") {
767
+ streamState = "running";
768
+ if (resumeDeferred) {
769
+ resumeDeferred.resolve("resume");
770
+ resumeDeferred = null;
771
+ }
772
+ } else if (streamState === "running") {
773
+ } else {
774
+ throw new StreamSwapError(
775
+ "INVALID_STATE",
776
+ `Cannot resume from state "${streamState}"`
777
+ );
778
+ }
779
+ },
780
+ stop() {
781
+ if (streamState === "completed" || streamState === "failed") return;
782
+ const prev = streamState;
783
+ streamState = "stopped";
784
+ if (prev === "paused" && resumeDeferred) {
785
+ resumeDeferred.resolve("stop");
786
+ resumeDeferred = null;
787
+ }
788
+ },
789
+ get state() {
790
+ return streamState;
791
+ }
792
+ };
793
+ const result = runLoop(
794
+ params,
795
+ frozenPair,
796
+ schedule,
797
+ senderPubkey,
798
+ logger,
799
+ () => streamState,
800
+ (v) => {
801
+ streamState = v;
802
+ },
803
+ () => {
804
+ if (streamState !== "paused") return Promise.resolve("resume");
805
+ if (!resumeDeferred) resumeDeferred = new Deferred();
806
+ return resumeDeferred.promise;
807
+ }
808
+ );
809
+ return { result, controller };
810
+ }
811
+ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, setState, waitForResumeOrStop) {
812
+ const claims = [];
813
+ const rejections = [];
814
+ const errors = [];
815
+ let cumulativeSource = 0n;
816
+ let cumulativeTarget = 0n;
817
+ let packetsSent = 0;
818
+ let abortReason = "complete";
819
+ const totalPackets = schedule.length;
820
+ const isAborted = () => params.signal?.aborted === true;
821
+ packetLoop: for (let packetIndex = 0; packetIndex < totalPackets; packetIndex++) {
822
+ if (isAborted()) {
823
+ abortReason = "aborted";
824
+ break;
825
+ }
826
+ if (getState() === "stopped") {
827
+ abortReason = "stopped";
828
+ break;
829
+ }
830
+ if (getState() === "paused") {
831
+ const resumedBy = await waitForResumeOrStop();
832
+ if (resumedBy === "stop" || getState() === "stopped") {
833
+ abortReason = "stopped";
834
+ break;
835
+ }
836
+ if (isAborted()) {
837
+ abortReason = "aborted";
838
+ break;
839
+ }
840
+ }
841
+ const sourceAmount = schedule[packetIndex];
842
+ if (sourceAmount === void 0) {
843
+ throw new StreamSwapError(
844
+ "INVALID_STATE",
845
+ `schedule[${packetIndex}] is undefined; schedule was mutated mid-stream`
846
+ );
847
+ }
848
+ const nonce = new Uint8Array(16);
849
+ getRandomValues(nonce);
850
+ const rumor = buildSwapRumor({
851
+ senderPubkey,
852
+ pair,
853
+ sourceAmount,
854
+ packetIndex: packetIndex + 1,
855
+ totalPackets,
856
+ nonce,
857
+ createdAt: Math.floor(Date.now() / 1e3),
858
+ chainRecipient: params.chainRecipient
859
+ });
860
+ let toonData;
861
+ try {
862
+ const wrapped = wrapSwapPacketToToon({
863
+ rumor,
864
+ senderSecretKey: params.senderSecretKey,
865
+ recipientPubkey: params.millPubkey,
866
+ destination: params.millIlpAddress,
867
+ amount: sourceAmount
868
+ });
869
+ toonData = new Uint8Array(Buffer.from(wrapped.ilpPrepare.data, "base64"));
870
+ } catch (err) {
871
+ logger.error({
872
+ event: "stream_swap.wrap_failed",
873
+ packetIndex,
874
+ error: err instanceof Error ? err.message : String(err)
875
+ });
876
+ errors.push({
877
+ packetIndex,
878
+ cause: err instanceof Error ? err : new Error(String(err))
879
+ });
880
+ continue;
881
+ }
882
+ let sendResult;
883
+ try {
884
+ sendResult = await params.client.sendSwapPacket({
885
+ destination: params.millIlpAddress,
886
+ amount: sourceAmount,
887
+ toonData,
888
+ timeout: params.packetTimeoutMs ?? 3e4,
889
+ claim: params.claim
890
+ });
891
+ packetsSent += 1;
892
+ } catch (err) {
893
+ logger.error({
894
+ event: "stream_swap.send_failed",
895
+ packetIndex,
896
+ error: err instanceof Error ? err.message : String(err)
897
+ });
898
+ errors.push({
899
+ packetIndex,
900
+ cause: err instanceof Error ? err : new Error(String(err))
901
+ });
902
+ continue;
903
+ }
904
+ if (!sendResult.accepted) {
905
+ const code = sendResult.code ?? "F00";
906
+ const message = sendResult.message ?? "rejected";
907
+ logger.warn({
908
+ event: "stream_swap.packet_rejected",
909
+ packetIndex,
910
+ code,
911
+ message
912
+ });
913
+ rejections.push({ packetIndex, sourceAmount, code, message });
914
+ continue;
915
+ }
916
+ let metadata;
917
+ try {
918
+ metadata = decodeFulfillMetadata(sendResult.data, pair.to.chain);
919
+ } catch (err) {
920
+ logger.error({
921
+ event: "stream_swap.fulfill_decode_failed",
922
+ packetIndex,
923
+ error: err instanceof Error ? err.message : String(err)
924
+ });
925
+ errors.push({
926
+ packetIndex,
927
+ cause: err instanceof Error ? err : new Error(String(err))
928
+ });
929
+ continue;
930
+ }
931
+ const isEvmTarget = pair.to.chain.startsWith("evm:");
932
+ const recipientMatches = metadata.recipient === void 0 || (isEvmTarget ? metadata.recipient.toLowerCase() === params.chainRecipient.toLowerCase() : metadata.recipient === params.chainRecipient);
933
+ if (!recipientMatches) {
934
+ logger.warn({
935
+ event: "stream_swap.recipient_mismatch",
936
+ packetIndex,
937
+ expected: params.chainRecipient,
938
+ actual: metadata.recipient
939
+ });
940
+ rejections.push({
941
+ packetIndex,
942
+ sourceAmount,
943
+ code: "MILL_RECIPIENT_MISMATCH",
944
+ message: `Mill echoed recipient ${metadata.recipient} but sender expected ${params.chainRecipient}`
945
+ });
946
+ continue;
947
+ }
948
+ let claimBytes;
949
+ try {
950
+ const ciphertext = new Uint8Array(Buffer.from(metadata.claim, "base64"));
951
+ claimBytes = decryptFulfillClaim({
952
+ ciphertext,
953
+ ephemeralPubkey: metadata.ephemeralPubkey,
954
+ recipientSecretKey: params.senderSecretKey
955
+ });
956
+ } catch (err) {
957
+ logger.error({
958
+ event: "stream_swap.decrypt_failed",
959
+ packetIndex,
960
+ error: err instanceof Error ? err.message : String(err)
961
+ });
962
+ errors.push({
963
+ packetIndex,
964
+ cause: err instanceof Error ? err : new Error(String(err))
965
+ });
966
+ continue;
967
+ }
968
+ if (claimBytes.length === 0) {
969
+ logger.warn({
970
+ event: "stream_swap.empty_claim_bytes",
971
+ packetIndex
972
+ });
973
+ }
974
+ const expectedTargetAmount = applyRate({
975
+ sourceAmount,
976
+ fromScale: pair.from.assetScale,
977
+ toScale: pair.to.assetScale,
978
+ rate: pair.rate
979
+ });
980
+ const targetAmount = metadata.targetAmount !== void 0 ? BigInt(metadata.targetAmount) : expectedTargetAmount;
981
+ let rateDeviation = 0;
982
+ if (expectedTargetAmount > 0n) {
983
+ const diff = targetAmount >= expectedTargetAmount ? targetAmount - expectedTargetAmount : expectedTargetAmount - targetAmount;
984
+ const scaled = diff * 1000000n / expectedTargetAmount;
985
+ rateDeviation = Number(scaled) / 1e6;
986
+ }
987
+ const advertisedRate = parseFloat(pair.rate);
988
+ let effectiveRate;
989
+ if (targetAmount === expectedTargetAmount) {
990
+ effectiveRate = advertisedRate;
991
+ } else {
992
+ const signedDeviation = targetAmount >= expectedTargetAmount ? rateDeviation : -rateDeviation;
993
+ effectiveRate = advertisedRate * (1 + signedDeviation);
994
+ }
995
+ if (!Number.isFinite(effectiveRate)) {
996
+ effectiveRate = advertisedRate;
997
+ }
998
+ cumulativeSource += sourceAmount;
999
+ cumulativeTarget += targetAmount;
1000
+ const accumulated = {
1001
+ packetIndex,
1002
+ sourceAmount,
1003
+ targetAmount,
1004
+ claimBytes,
1005
+ millEphemeralPubkey: metadata.ephemeralPubkey,
1006
+ pair,
1007
+ receivedAt: Date.now()
1008
+ };
1009
+ if (metadata.claimId !== void 0) accumulated.claimId = metadata.claimId;
1010
+ if (metadata.channelId !== void 0)
1011
+ accumulated.channelId = metadata.channelId;
1012
+ if (metadata.nonce !== void 0) accumulated.nonce = metadata.nonce;
1013
+ if (metadata.cumulativeAmount !== void 0)
1014
+ accumulated.cumulativeAmount = metadata.cumulativeAmount;
1015
+ if (metadata.recipient !== void 0)
1016
+ accumulated.recipient = metadata.recipient;
1017
+ if (metadata.millSignerAddress !== void 0)
1018
+ accumulated.millSignerAddress = metadata.millSignerAddress;
1019
+ claims.push(accumulated);
1020
+ logger.debug({
1021
+ event: "stream_swap.packet_accepted",
1022
+ packetIndex,
1023
+ sourceAmount: sourceAmount.toString(),
1024
+ targetAmount: targetAmount.toString()
1025
+ });
1026
+ if (params.onPacket) {
1027
+ const progress = Object.freeze({
1028
+ index: packetIndex,
1029
+ total: totalPackets,
1030
+ sourceAmount,
1031
+ targetAmount,
1032
+ advertisedRate: pair.rate,
1033
+ effectiveRate,
1034
+ rateDeviation,
1035
+ cumulativeSource,
1036
+ cumulativeTarget,
1037
+ state: getState() === "paused" ? "paused" : getState() === "stopped" ? "stopped" : "running"
1038
+ });
1039
+ try {
1040
+ const maybePromise = params.onPacket(progress);
1041
+ if (maybePromise && typeof maybePromise.then === "function") {
1042
+ await maybePromise;
1043
+ }
1044
+ } catch (err) {
1045
+ logger.warn({
1046
+ event: "stream_swap.callback_threw",
1047
+ packetIndex,
1048
+ error: err instanceof Error ? err.message : String(err)
1049
+ });
1050
+ errors.push({
1051
+ packetIndex,
1052
+ cause: err instanceof Error ? err : new Error(String(err))
1053
+ });
1054
+ abortReason = "callback-throw";
1055
+ break packetLoop;
1056
+ }
1057
+ }
1058
+ if (isAborted()) {
1059
+ abortReason = "aborted";
1060
+ break;
1061
+ }
1062
+ if (getState() === "stopped") {
1063
+ abortReason = "stopped";
1064
+ break;
1065
+ }
1066
+ if (params.rateDeviationThreshold !== void 0 && rateDeviation > params.rateDeviationThreshold) {
1067
+ abortReason = "rate-deviation";
1068
+ break;
1069
+ }
1070
+ }
1071
+ let finalState;
1072
+ if (abortReason === "aborted" || abortReason === "stopped") {
1073
+ finalState = "stopped";
1074
+ } else if (claims.length === 0 && (rejections.length > 0 || errors.length > 0)) {
1075
+ finalState = "failed";
1076
+ if (abortReason === "complete" && rejections.length > 0 && errors.length === 0) {
1077
+ abortReason = "all-rejected";
1078
+ }
1079
+ } else {
1080
+ finalState = "completed";
1081
+ }
1082
+ setState(finalState);
1083
+ return {
1084
+ state: finalState,
1085
+ claims,
1086
+ rejections,
1087
+ errors,
1088
+ abortReason,
1089
+ cumulativeSource,
1090
+ cumulativeTarget,
1091
+ packetsSent,
1092
+ packetsScheduled: totalPackets
1093
+ };
1094
+ }
1095
+ function getRandomValues(buf) {
1096
+ const g = globalThis;
1097
+ if (g.crypto && typeof g.crypto.getRandomValues === "function") {
1098
+ g.crypto.getRandomValues(buf);
1099
+ return buf;
1100
+ }
1101
+ const nodeCrypto = __require2("crypto");
1102
+ if (nodeCrypto.webcrypto?.getRandomValues) {
1103
+ nodeCrypto.webcrypto.getRandomValues(buf);
1104
+ return buf;
1105
+ }
1106
+ if (nodeCrypto.randomFillSync) {
1107
+ nodeCrypto.randomFillSync(buf);
1108
+ return buf;
1109
+ }
1110
+ throw new StreamSwapError(
1111
+ "INVALID_STATE",
1112
+ "No crypto.getRandomValues available in this environment"
1113
+ );
1114
+ }
1115
+
1116
+ // src/daemon/apex-channel-store.ts
1117
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
1118
+ import { dirname } from "path";
1119
+ function key(destination, chain) {
1120
+ return `${destination}|${chain}`;
1121
+ }
1122
+ function readStore(path) {
1123
+ try {
1124
+ return JSON.parse(readFileSync(path, "utf8"));
1125
+ } catch {
1126
+ return {};
1127
+ }
1128
+ }
1129
+ function loadApexChannel(path, destination, chain) {
1130
+ return readStore(path)[key(destination, chain)] ?? null;
1131
+ }
1132
+ function saveApexChannel(path, destination, chain, record) {
1133
+ const store = readStore(path);
1134
+ store[key(destination, chain)] = record;
1135
+ mkdirSync(dirname(path), { recursive: true });
1136
+ writeFileSync(path, JSON.stringify(store, null, 2), { mode: 384 });
1137
+ }
1138
+
1139
+ // src/daemon/client-runner.ts
1140
+ var ClientRunner = class {
1141
+ config;
1142
+ client;
1143
+ relay;
1144
+ log;
1145
+ startedAt = Date.now();
1146
+ bootstrapping = false;
1147
+ ready = false;
1148
+ lastError;
1149
+ /** Channel opened against the default apex destination. */
1150
+ apexChannelId;
1151
+ stopped = false;
1152
+ constructor(deps) {
1153
+ this.config = deps.config;
1154
+ this.client = deps.createClient();
1155
+ this.relay = deps.createRelay?.() ?? new RelaySubscription({
1156
+ relayUrl: deps.config.relayUrl,
1157
+ socksProxy: deps.config.socksProxy,
1158
+ logger: deps.logger,
1159
+ // The TOON relay sends events TOON-encoded (text) on reads, not as JSON.
1160
+ decodeEvent: (raw) => decodeEventFromToon(new TextEncoder().encode(raw))
1161
+ });
1162
+ this.log = deps.logger ?? (() => void 0);
1163
+ }
1164
+ /**
1165
+ * Begin bootstrapping in the background. Resolves once kicked off; the
1166
+ * connection becomes ready asynchronously. Awaitable for tests via the
1167
+ * returned promise of the underlying work.
1168
+ */
1169
+ start() {
1170
+ if (this.bootstrapping || this.ready) return;
1171
+ this.bootstrapping = true;
1172
+ this.relay.start();
1173
+ void this.bootstrap();
1174
+ }
1175
+ /** The background bootstrap routine (exposed for awaiting in tests). */
1176
+ async bootstrap() {
1177
+ try {
1178
+ await this.client.start();
1179
+ this.injectApexNegotiation(this.config.apex);
1180
+ this.apexChannelId = await this.openOrResumeApexChannel();
1181
+ this.routeChildPeersThroughApexChannel();
1182
+ this.ready = true;
1183
+ this.lastError = void 0;
1184
+ this.log(`[runner] ready; apex channel ${this.apexChannelId}`);
1185
+ } catch (err) {
1186
+ this.lastError = err instanceof Error ? err.message : String(err);
1187
+ this.log(`[runner] bootstrap failed: ${this.lastError}`);
1188
+ } finally {
1189
+ this.bootstrapping = false;
1190
+ }
1191
+ }
1192
+ /**
1193
+ * Open the apex channel — or, on a restart, RESUME the existing one.
1194
+ *
1195
+ * `ChannelManager` persists the nonce watermark (by channelId) but not the
1196
+ * peer→channelId mapping, so a naive `openChannel()` after restart re-deposits
1197
+ * into a fresh channel and reverts on-chain. We persist the channelId here and,
1198
+ * when present, `trackChannel()` the live channel (which rehydrates the nonce
1199
+ * from the channel store) — no on-chain write, watermark continues.
1200
+ */
1201
+ async openOrResumeApexChannel() {
1202
+ const { destination, chain, apexChannelStorePath } = this.config;
1203
+ const saved = loadApexChannel(apexChannelStorePath, destination, chain);
1204
+ const cm = this.client.channelManager;
1205
+ if (saved && cm && typeof cm.trackChannel === "function") {
1206
+ cm.trackChannel(saved.channelId, saved.context);
1207
+ this.log(
1208
+ `[runner] resumed apex channel ${saved.channelId} (tracked, no re-deposit)`
1209
+ );
1210
+ return saved.channelId;
1211
+ }
1212
+ const channelId = await this.client.openChannel(destination);
1213
+ if (this.config.apex) {
1214
+ const a = this.config.apex;
1215
+ saveApexChannel(apexChannelStorePath, destination, chain, {
1216
+ channelId,
1217
+ context: {
1218
+ chainType: a.chain,
1219
+ chainId: a.chainId,
1220
+ tokenNetworkAddress: a.tokenNetwork ?? "",
1221
+ tokenAddress: a.tokenAddress,
1222
+ recipient: a.settlementAddress
1223
+ }
1224
+ });
1225
+ }
1226
+ return channelId;
1227
+ }
1228
+ /**
1229
+ * Inject the apex settlement negotiation directly into the ToonClient when
1230
+ * configured. Required for HS / direct-apex mode where bootstrap discovers 0
1231
+ * peers (no relay-based discovery). Mirrors the docker entrypoint's approach.
1232
+ */
1233
+ injectApexNegotiation(apex) {
1234
+ if (!apex) return;
1235
+ const negotiations = this.client.peerNegotiations;
1236
+ if (!(negotiations instanceof Map)) {
1237
+ throw new Error(
1238
+ "ToonClient.peerNegotiations layout changed \u2014 cannot inject apex negotiation"
1239
+ );
1240
+ }
1241
+ negotiations.set(apex.peerId, {
1242
+ chain: apex.chainKey,
1243
+ chainType: apex.chain,
1244
+ chainId: apex.chainId,
1245
+ settlementAddress: apex.settlementAddress,
1246
+ tokenAddress: apex.tokenAddress,
1247
+ tokenNetwork: apex.tokenNetwork
1248
+ });
1249
+ this.log(`[runner] injected apex negotiation for peer "${apex.peerId}"`);
1250
+ }
1251
+ /**
1252
+ * Route additional apex CHILD peers (e.g. `dvm`, `mill`) through the SAME
1253
+ * apex payment channel. In the parent→child apex model the client holds ONE
1254
+ * channel with the apex (g.townhouse) and pays via it regardless of which
1255
+ * child the ILP destination addresses; but `ToonClient.resolvePeerId` keys off
1256
+ * the destination's last segment (`town`/`dvm`/`mill`), so without this each
1257
+ * child would (a) fail the "no negotiation for peer" guard and (b) try to open
1258
+ * a SECOND on-chain channel to the same apex receive (which reverts —
1259
+ * channel-exists). So: inject the same apex negotiation under each child peer
1260
+ * AND pre-map its peer→channel to the already-open apex channel so
1261
+ * `ensureChannel` reuses it (no second open; one shared nonce sequence).
1262
+ */
1263
+ routeChildPeersThroughApexChannel() {
1264
+ const apex = this.config.apex;
1265
+ const children = this.config.apexChildPeers ?? [];
1266
+ if (!apex || !this.apexChannelId || children.length === 0) return;
1267
+ const client = this.client;
1268
+ const negotiations = client.peerNegotiations;
1269
+ const peerChannels = client.channelManager?.peerChannels;
1270
+ if (!(negotiations instanceof Map) || !(peerChannels instanceof Map)) {
1271
+ this.log(
1272
+ "[runner] cannot route child peers \u2014 ToonClient internals layout changed"
1273
+ );
1274
+ return;
1275
+ }
1276
+ for (const peer of children) {
1277
+ if (peer === apex.peerId) continue;
1278
+ negotiations.set(peer, {
1279
+ chain: apex.chainKey,
1280
+ chainType: apex.chain,
1281
+ chainId: apex.chainId,
1282
+ settlementAddress: apex.settlementAddress,
1283
+ tokenAddress: apex.tokenAddress,
1284
+ tokenNetwork: apex.tokenNetwork
1285
+ });
1286
+ peerChannels.set(peer, this.apexChannelId);
1287
+ this.log(
1288
+ `[runner] routed child peer "${peer}" through apex channel ${this.apexChannelId}`
1289
+ );
1290
+ }
1291
+ }
1292
+ isReady() {
1293
+ return this.ready;
1294
+ }
1295
+ isBootstrapping() {
1296
+ return this.bootstrapping;
1297
+ }
1298
+ getStatus() {
1299
+ const net = this.client.getNetworkStatus();
1300
+ const network = net ? ["evm", "solana", "mina"].map((c) => ({
1301
+ chain: c,
1302
+ ready: net[c] === "configured",
1303
+ detail: net[c]
1304
+ })) : void 0;
1305
+ return {
1306
+ uptimeMs: Date.now() - this.startedAt,
1307
+ bootstrapping: this.bootstrapping,
1308
+ ready: this.ready,
1309
+ settlementChain: this.config.chain,
1310
+ identity: {
1311
+ nostrPubkey: safe(() => this.client.getPublicKey()) ?? "",
1312
+ evmAddress: safe(() => this.client.getEvmAddress()),
1313
+ solanaAddress: safe(() => this.client.getSolanaAddress()),
1314
+ minaAddress: safe(() => this.client.getMinaAddress())
1315
+ },
1316
+ transport: {
1317
+ type: this.config.socksProxy ? "socks5" : "direct",
1318
+ socksProxy: this.config.socksProxy,
1319
+ btpUrl: this.config.toonClientConfig.btpUrl
1320
+ },
1321
+ relay: {
1322
+ url: this.config.relayUrl,
1323
+ connected: this.relay.isConnected(),
1324
+ buffered: this.relay.bufferedCount(),
1325
+ subscriptions: this.relay.activeSubscriptions()
1326
+ },
1327
+ ...network ? { network } : {},
1328
+ ...this.lastError ? { lastError: this.lastError } : {}
1329
+ };
1330
+ }
1331
+ /** Pay-to-write a single event. Throws NOT_READY while bootstrapping. */
1332
+ async publish(req) {
1333
+ this.assertReady();
1334
+ const channelId = this.apexChannelId ?? await this.client.openChannel(req.destination);
1335
+ const fee = req.fee !== void 0 ? BigInt(req.fee) : this.config.feePerEvent;
1336
+ const claim = await this.client.signBalanceProof(channelId, fee);
1337
+ const result = await this.client.publishEvent(req.event, {
1338
+ destination: req.destination,
1339
+ claim,
1340
+ ilpAmount: fee
1341
+ });
1342
+ if (!result.success) {
1343
+ throw new PublishRejectedError(result.error ?? "relay rejected event");
1344
+ }
1345
+ return {
1346
+ eventId: result.eventId ?? req.event.id,
1347
+ data: result.data,
1348
+ channelId,
1349
+ nonce: this.client.getChannelNonce(channelId)
1350
+ };
1351
+ }
1352
+ /** Register a free-read subscription (does not require the paid path). */
1353
+ subscribe(req) {
1354
+ const subId = this.relay.subscribe(req.filters, req.subId);
1355
+ return { subId };
1356
+ }
1357
+ /** Drain buffered events newer than the cursor (free read). */
1358
+ getEvents(query) {
1359
+ const opts = {};
1360
+ if (query.subId !== void 0) opts.subId = query.subId;
1361
+ if (query.cursor !== void 0) opts.cursor = query.cursor;
1362
+ if (query.limit !== void 0) opts.limit = query.limit;
1363
+ const { events, cursor, hasMore } = this.relay.getEvents(opts);
1364
+ return { events, cursor, hasMore };
1365
+ }
1366
+ /** Open (or return) a payment channel for a destination. */
1367
+ async openChannel(destination) {
1368
+ this.assertReady();
1369
+ const channelId = await this.client.openChannel(
1370
+ destination ?? this.config.destination
1371
+ );
1372
+ if (!destination || destination === this.config.destination) {
1373
+ this.apexChannelId = channelId;
1374
+ }
1375
+ return { channelId };
1376
+ }
1377
+ /** List tracked channels with their nonce watermark + cumulative amount. */
1378
+ getChannels() {
1379
+ const channels = this.client.getTrackedChannels().map((channelId) => ({
1380
+ channelId,
1381
+ nonce: this.client.getChannelNonce(channelId),
1382
+ cumulativeAmount: this.client.getChannelCumulativeAmount(channelId).toString()
1383
+ }));
1384
+ return { channels };
1385
+ }
1386
+ /**
1387
+ * Swap source asset → target asset against a mill peer.
1388
+ *
1389
+ * Uses SDK `streamSwap`, which builds a NIP-59 gift-wrapped kind:20032 swap
1390
+ * rumor per packet and sends it over the open BTP session. The source-asset
1391
+ * balance proof is signed by the ToonClient's ChannelManager against the apex
1392
+ * channel — so the mill peer MUST be routed via `apexChildPeers` (otherwise
1393
+ * there is no channel to sign against and the mill rejects with F99).
1394
+ *
1395
+ * A fresh ephemeral gift-wrap key is generated per swap (used for sealing the
1396
+ * rumor AND decrypting the FULFILL claims) — independent of the daemon's
1397
+ * settlement identity, so callers never need to expose a key.
1398
+ */
1399
+ async swap(req) {
1400
+ this.assertReady();
1401
+ const senderSecretKey = generateSecretKey2();
1402
+ const result = await streamSwap({
1403
+ client: this.client,
1404
+ millPubkey: req.millPubkey,
1405
+ millIlpAddress: req.destination,
1406
+ pair: req.pair,
1407
+ senderSecretKey,
1408
+ chainRecipient: req.chainRecipient,
1409
+ totalAmount: BigInt(req.amount),
1410
+ packetCount: req.packetCount ?? 1
1411
+ });
1412
+ const firstReject = result.rejections[0];
1413
+ return {
1414
+ accepted: result.claims.length > 0,
1415
+ packetsAccepted: result.claims.length,
1416
+ claims: result.claims.map((c) => ({
1417
+ sourceAmount: c.sourceAmount.toString(),
1418
+ targetAmount: c.targetAmount.toString(),
1419
+ claim: Buffer.from(c.claimBytes).toString("base64"),
1420
+ ...c.channelId ? { channelId: c.channelId } : {},
1421
+ ...c.recipient ? { recipient: c.recipient } : {},
1422
+ ...c.millSignerAddress ? { millSignerAddress: c.millSignerAddress } : {},
1423
+ ...c.claimId ? { claimId: c.claimId } : {},
1424
+ ...c.nonce ? { nonce: c.nonce } : {},
1425
+ ...c.cumulativeAmount ? { cumulativeAmount: c.cumulativeAmount } : {}
1426
+ })),
1427
+ cumulativeSource: result.cumulativeSource.toString(),
1428
+ cumulativeTarget: result.cumulativeTarget.toString(),
1429
+ state: result.state,
1430
+ ...firstReject ? { code: firstReject.code, message: firstReject.message } : {}
1431
+ };
1432
+ }
1433
+ /** Graceful teardown of the relay subscription + ToonClient. */
1434
+ async stop() {
1435
+ if (this.stopped) return;
1436
+ this.stopped = true;
1437
+ this.relay.close();
1438
+ try {
1439
+ await this.client.stop();
1440
+ } catch (err) {
1441
+ this.log(
1442
+ `[runner] client stop error: ${err instanceof Error ? err.message : String(err)}`
1443
+ );
1444
+ }
1445
+ }
1446
+ assertReady() {
1447
+ if (!this.ready) {
1448
+ throw new NotReadyError(
1449
+ this.bootstrapping ? "Daemon is still bootstrapping (BTP/anon coming up) \u2014 retry shortly." : this.lastError ?? "Daemon is not ready."
1450
+ );
1451
+ }
1452
+ }
1453
+ };
1454
+ var NotReadyError = class extends Error {
1455
+ retryable = true;
1456
+ constructor(message) {
1457
+ super(message);
1458
+ this.name = "NotReadyError";
1459
+ }
1460
+ };
1461
+ var PublishRejectedError = class extends Error {
1462
+ constructor(message) {
1463
+ super(message);
1464
+ this.name = "PublishRejectedError";
1465
+ }
1466
+ };
1467
+ function safe(fn) {
1468
+ try {
1469
+ return fn();
1470
+ } catch {
1471
+ return void 0;
1472
+ }
1473
+ }
1474
+
1475
+ // src/daemon/routes.ts
1476
+ function registerRoutes(app, runner) {
1477
+ app.get("/status", async () => runner.getStatus());
1478
+ app.post("/publish", async (req, reply) => {
1479
+ const body = req.body;
1480
+ if (!body || !isSignedEvent(body.event)) {
1481
+ return sendError(reply, 400, "invalid_event", {
1482
+ detail: "body.event must be a fully-signed Nostr event (id + sig)."
1483
+ });
1484
+ }
1485
+ try {
1486
+ return await runner.publish(body);
1487
+ } catch (err) {
1488
+ return mapError(reply, err);
1489
+ }
1490
+ });
1491
+ app.post("/subscribe", async (req, reply) => {
1492
+ const body = req.body;
1493
+ if (!body || body.filters === void 0) {
1494
+ return sendError(reply, 400, "invalid_filters", {
1495
+ detail: "body.filters is required (a NIP-01 filter or array of filters)."
1496
+ });
1497
+ }
1498
+ return runner.subscribe(body);
1499
+ });
1500
+ app.get(
1501
+ "/events",
1502
+ async (req) => {
1503
+ const q = req.query;
1504
+ const query = {};
1505
+ if (q.subId) query.subId = q.subId;
1506
+ if (q.cursor !== void 0) query.cursor = Number(q.cursor);
1507
+ if (q.limit !== void 0) query.limit = Number(q.limit);
1508
+ return runner.getEvents(query);
1509
+ }
1510
+ );
1511
+ app.post("/channels", async (req, reply) => {
1512
+ try {
1513
+ return await runner.openChannel(req.body?.destination);
1514
+ } catch (err) {
1515
+ return mapError(reply, err);
1516
+ }
1517
+ });
1518
+ app.get("/channels", async () => runner.getChannels());
1519
+ app.post("/swap", async (req, reply) => {
1520
+ const body = req.body;
1521
+ if (!body || !body.destination || body.amount === void 0 || !body.millPubkey || !body.pair || !body.chainRecipient) {
1522
+ return sendError(reply, 400, "invalid_swap", {
1523
+ detail: "body.destination, amount, millPubkey, pair, and chainRecipient are required."
1524
+ });
1525
+ }
1526
+ try {
1527
+ return await runner.swap(body);
1528
+ } catch (err) {
1529
+ return mapError(reply, err);
1530
+ }
1531
+ });
1532
+ }
1533
+ function isSignedEvent(event) {
1534
+ if (typeof event !== "object" || event === null) return false;
1535
+ const e = event;
1536
+ return typeof e["id"] === "string" && typeof e["sig"] === "string" && typeof e["pubkey"] === "string" && typeof e["kind"] === "number";
1537
+ }
1538
+ function mapError(reply, err) {
1539
+ if (err instanceof NotReadyError) {
1540
+ return sendError(reply, 503, "bootstrapping", {
1541
+ detail: err.message,
1542
+ retryable: true
1543
+ });
1544
+ }
1545
+ if (err instanceof PublishRejectedError) {
1546
+ return sendError(reply, 502, "rejected", { detail: err.message });
1547
+ }
1548
+ return sendError(reply, 500, "internal_error", {
1549
+ detail: err instanceof Error ? err.message : String(err)
1550
+ });
1551
+ }
1552
+ function sendError(reply, status, error, extra = {}) {
1553
+ return reply.status(status).send({
1554
+ error,
1555
+ ...extra.detail ? { detail: extra.detail } : {},
1556
+ ...extra.retryable ? { retryable: true } : {}
1557
+ });
1558
+ }
1559
+
1560
+ export {
1561
+ RelaySubscription,
1562
+ ClientRunner,
1563
+ NotReadyError,
1564
+ PublishRejectedError,
1565
+ registerRoutes
1566
+ };
1567
+ //# sourceMappingURL=chunk-4K6S3VPM.js.map