@toon-protocol/client-mcp 0.26.2

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 (40) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +261 -0
  3. package/dist/anon-proxy-6N362VEV-M7AX2QD7.js +24 -0
  4. package/dist/anon-proxy-6N362VEV-M7AX2QD7.js.map +1 -0
  5. package/dist/chunk-245J23EB.js +278 -0
  6. package/dist/chunk-245J23EB.js.map +1 -0
  7. package/dist/chunk-2SGZPDGE.js +625 -0
  8. package/dist/chunk-2SGZPDGE.js.map +1 -0
  9. package/dist/chunk-32QD72IL.js +83 -0
  10. package/dist/chunk-32QD72IL.js.map +1 -0
  11. package/dist/chunk-5YIZ2JQO.js +205 -0
  12. package/dist/chunk-5YIZ2JQO.js.map +1 -0
  13. package/dist/chunk-LR7W2ISE.js +657 -0
  14. package/dist/chunk-LR7W2ISE.js.map +1 -0
  15. package/dist/chunk-QTDCFXPF.js +2802 -0
  16. package/dist/chunk-QTDCFXPF.js.map +1 -0
  17. package/dist/chunk-VA7XC4FD.js +185 -0
  18. package/dist/chunk-VA7XC4FD.js.map +1 -0
  19. package/dist/chunk-WMYY5I3H.js +10818 -0
  20. package/dist/chunk-WMYY5I3H.js.map +1 -0
  21. package/dist/daemon.d.ts +1 -0
  22. package/dist/daemon.js +137 -0
  23. package/dist/daemon.js.map +1 -0
  24. package/dist/ed25519-OFFWPWRE.js +26 -0
  25. package/dist/ed25519-OFFWPWRE.js.map +1 -0
  26. package/dist/gateway-QOK47RKS-HB65KIKC.js +15 -0
  27. package/dist/gateway-QOK47RKS-HB65KIKC.js.map +1 -0
  28. package/dist/hmac-7WSXTWW4.js +11 -0
  29. package/dist/hmac-7WSXTWW4.js.map +1 -0
  30. package/dist/index.d.ts +642 -0
  31. package/dist/index.js +59 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/mcp.d.ts +1 -0
  34. package/dist/mcp.js +80 -0
  35. package/dist/mcp.js.map +1 -0
  36. package/dist/sha512-LMOIUNFJ.js +33 -0
  37. package/dist/sha512-LMOIUNFJ.js.map +1 -0
  38. package/dist/socks5-WTJBYGME-IXWLQDE7.js +138 -0
  39. package/dist/socks5-WTJBYGME-IXWLQDE7.js.map +1 -0
  40. package/package.json +59 -0
@@ -0,0 +1,625 @@
1
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
2
+ import {
3
+ decodeEventFromToon
4
+ } from "./chunk-WMYY5I3H.js";
5
+
6
+ // src/relay-subscription.ts
7
+ import { createRequire } from "module";
8
+ var nodeRequire = createRequire(import.meta.url);
9
+ var DEFAULT_BUFFER = 5e3;
10
+ var DEFAULT_BASE_MS = 1e3;
11
+ var DEFAULT_MAX_MS = 3e4;
12
+ var noop = () => void 0;
13
+ var RelaySubscription = class {
14
+ relayUrl;
15
+ socksProxy;
16
+ bufferSize;
17
+ reconnectBaseMs;
18
+ reconnectMaxMs;
19
+ log;
20
+ wsFactory;
21
+ decodeEvent;
22
+ /** Active subscriptions: subId -> filters (re-sent on every (re)connect). */
23
+ subscriptions = /* @__PURE__ */ new Map();
24
+ /** Ring buffer of received events, ordered by ascending `seq`. */
25
+ buffer = [];
26
+ /** De-dup index: event.id -> seq (kept in lockstep with the buffer). */
27
+ seen = /* @__PURE__ */ new Set();
28
+ seqCounter = 0;
29
+ subIdCounter = 0;
30
+ ws = null;
31
+ connected = false;
32
+ closing = false;
33
+ reconnectAttempts = 0;
34
+ reconnectTimer = null;
35
+ constructor(opts) {
36
+ this.relayUrl = opts.relayUrl;
37
+ this.socksProxy = opts.socksProxy;
38
+ this.bufferSize = opts.bufferSize ?? DEFAULT_BUFFER;
39
+ this.reconnectBaseMs = opts.reconnectBaseMs ?? DEFAULT_BASE_MS;
40
+ this.reconnectMaxMs = opts.reconnectMaxMs ?? DEFAULT_MAX_MS;
41
+ this.log = opts.logger ?? noop;
42
+ this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory(this.socksProxy);
43
+ this.decodeEvent = opts.decodeEvent;
44
+ }
45
+ /** Whether the underlying socket is currently open. */
46
+ isConnected() {
47
+ return this.connected;
48
+ }
49
+ /** Number of events currently held in the buffer. */
50
+ bufferedCount() {
51
+ return this.buffer.length;
52
+ }
53
+ /** Active subscription ids. */
54
+ activeSubscriptions() {
55
+ return [...this.subscriptions.keys()];
56
+ }
57
+ /** Open the connection (idempotent). */
58
+ start() {
59
+ this.closing = false;
60
+ if (this.ws) return;
61
+ this.open();
62
+ }
63
+ /**
64
+ * Register a persistent subscription and (if connected) send the REQ.
65
+ * Returns the subscription id (caller-supplied or generated).
66
+ */
67
+ subscribe(filters, subId) {
68
+ const id = subId ?? `sub-${++this.subIdCounter}`;
69
+ const list = Array.isArray(filters) ? filters : [filters];
70
+ this.subscriptions.set(id, list);
71
+ if (this.connected) this.sendReq(id, list);
72
+ return id;
73
+ }
74
+ /** Cancel a subscription and send CLOSE if connected. */
75
+ unsubscribe(subId) {
76
+ if (!this.subscriptions.delete(subId)) return;
77
+ if (this.connected) this.sendRaw(["CLOSE", subId]);
78
+ }
79
+ /**
80
+ * Drain events newer than `cursor`. The cursor is the highest `seq` returned;
81
+ * pass it back to fetch only events received since. Filtering by `subId`
82
+ * restricts to one subscription.
83
+ */
84
+ getEvents(opts = {}) {
85
+ const after = opts.cursor ?? 0;
86
+ const limit = opts.limit ?? 200;
87
+ const matches = this.buffer.filter(
88
+ (b) => b.seq > after && (opts.subId === void 0 || b.subId === opts.subId)
89
+ );
90
+ const page = matches.slice(0, limit);
91
+ const hasMore = matches.length > page.length;
92
+ const last = page.at(-1);
93
+ const cursor = last ? last.seq : after;
94
+ return { events: page.map((b) => b.event), cursor, hasMore };
95
+ }
96
+ /** Close the connection permanently and stop reconnecting. */
97
+ close() {
98
+ this.closing = true;
99
+ if (this.reconnectTimer) {
100
+ clearTimeout(this.reconnectTimer);
101
+ this.reconnectTimer = null;
102
+ }
103
+ if (this.ws) {
104
+ try {
105
+ this.ws.close();
106
+ } catch {
107
+ }
108
+ this.ws = null;
109
+ }
110
+ this.connected = false;
111
+ }
112
+ // ── internals ────────────────────────────────────────────────────────────
113
+ open() {
114
+ let ws;
115
+ try {
116
+ ws = this.wsFactory(this.relayUrl);
117
+ } catch (err) {
118
+ this.log(`[relay] connect failed: ${errMsg(err)}`);
119
+ this.scheduleReconnect();
120
+ return;
121
+ }
122
+ this.ws = ws;
123
+ ws.on("open", () => {
124
+ this.connected = true;
125
+ this.reconnectAttempts = 0;
126
+ this.log(`[relay] connected to ${this.relayUrl}`);
127
+ for (const [id, filters] of this.subscriptions) this.sendReq(id, filters);
128
+ });
129
+ ws.on("message", (data) => this.handleMessage(data));
130
+ ws.on("error", (err) => {
131
+ this.log(`[relay] socket error: ${errMsg(err)}`);
132
+ });
133
+ ws.on("close", () => {
134
+ this.connected = false;
135
+ this.ws = null;
136
+ if (!this.closing) {
137
+ this.log("[relay] disconnected; scheduling reconnect");
138
+ this.scheduleReconnect();
139
+ }
140
+ });
141
+ }
142
+ scheduleReconnect() {
143
+ if (this.closing || this.reconnectTimer) return;
144
+ const delay = Math.min(
145
+ this.reconnectMaxMs,
146
+ this.reconnectBaseMs * 2 ** this.reconnectAttempts
147
+ );
148
+ this.reconnectAttempts += 1;
149
+ this.reconnectTimer = setTimeout(() => {
150
+ this.reconnectTimer = null;
151
+ if (!this.closing) this.open();
152
+ }, delay);
153
+ this.reconnectTimer.unref?.();
154
+ }
155
+ sendReq(subId, filters) {
156
+ this.sendRaw(["REQ", subId, ...filters]);
157
+ }
158
+ sendRaw(message) {
159
+ if (!this.ws || !this.connected) return;
160
+ try {
161
+ this.ws.send(JSON.stringify(message));
162
+ } catch (err) {
163
+ this.log(`[relay] send failed: ${errMsg(err)}`);
164
+ }
165
+ }
166
+ handleMessage(data) {
167
+ let parsed;
168
+ try {
169
+ parsed = JSON.parse(toText(data));
170
+ } catch {
171
+ return;
172
+ }
173
+ if (!Array.isArray(parsed) || parsed.length === 0) return;
174
+ const type = parsed[0];
175
+ switch (type) {
176
+ case "EVENT": {
177
+ const subId = typeof parsed[1] === "string" ? parsed[1] : "";
178
+ const event = this.parseEventPayload(parsed[2]);
179
+ if (event && typeof event.id === "string")
180
+ this.bufferEvent(subId, event);
181
+ break;
182
+ }
183
+ case "EOSE":
184
+ break;
185
+ case "CLOSED":
186
+ this.log(
187
+ `[relay] subscription closed by relay: ${String(parsed[2] ?? "")}`
188
+ );
189
+ break;
190
+ case "NOTICE":
191
+ this.log(`[relay] NOTICE: ${String(parsed[1] ?? "")}`);
192
+ break;
193
+ default:
194
+ break;
195
+ }
196
+ }
197
+ /**
198
+ * Normalise an `EVENT` payload to a NostrEvent. Standard relays send a JSON
199
+ * object; the TOON relay sends a TOON-encoded string, decoded via the injected
200
+ * `decodeEvent`. Returns undefined when it can't be parsed.
201
+ */
202
+ parseEventPayload(raw) {
203
+ if (raw && typeof raw === "object" && typeof raw.id === "string") {
204
+ return raw;
205
+ }
206
+ if (typeof raw === "string" && this.decodeEvent) {
207
+ try {
208
+ return this.decodeEvent(raw);
209
+ } catch (err) {
210
+ this.log(`[relay] event decode failed: ${errMsg(err)}`);
211
+ return void 0;
212
+ }
213
+ }
214
+ return void 0;
215
+ }
216
+ bufferEvent(subId, event) {
217
+ if (this.seen.has(event.id)) return;
218
+ this.seen.add(event.id);
219
+ this.buffer.push({ seq: ++this.seqCounter, subId, event });
220
+ if (this.buffer.length > this.bufferSize) {
221
+ const evicted = this.buffer.shift();
222
+ if (evicted) this.seen.delete(evicted.event.id);
223
+ }
224
+ }
225
+ };
226
+ function toText(data) {
227
+ if (typeof data === "string") return data;
228
+ if (data instanceof Uint8Array) return Buffer.from(data).toString("utf8");
229
+ if (Buffer.isBuffer(data)) return data.toString("utf8");
230
+ return String(data);
231
+ }
232
+ function errMsg(err) {
233
+ return err instanceof Error ? err.message : String(err);
234
+ }
235
+ function defaultWebSocketFactory(socksProxy) {
236
+ return (url) => {
237
+ const WebSocketImpl = nodeRequire("ws");
238
+ let agent;
239
+ if (socksProxy) {
240
+ const { SocksProxyAgent } = nodeRequire("socks-proxy-agent");
241
+ agent = new SocksProxyAgent(socksProxy);
242
+ }
243
+ return new WebSocketImpl(url, agent ? { agent } : void 0);
244
+ };
245
+ }
246
+
247
+ // src/daemon/apex-channel-store.ts
248
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
249
+ import { dirname } from "path";
250
+ function key(destination, chain) {
251
+ return `${destination}|${chain}`;
252
+ }
253
+ function readStore(path) {
254
+ try {
255
+ return JSON.parse(readFileSync(path, "utf8"));
256
+ } catch {
257
+ return {};
258
+ }
259
+ }
260
+ function loadApexChannel(path, destination, chain) {
261
+ return readStore(path)[key(destination, chain)] ?? null;
262
+ }
263
+ function saveApexChannel(path, destination, chain, record) {
264
+ const store = readStore(path);
265
+ store[key(destination, chain)] = record;
266
+ mkdirSync(dirname(path), { recursive: true });
267
+ writeFileSync(path, JSON.stringify(store, null, 2), { mode: 384 });
268
+ }
269
+
270
+ // src/daemon/client-runner.ts
271
+ var ClientRunner = class {
272
+ config;
273
+ client;
274
+ relay;
275
+ log;
276
+ startedAt = Date.now();
277
+ bootstrapping = false;
278
+ ready = false;
279
+ lastError;
280
+ /** Channel opened against the default apex destination. */
281
+ apexChannelId;
282
+ stopped = false;
283
+ constructor(deps) {
284
+ this.config = deps.config;
285
+ this.client = deps.createClient();
286
+ this.relay = deps.createRelay?.() ?? new RelaySubscription({
287
+ relayUrl: deps.config.relayUrl,
288
+ socksProxy: deps.config.socksProxy,
289
+ logger: deps.logger,
290
+ // The TOON relay sends events TOON-encoded (text) on reads, not as JSON.
291
+ decodeEvent: (raw) => decodeEventFromToon(new TextEncoder().encode(raw))
292
+ });
293
+ this.log = deps.logger ?? (() => void 0);
294
+ }
295
+ /**
296
+ * Begin bootstrapping in the background. Resolves once kicked off; the
297
+ * connection becomes ready asynchronously. Awaitable for tests via the
298
+ * returned promise of the underlying work.
299
+ */
300
+ start() {
301
+ if (this.bootstrapping || this.ready) return;
302
+ this.bootstrapping = true;
303
+ this.relay.start();
304
+ void this.bootstrap();
305
+ }
306
+ /** The background bootstrap routine (exposed for awaiting in tests). */
307
+ async bootstrap() {
308
+ try {
309
+ await this.client.start();
310
+ this.injectApexNegotiation(this.config.apex);
311
+ this.apexChannelId = await this.openOrResumeApexChannel();
312
+ this.ready = true;
313
+ this.lastError = void 0;
314
+ this.log(`[runner] ready; apex channel ${this.apexChannelId}`);
315
+ } catch (err) {
316
+ this.lastError = err instanceof Error ? err.message : String(err);
317
+ this.log(`[runner] bootstrap failed: ${this.lastError}`);
318
+ } finally {
319
+ this.bootstrapping = false;
320
+ }
321
+ }
322
+ /**
323
+ * Open the apex channel — or, on a restart, RESUME the existing one.
324
+ *
325
+ * `ChannelManager` persists the nonce watermark (by channelId) but not the
326
+ * peer→channelId mapping, so a naive `openChannel()` after restart re-deposits
327
+ * into a fresh channel and reverts on-chain. We persist the channelId here and,
328
+ * when present, `trackChannel()` the live channel (which rehydrates the nonce
329
+ * from the channel store) — no on-chain write, watermark continues.
330
+ */
331
+ async openOrResumeApexChannel() {
332
+ const { destination, chain, apexChannelStorePath } = this.config;
333
+ const saved = loadApexChannel(apexChannelStorePath, destination, chain);
334
+ const cm = this.client.channelManager;
335
+ if (saved && cm && typeof cm.trackChannel === "function") {
336
+ cm.trackChannel(saved.channelId, saved.context);
337
+ this.log(
338
+ `[runner] resumed apex channel ${saved.channelId} (tracked, no re-deposit)`
339
+ );
340
+ return saved.channelId;
341
+ }
342
+ const channelId = await this.client.openChannel(destination);
343
+ if (this.config.apex) {
344
+ const a = this.config.apex;
345
+ saveApexChannel(apexChannelStorePath, destination, chain, {
346
+ channelId,
347
+ context: {
348
+ chainType: a.chain,
349
+ chainId: a.chainId,
350
+ tokenNetworkAddress: a.tokenNetwork ?? "",
351
+ tokenAddress: a.tokenAddress,
352
+ recipient: a.settlementAddress
353
+ }
354
+ });
355
+ }
356
+ return channelId;
357
+ }
358
+ /**
359
+ * Inject the apex settlement negotiation directly into the ToonClient when
360
+ * configured. Required for HS / direct-apex mode where bootstrap discovers 0
361
+ * peers (no relay-based discovery). Mirrors the docker entrypoint's approach.
362
+ */
363
+ injectApexNegotiation(apex) {
364
+ if (!apex) return;
365
+ const negotiations = this.client.peerNegotiations;
366
+ if (!(negotiations instanceof Map)) {
367
+ throw new Error(
368
+ "ToonClient.peerNegotiations layout changed \u2014 cannot inject apex negotiation"
369
+ );
370
+ }
371
+ negotiations.set(apex.peerId, {
372
+ chain: apex.chainKey,
373
+ chainType: apex.chain,
374
+ chainId: apex.chainId,
375
+ settlementAddress: apex.settlementAddress,
376
+ tokenAddress: apex.tokenAddress,
377
+ tokenNetwork: apex.tokenNetwork
378
+ });
379
+ this.log(`[runner] injected apex negotiation for peer "${apex.peerId}"`);
380
+ }
381
+ isReady() {
382
+ return this.ready;
383
+ }
384
+ isBootstrapping() {
385
+ return this.bootstrapping;
386
+ }
387
+ getStatus() {
388
+ const net = this.client.getNetworkStatus();
389
+ const network = net ? ["evm", "solana", "mina"].map((c) => ({
390
+ chain: c,
391
+ ready: net[c] === "configured",
392
+ detail: net[c]
393
+ })) : void 0;
394
+ return {
395
+ uptimeMs: Date.now() - this.startedAt,
396
+ bootstrapping: this.bootstrapping,
397
+ ready: this.ready,
398
+ settlementChain: this.config.chain,
399
+ identity: {
400
+ nostrPubkey: safe(() => this.client.getPublicKey()) ?? "",
401
+ evmAddress: safe(() => this.client.getEvmAddress()),
402
+ solanaAddress: safe(() => this.client.getSolanaAddress()),
403
+ minaAddress: safe(() => this.client.getMinaAddress())
404
+ },
405
+ transport: {
406
+ type: this.config.socksProxy ? "socks5" : "direct",
407
+ socksProxy: this.config.socksProxy,
408
+ btpUrl: this.config.toonClientConfig.btpUrl
409
+ },
410
+ relay: {
411
+ url: this.config.relayUrl,
412
+ connected: this.relay.isConnected(),
413
+ buffered: this.relay.bufferedCount(),
414
+ subscriptions: this.relay.activeSubscriptions()
415
+ },
416
+ ...network ? { network } : {},
417
+ ...this.lastError ? { lastError: this.lastError } : {}
418
+ };
419
+ }
420
+ /** Pay-to-write a single event. Throws NOT_READY while bootstrapping. */
421
+ async publish(req) {
422
+ this.assertReady();
423
+ const channelId = this.apexChannelId ?? await this.client.openChannel(req.destination);
424
+ const fee = req.fee !== void 0 ? BigInt(req.fee) : this.config.feePerEvent;
425
+ const claim = await this.client.signBalanceProof(channelId, fee);
426
+ const result = await this.client.publishEvent(req.event, {
427
+ destination: req.destination,
428
+ claim,
429
+ ilpAmount: fee
430
+ });
431
+ if (!result.success) {
432
+ throw new PublishRejectedError(result.error ?? "relay rejected event");
433
+ }
434
+ return {
435
+ eventId: result.eventId ?? req.event.id,
436
+ data: result.data,
437
+ channelId,
438
+ nonce: this.client.getChannelNonce(channelId)
439
+ };
440
+ }
441
+ /** Register a free-read subscription (does not require the paid path). */
442
+ subscribe(req) {
443
+ const subId = this.relay.subscribe(req.filters, req.subId);
444
+ return { subId };
445
+ }
446
+ /** Drain buffered events newer than the cursor (free read). */
447
+ getEvents(query) {
448
+ const opts = {};
449
+ if (query.subId !== void 0) opts.subId = query.subId;
450
+ if (query.cursor !== void 0) opts.cursor = query.cursor;
451
+ if (query.limit !== void 0) opts.limit = query.limit;
452
+ const { events, cursor, hasMore } = this.relay.getEvents(opts);
453
+ return { events, cursor, hasMore };
454
+ }
455
+ /** Open (or return) a payment channel for a destination. */
456
+ async openChannel(destination) {
457
+ this.assertReady();
458
+ const channelId = await this.client.openChannel(
459
+ destination ?? this.config.destination
460
+ );
461
+ if (!destination || destination === this.config.destination) {
462
+ this.apexChannelId = channelId;
463
+ }
464
+ return { channelId };
465
+ }
466
+ /** List tracked channels with their nonce watermark + cumulative amount. */
467
+ getChannels() {
468
+ const channels = this.client.getTrackedChannels().map((channelId) => ({
469
+ channelId,
470
+ nonce: this.client.getChannelNonce(channelId),
471
+ cumulativeAmount: this.client.getChannelCumulativeAmount(channelId).toString()
472
+ }));
473
+ return { channels };
474
+ }
475
+ /** Send a swap packet to a mill peer. */
476
+ async swap(req) {
477
+ this.assertReady();
478
+ const toonData = req.toonData ? new Uint8Array(Buffer.from(req.toonData, "base64")) : new Uint8Array(0);
479
+ const result = await this.client.sendSwapPacket({
480
+ destination: req.destination,
481
+ amount: BigInt(req.amount),
482
+ toonData
483
+ });
484
+ return {
485
+ accepted: result.accepted,
486
+ data: result.data,
487
+ code: result.code,
488
+ message: result.message
489
+ };
490
+ }
491
+ /** Graceful teardown of the relay subscription + ToonClient. */
492
+ async stop() {
493
+ if (this.stopped) return;
494
+ this.stopped = true;
495
+ this.relay.close();
496
+ try {
497
+ await this.client.stop();
498
+ } catch (err) {
499
+ this.log(
500
+ `[runner] client stop error: ${err instanceof Error ? err.message : String(err)}`
501
+ );
502
+ }
503
+ }
504
+ assertReady() {
505
+ if (!this.ready) {
506
+ throw new NotReadyError(
507
+ this.bootstrapping ? "Daemon is still bootstrapping (BTP/anon coming up) \u2014 retry shortly." : this.lastError ?? "Daemon is not ready."
508
+ );
509
+ }
510
+ }
511
+ };
512
+ var NotReadyError = class extends Error {
513
+ retryable = true;
514
+ constructor(message) {
515
+ super(message);
516
+ this.name = "NotReadyError";
517
+ }
518
+ };
519
+ var PublishRejectedError = class extends Error {
520
+ constructor(message) {
521
+ super(message);
522
+ this.name = "PublishRejectedError";
523
+ }
524
+ };
525
+ function safe(fn) {
526
+ try {
527
+ return fn();
528
+ } catch {
529
+ return void 0;
530
+ }
531
+ }
532
+
533
+ // src/daemon/routes.ts
534
+ function registerRoutes(app, runner) {
535
+ app.get("/status", async () => runner.getStatus());
536
+ app.post("/publish", async (req, reply) => {
537
+ const body = req.body;
538
+ if (!body || !isSignedEvent(body.event)) {
539
+ return sendError(reply, 400, "invalid_event", {
540
+ detail: "body.event must be a fully-signed Nostr event (id + sig)."
541
+ });
542
+ }
543
+ try {
544
+ return await runner.publish(body);
545
+ } catch (err) {
546
+ return mapError(reply, err);
547
+ }
548
+ });
549
+ app.post("/subscribe", async (req, reply) => {
550
+ const body = req.body;
551
+ if (!body || body.filters === void 0) {
552
+ return sendError(reply, 400, "invalid_filters", {
553
+ detail: "body.filters is required (a NIP-01 filter or array of filters)."
554
+ });
555
+ }
556
+ return runner.subscribe(body);
557
+ });
558
+ app.get(
559
+ "/events",
560
+ async (req) => {
561
+ const q = req.query;
562
+ const query = {};
563
+ if (q.subId) query.subId = q.subId;
564
+ if (q.cursor !== void 0) query.cursor = Number(q.cursor);
565
+ if (q.limit !== void 0) query.limit = Number(q.limit);
566
+ return runner.getEvents(query);
567
+ }
568
+ );
569
+ app.post("/channels", async (req, reply) => {
570
+ try {
571
+ return await runner.openChannel(req.body?.destination);
572
+ } catch (err) {
573
+ return mapError(reply, err);
574
+ }
575
+ });
576
+ app.get("/channels", async () => runner.getChannels());
577
+ app.post("/swap", async (req, reply) => {
578
+ const body = req.body;
579
+ if (!body || !body.destination || body.amount === void 0) {
580
+ return sendError(reply, 400, "invalid_swap", {
581
+ detail: "body.destination and body.amount are required."
582
+ });
583
+ }
584
+ try {
585
+ return await runner.swap(body);
586
+ } catch (err) {
587
+ return mapError(reply, err);
588
+ }
589
+ });
590
+ }
591
+ function isSignedEvent(event) {
592
+ if (typeof event !== "object" || event === null) return false;
593
+ const e = event;
594
+ return typeof e["id"] === "string" && typeof e["sig"] === "string" && typeof e["pubkey"] === "string" && typeof e["kind"] === "number";
595
+ }
596
+ function mapError(reply, err) {
597
+ if (err instanceof NotReadyError) {
598
+ return sendError(reply, 503, "bootstrapping", {
599
+ detail: err.message,
600
+ retryable: true
601
+ });
602
+ }
603
+ if (err instanceof PublishRejectedError) {
604
+ return sendError(reply, 502, "rejected", { detail: err.message });
605
+ }
606
+ return sendError(reply, 500, "internal_error", {
607
+ detail: err instanceof Error ? err.message : String(err)
608
+ });
609
+ }
610
+ function sendError(reply, status, error, extra = {}) {
611
+ return reply.status(status).send({
612
+ error,
613
+ ...extra.detail ? { detail: extra.detail } : {},
614
+ ...extra.retryable ? { retryable: true } : {}
615
+ });
616
+ }
617
+
618
+ export {
619
+ RelaySubscription,
620
+ ClientRunner,
621
+ NotReadyError,
622
+ PublishRejectedError,
623
+ registerRoutes
624
+ };
625
+ //# sourceMappingURL=chunk-2SGZPDGE.js.map