@dvmkit/sdk 0.1.1-rc.7 → 0.1.3-rc.7

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 (47) hide show
  1. package/README.md +14 -0
  2. package/dist/chunk-2K7E3N2D.js +1709 -0
  3. package/dist/{chunk-Y6W5ZZTW.js → chunk-3ZHMQCYP.js} +666 -3031
  4. package/dist/{chunk-TKA6ZP4M.js → chunk-4B56DEEV.js} +50 -426
  5. package/dist/{chunk-X3IKFWJA.js → chunk-6BQM7TOW.js} +4 -1
  6. package/dist/chunk-BIFLRKMO.js +87 -0
  7. package/dist/chunk-BQ2NMWKE.js +160 -0
  8. package/dist/{chunk-LDTWX7JW.js → chunk-BTZY7VPH.js} +13 -1
  9. package/dist/{chunk-FJDCFHW5.js → chunk-C6JHBLMW.js} +3 -81
  10. package/dist/{chunk-EXHBXA4U.js → chunk-DBCLBYHP.js} +13 -1
  11. package/dist/chunk-EPNDZ5DH.js +1010 -0
  12. package/dist/{chunk-P4RUVDU7.js → chunk-H2MEFVH6.js} +12 -141
  13. package/dist/chunk-KXZUCCEY.js +142 -0
  14. package/dist/{chunk-6JZIX5WW.js → chunk-LLXV32HA.js} +82 -5
  15. package/dist/{chunk-LWUR4CGG.js → chunk-MLRCSJYX.js} +11 -3
  16. package/dist/{chunk-2ABMGUDS.js → chunk-NFRM5QYP.js} +83 -10
  17. package/dist/chunk-RW5LP57K.js +44 -0
  18. package/dist/chunk-YDIYXGYL.js +384 -0
  19. package/dist/{chunk-JGGI65I3.js → chunk-Z4BNLUZF.js} +1 -150
  20. package/dist/{credit-ledger-ED6JXKVD.js → credit-ledger-2DFQHNLB.js} +2 -2
  21. package/dist/{credit-menu-BrTfAr7l.d.ts → credit-menu-C7zAJElJ.d.ts} +1632 -1946
  22. package/dist/{fx-H6K6qcH1.d.ts → fx-BF_SG2i0.d.ts} +1 -1
  23. package/dist/index.d.ts +7 -6
  24. package/dist/index.js +8 -4
  25. package/dist/internal/caller.d.ts +10429 -0
  26. package/dist/internal/caller.js +9957 -0
  27. package/dist/internal/index.d.ts +2 -10816
  28. package/dist/internal/index.js +2 -10130
  29. package/dist/internal/server.d.ts +404 -0
  30. package/dist/internal/server.js +202 -0
  31. package/dist/job-store-Bn23V3QU.d.ts +576 -0
  32. package/dist/lightning-backend-C04nH94l.d.ts +367 -0
  33. package/dist/{memory-credit-ledger-XJ5VQEVP.js → memory-credit-ledger-OP24Z2KO.js} +3 -3
  34. package/dist/{payout-reporter-3UB5WRCV.js → payout-reporter-RG6XNGPI.js} +1 -1
  35. package/dist/{postgres-job-store-J5F4GUWU.js → postgres-job-store-TAONYLIF.js} +1 -1
  36. package/dist/{revenue-reporter-JIKUPXOK.js → revenue-reporter-XXSU5KVB.js} +1 -1
  37. package/dist/server/index.d.ts +27 -11
  38. package/dist/server/index.js +93 -69
  39. package/dist/{job-store-DOSfYnLX.d.ts → step-cache-3cT4Shk0.d.ts} +38 -585
  40. package/dist/{tempo-session-store-DALMRIWN.js → tempo-session-store-2JNOKJGX.js} +2 -2
  41. package/dist/testing/index.d.ts +16 -4
  42. package/dist/testing/index.js +7 -3
  43. package/dist/{usd-Civ738b3.d.ts → usd-BnuXoFl5.d.ts} +1 -1
  44. package/dist/wallet-CJC8lwxx.d.ts +29 -0
  45. package/dist/{x402-FTG2GRAQ.js → x402-T2C5MX3T.js} +6 -3
  46. package/package.json +7 -2
  47. package/dist/{chunk-RU7SXHLO.js → chunk-UP2F5RRT.js} +3 -3
@@ -0,0 +1,1010 @@
1
+ import {
2
+ callerLoggers
3
+ } from "./chunk-66HGCPBU.js";
4
+
5
+ // src/lib/mints.ts
6
+ var RECOMMENDED_MINTS = [
7
+ {
8
+ name: "Testnut",
9
+ description: "Cashu test mint \u2014 use for testing only, not real funds",
10
+ url: "https://testnut.cashu.space"
11
+ },
12
+ {
13
+ name: "Voltz Mint",
14
+ description: "Production Cashu mint by the Voltz Wallet team",
15
+ url: "https://mint.lnvoltz.com"
16
+ }
17
+ ];
18
+ function isTestMintUrl(url) {
19
+ let host;
20
+ try {
21
+ host = new URL(url).hostname.toLowerCase();
22
+ } catch {
23
+ return false;
24
+ }
25
+ if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
26
+ if (LOOPBACK_HOSTS.has(host)) return true;
27
+ if (host.endsWith(".localhost")) return true;
28
+ return TEST_MINT_HOSTS.has(host);
29
+ }
30
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
31
+ var TEST_MINT_HOSTS = /* @__PURE__ */ new Set(["testnut.cashu.space"]);
32
+
33
+ // src/lib/x402/env.ts
34
+ import { isAddress } from "viem";
35
+ import { privateKeyToAccount } from "viem/accounts";
36
+ var X402_SELF_RELAY_KEY = "DVMKIT_X402_SELF_RELAY_KEY";
37
+ var X402_RECEIVER_AUTHORIZER_KEY = "DVMKIT_X402_RECEIVER_AUTHORIZER_KEY";
38
+ var X402_BATCH_SETTLEMENT_KEY = "DVMKIT_X402_BATCH_SETTLEMENT";
39
+ var X402_RPC_URL_KEY = "DVMKIT_X402_RPC_URL";
40
+ var PRIVATE_KEY_RE = /^0x[0-9a-fA-F]{64}$/;
41
+ var X402EnvValidationError = class extends Error {
42
+ constructor(code, variable, message, expectedAddress) {
43
+ super(message);
44
+ this.code = code;
45
+ this.variable = variable;
46
+ this.expectedAddress = expectedAddress;
47
+ this.name = "X402EnvValidationError";
48
+ }
49
+ code;
50
+ variable;
51
+ expectedAddress;
52
+ };
53
+ function validateX402Env(env, opts = {}) {
54
+ const relayKey = env[X402_SELF_RELAY_KEY];
55
+ const authorizerKey = env[X402_RECEIVER_AUTHORIZER_KEY];
56
+ const hasRelayKey = relayKey !== void 0;
57
+ const hasAuthorizerKey = authorizerKey !== void 0;
58
+ const relay = hasRelayKey ? derive(X402_SELF_RELAY_KEY, relayKey) : void 0;
59
+ const authorizer = hasAuthorizerKey ? derive(X402_RECEIVER_AUTHORIZER_KEY, authorizerKey) : void 0;
60
+ const completeWrite = opts.requireCompleteBundle === true && (hasRelayKey || hasAuthorizerKey);
61
+ if (hasRelayKey || completeWrite) {
62
+ if (env[X402_BATCH_SETTLEMENT_KEY] !== "true") {
63
+ fail(
64
+ "x402_bundle_incomplete",
65
+ hasRelayKey ? X402_SELF_RELAY_KEY : X402_RECEIVER_AUTHORIZER_KEY,
66
+ `Changing either x402 private key requires ${X402_BATCH_SETTLEMENT_KEY}=true in the same write.`
67
+ );
68
+ }
69
+ if (!env[X402_RPC_URL_KEY]?.trim()) {
70
+ fail(
71
+ "x402_bundle_incomplete",
72
+ hasRelayKey ? X402_SELF_RELAY_KEY : X402_RECEIVER_AUTHORIZER_KEY,
73
+ `Changing either x402 private key requires an explicit ${X402_RPC_URL_KEY} in the same write.`
74
+ );
75
+ }
76
+ if (!hasAuthorizerKey || completeWrite && !hasRelayKey) {
77
+ if (!completeWrite && !hasAuthorizerKey) {
78
+ fail(
79
+ "x402_bundle_incomplete",
80
+ X402_RECEIVER_AUTHORIZER_KEY,
81
+ `${X402_SELF_RELAY_KEY} requires ${X402_RECEIVER_AUTHORIZER_KEY}.`
82
+ );
83
+ }
84
+ fail(
85
+ "x402_bundle_incomplete",
86
+ hasRelayKey ? X402_RECEIVER_AUTHORIZER_KEY : X402_SELF_RELAY_KEY,
87
+ `Changing either x402 private key requires both ${X402_SELF_RELAY_KEY} and ${X402_RECEIVER_AUTHORIZER_KEY} in the same write.`
88
+ );
89
+ }
90
+ }
91
+ if (hasRelayKey && relayKey.toLowerCase() === authorizerKey?.toLowerCase()) {
92
+ fail(
93
+ "x402_key_reuse",
94
+ X402_SELF_RELAY_KEY,
95
+ `${X402_SELF_RELAY_KEY} and ${X402_RECEIVER_AUTHORIZER_KEY} must use different private keys.`
96
+ );
97
+ }
98
+ assertExpectation(X402_SELF_RELAY_KEY, relay, opts.expectedRelay);
99
+ assertExpectation(X402_RECEIVER_AUTHORIZER_KEY, authorizer, opts.expectedAuthorizer);
100
+ return { ...relay ? { relay } : {}, ...authorizer ? { authorizer } : {} };
101
+ }
102
+ function derive(variable, key) {
103
+ if (!PRIVATE_KEY_RE.test(key)) {
104
+ fail(
105
+ "invalid_x402_private_key",
106
+ variable,
107
+ `${variable} must be a 0x-prefixed 32-byte hex secp256k1 private key.`
108
+ );
109
+ }
110
+ try {
111
+ return privateKeyToAccount(key).address;
112
+ } catch {
113
+ fail(
114
+ "invalid_x402_private_key",
115
+ variable,
116
+ `${variable} must be a valid secp256k1 private key.`
117
+ );
118
+ }
119
+ }
120
+ function assertExpectation(variable, actual, expected) {
121
+ if (expected === void 0) return;
122
+ if (!isAddress(expected, { strict: false })) {
123
+ fail(
124
+ "invalid_x402_expected_address",
125
+ variable,
126
+ `Expected address for ${variable} must be a 0x-prefixed 20-byte EVM address.`
127
+ );
128
+ }
129
+ if (actual?.toLowerCase() !== expected.toLowerCase()) {
130
+ fail(
131
+ "x402_address_mismatch",
132
+ variable,
133
+ `${variable} derives to a different public address than expected ${expected}.`,
134
+ expected
135
+ );
136
+ }
137
+ }
138
+ function fail(code, variable, message, expectedAddress) {
139
+ throw new X402EnvValidationError(code, variable, message, expectedAddress);
140
+ }
141
+
142
+ // src/observability/span-writer.ts
143
+ var noop = () => {
144
+ };
145
+ var SPANS_INSERTED_CHANNEL = "spans_inserted";
146
+ var MAX_BUFFERED_SPANS = 1e4;
147
+ var MAX_ROW_JSONB_BYTES = 64 * 1024;
148
+ var MAX_STRING_VALUE_BYTES = 4 * 1024;
149
+ var FLUSH_THRESHOLD = 1e3;
150
+ var MAX_BACKOFF_EXPONENT = 6;
151
+ var SpanWriter = class {
152
+ constructor(transport, flushIntervalMs = 1e4) {
153
+ this.transport = transport;
154
+ this.flushIntervalMs = flushIntervalMs;
155
+ }
156
+ transport;
157
+ flushIntervalMs;
158
+ buffer = [];
159
+ flushTimer = null;
160
+ flushing = false;
161
+ dropCount = 0;
162
+ truncatedSpanCount = 0;
163
+ consecutiveFailures = 0;
164
+ backoffTimer = null;
165
+ /** Enqueue a span row. Non-blocking — truncates oversize JSONB and appends to buffer. */
166
+ enqueue(row) {
167
+ const originalBytes = JSON.stringify({
168
+ attributes: row.attributes,
169
+ dimensions: row.dimensions
170
+ }).length;
171
+ if (originalBytes > MAX_ROW_JSONB_BYTES) {
172
+ row = this.truncateForSize(row, originalBytes);
173
+ }
174
+ if (this.buffer.length >= MAX_BUFFERED_SPANS) {
175
+ this.dropCount++;
176
+ if (this.dropCount === 1 || this.dropCount % 1e3 === 0) {
177
+ console.error(
178
+ JSON.stringify({
179
+ event: "span_buffer_drop",
180
+ reason: "buffer_full",
181
+ total_dropped: this.dropCount,
182
+ buffer_size: this.buffer.length
183
+ })
184
+ );
185
+ }
186
+ return;
187
+ }
188
+ this.buffer.push(row);
189
+ if (this.buffer.length >= FLUSH_THRESHOLD && !this.flushing) {
190
+ void this.flush().catch(noop);
191
+ }
192
+ }
193
+ /** Start periodic flush to the transport. */
194
+ startFlush() {
195
+ this.flushTimer = setInterval(() => {
196
+ void this.flush().catch(noop);
197
+ }, this.flushIntervalMs);
198
+ }
199
+ /** Stop periodic flush. */
200
+ stopFlush() {
201
+ if (this.flushTimer) {
202
+ clearInterval(this.flushTimer);
203
+ this.flushTimer = null;
204
+ }
205
+ if (this.backoffTimer) {
206
+ clearTimeout(this.backoffTimer);
207
+ this.backoffTimer = null;
208
+ }
209
+ }
210
+ /** Flush buffered spans. Single-flight: concurrent calls exit early. */
211
+ async flush() {
212
+ if (this.flushing) return;
213
+ if (this.buffer.length === 0) return;
214
+ this.flushing = true;
215
+ const batch = this.buffer;
216
+ this.buffer = [];
217
+ try {
218
+ await this.transport.send(batch);
219
+ this.consecutiveFailures = 0;
220
+ } catch (err) {
221
+ this.consecutiveFailures++;
222
+ console.error(
223
+ JSON.stringify({
224
+ event: "span_flush_failed",
225
+ error: err instanceof Error ? err.message : String(err),
226
+ batch_size: batch.length,
227
+ consecutive_failures: this.consecutiveFailures
228
+ })
229
+ );
230
+ const room = MAX_BUFFERED_SPANS - this.buffer.length;
231
+ if (room > 0) {
232
+ this.buffer.unshift(...batch.slice(0, room));
233
+ }
234
+ if (this.consecutiveFailures > 1) {
235
+ const exponent = Math.min(this.consecutiveFailures - 1, MAX_BACKOFF_EXPONENT);
236
+ const backoffMs = this.flushIntervalMs * Math.pow(2, exponent);
237
+ this.stopFlush();
238
+ this.backoffTimer = setTimeout(() => {
239
+ this.backoffTimer = null;
240
+ this.startFlush();
241
+ }, backoffMs);
242
+ }
243
+ } finally {
244
+ this.flushing = false;
245
+ }
246
+ }
247
+ /** Flush remaining spans on shutdown. */
248
+ async shutdown() {
249
+ this.stopFlush();
250
+ await this.flush();
251
+ }
252
+ /** Number of spans dropped due to buffer overflow. */
253
+ getDropCount() {
254
+ return this.dropCount;
255
+ }
256
+ /** Number of spans truncated due to oversized JSONB payload. */
257
+ getTruncatedSpanCount() {
258
+ return this.truncatedSpanCount;
259
+ }
260
+ /** Current buffer size. */
261
+ getBufferSize() {
262
+ return this.buffer.length;
263
+ }
264
+ /** Consecutive flush failures; resets to 0 on the next successful flush. */
265
+ getConsecutiveFailures() {
266
+ return this.consecutiveFailures;
267
+ }
268
+ /**
269
+ * Two-pass truncation: cap individual string values, then fall back to
270
+ * wholesale attribute replacement if the blob is still over the limit.
271
+ */
272
+ truncateForSize(row, originalBytes) {
273
+ const truncatedAttrs = {};
274
+ for (const [key, value] of Object.entries(row.attributes)) {
275
+ if (typeof value === "string" && value.length > MAX_STRING_VALUE_BYTES) {
276
+ const omitted = value.length - MAX_STRING_VALUE_BYTES;
277
+ truncatedAttrs[key] = `${value.slice(0, MAX_STRING_VALUE_BYTES)}\u2026 [truncated, ${omitted} chars omitted]`;
278
+ } else {
279
+ truncatedAttrs[key] = value;
280
+ }
281
+ }
282
+ const afterStringPass = JSON.stringify({
283
+ attributes: truncatedAttrs,
284
+ dimensions: row.dimensions
285
+ }).length;
286
+ let mode;
287
+ if (afterStringPass <= MAX_ROW_JSONB_BYTES) {
288
+ row = { ...row, attributes: truncatedAttrs };
289
+ mode = "string_pass";
290
+ } else {
291
+ const dimSize = JSON.stringify(row.dimensions).length;
292
+ if (dimSize > MAX_ROW_JSONB_BYTES) {
293
+ console.error(
294
+ JSON.stringify({
295
+ event: "span_dimension_oversized",
296
+ name: row.name,
297
+ dataset: row.dataset,
298
+ dimension_bytes: dimSize
299
+ })
300
+ );
301
+ }
302
+ row = {
303
+ ...row,
304
+ attributes: {
305
+ _truncated: true,
306
+ original_bytes: originalBytes,
307
+ original_keys: Object.keys(row.attributes).sort()
308
+ }
309
+ };
310
+ mode = "wholesale_fallback";
311
+ }
312
+ this.truncatedSpanCount++;
313
+ if (this.truncatedSpanCount === 1 || this.truncatedSpanCount % 100 === 0) {
314
+ console.error(
315
+ JSON.stringify({
316
+ event: "span_truncated",
317
+ name: row.name,
318
+ dataset: row.dataset,
319
+ original_bytes: originalBytes,
320
+ after_string_pass_bytes: afterStringPass,
321
+ mode,
322
+ total_truncated: this.truncatedSpanCount
323
+ })
324
+ );
325
+ }
326
+ return row;
327
+ }
328
+ };
329
+ var SPAN_COLUMNS = [
330
+ "span_id",
331
+ "trace_id",
332
+ "parent_span_id",
333
+ "dataset",
334
+ "name",
335
+ "level",
336
+ "status",
337
+ "started_at_ms",
338
+ "ended_at_ms",
339
+ "service",
340
+ "service_version",
341
+ "host",
342
+ "pid",
343
+ "dimensions",
344
+ "attributes",
345
+ "error_message",
346
+ "error_stack",
347
+ "sample_rate"
348
+ ];
349
+ async function insertSpanBatch(pool, rows) {
350
+ if (rows.length === 0) return;
351
+ const values = [];
352
+ const placeholders = [];
353
+ let idx = 1;
354
+ for (const row of rows) {
355
+ const rowPlaceholders = [];
356
+ for (const col of SPAN_COLUMNS) {
357
+ let val = row[col] ?? null;
358
+ if (col === "sample_rate" && val == null) {
359
+ val = 1;
360
+ }
361
+ if (col === "dimensions" || col === "attributes") {
362
+ rowPlaceholders.push(`$${idx}::jsonb`);
363
+ values.push(typeof val === "string" ? val : JSON.stringify(val ?? {}));
364
+ } else {
365
+ rowPlaceholders.push(`$${idx}`);
366
+ values.push(val);
367
+ }
368
+ idx++;
369
+ }
370
+ placeholders.push(`(${rowPlaceholders.join(", ")})`);
371
+ }
372
+ await pool.query(
373
+ `INSERT INTO observability.spans (${SPAN_COLUMNS.join(", ")})
374
+ VALUES ${placeholders.join(",\n")}
375
+ ON CONFLICT (dataset, span_id) DO NOTHING`,
376
+ values
377
+ );
378
+ try {
379
+ await pool.query(`SELECT pg_notify('${SPANS_INSERTED_CHANNEL}', '')`);
380
+ } catch {
381
+ }
382
+ }
383
+ var DbTransport = class {
384
+ constructor(pool) {
385
+ this.pool = pool;
386
+ }
387
+ pool;
388
+ async send(rows) {
389
+ await insertSpanBatch(this.pool, rows);
390
+ }
391
+ };
392
+ var HttpTransport = class {
393
+ constructor(platformUrl, serviceToken) {
394
+ this.platformUrl = platformUrl;
395
+ this.serviceToken = serviceToken;
396
+ }
397
+ platformUrl;
398
+ serviceToken;
399
+ async send(rows) {
400
+ if (rows.length === 0) return;
401
+ const resp = await fetch(`${this.platformUrl.replace(/\/$/, "")}/_internal/spans`, {
402
+ method: "POST",
403
+ headers: {
404
+ "Content-Type": "application/json",
405
+ Authorization: `Bearer ${this.serviceToken}`
406
+ },
407
+ body: JSON.stringify({ spans: rows })
408
+ });
409
+ if (!resp.ok) {
410
+ const text = await resp.text().catch(() => "");
411
+ throw new Error(`Span ingestion failed: ${resp.status} ${text}`);
412
+ }
413
+ }
414
+ };
415
+
416
+ // src/lib/nwc.ts
417
+ import { decrypt, encrypt } from "nostr-tools/nip04";
418
+ import { SimplePool, useWebSocketImplementation } from "nostr-tools/pool";
419
+ import { finalizeEvent, getPublicKey } from "nostr-tools/pure";
420
+ import WebSocket from "ws";
421
+
422
+ // src/lib/hash.ts
423
+ import { createHash } from "crypto";
424
+ function sha256hex(input) {
425
+ return createHash("sha256").update(input).digest("hex");
426
+ }
427
+
428
+ // src/lib/nwc.ts
429
+ var webSocketImplementationConfigured = false;
430
+ function ensureWebSocketImplementation() {
431
+ if (webSocketImplementationConfigured) return;
432
+ useWebSocketImplementation(WebSocket);
433
+ webSocketImplementationConfigured = true;
434
+ }
435
+ var NwcError = class extends Error {
436
+ constructor(code, message, method) {
437
+ super(message);
438
+ this.code = code;
439
+ this.method = method;
440
+ this.name = "NwcError";
441
+ }
442
+ code;
443
+ method;
444
+ };
445
+ function nwcTimeoutMs() {
446
+ return positiveSecondsEnv("DVM_NWC_TIMEOUT_SEC", 30);
447
+ }
448
+ function nwcDedupWindowMs() {
449
+ return positiveSecondsEnv("DVM_NWC_DEDUP_WINDOW_SEC", 240);
450
+ }
451
+ async function withNwcPool(conn, fn, timeoutMs) {
452
+ return withPool(void 0, conn, (pool) => fn({ pool, timeoutMs }));
453
+ }
454
+ function parseNwcUri(uri) {
455
+ if (!uri.startsWith("nostr+walletconnect://")) {
456
+ throw new NwcError("invalid_uri", "NWC URI must start with nostr+walletconnect://");
457
+ }
458
+ const withoutScheme = uri.slice("nostr+walletconnect://".length);
459
+ const [walletPubkey, queryString] = withoutScheme.split("?", 2);
460
+ if (!walletPubkey || !queryString) {
461
+ throw new NwcError("invalid_uri", "NWC URI missing pubkey or query params");
462
+ }
463
+ const params = new URLSearchParams(queryString);
464
+ const relays = params.getAll("relay").filter((r) => r.length > 0);
465
+ const secretHex = params.get("secret");
466
+ if (relays.length === 0) throw new NwcError("invalid_uri", "NWC URI missing relay param");
467
+ if (!secretHex) throw new NwcError("invalid_uri", "NWC URI missing secret param");
468
+ if (!/^[0-9a-fA-F]{64}$/.test(secretHex)) {
469
+ throw new NwcError("invalid_uri", "NWC URI secret must be 64 hex characters");
470
+ }
471
+ const secret = hexToBytes(secretHex);
472
+ const clientPubkey = getPublicKey(secret);
473
+ return { walletPubkey, relays, secret, clientPubkey };
474
+ }
475
+ async function payInvoiceReconciling(conn, bolt11, opts = {}) {
476
+ return callerLoggers.lightning.span(
477
+ "lightning.pay_invoice",
478
+ {
479
+ relays: conn.relays.join(","),
480
+ wallet_pubkey: conn.walletPubkey,
481
+ bolt11_hash: sha256hex(bolt11).slice(0, 16)
482
+ },
483
+ async () => {
484
+ const now = opts.now ?? (() => Date.now());
485
+ const dedupWindowMs = opts.dedupWindowMs ?? nwcDedupWindowMs();
486
+ return withPool(opts.pool, conn, async (pool) => {
487
+ const method = "pay_invoice";
488
+ const event = buildRequestEvent(conn, method, { invoice: bolt11 });
489
+ const publishedAtMs = now();
490
+ const first = await attemptPayment(conn, method, event, pool, opts.timeoutMs);
491
+ if (first.kind === "settled") {
492
+ return settledOutcome(first, "paid");
493
+ }
494
+ if (first.kind === "rejected") throw first.error;
495
+ if (first.kind === "not_sent") {
496
+ throw first.error;
497
+ }
498
+ const reconciled = await reconcile(conn, bolt11, pool, opts.timeoutMs);
499
+ if (reconciled.kind === "settled") {
500
+ callerLoggers.lightning.info("lightning.pay_invoice.reconciled", { republished: false });
501
+ return settledOutcome(reconciled, "reconciled");
502
+ }
503
+ if (reconciled.kind === "unknown") {
504
+ throw new NwcError(
505
+ "payment_unknown",
506
+ "The wallet never answered and could not confirm whether the payment went through."
507
+ );
508
+ }
509
+ if (now() - publishedAtMs >= dedupWindowMs) {
510
+ throw new NwcError(
511
+ "payment_unknown",
512
+ "The wallet never answered and too much time has passed to safely retry the payment."
513
+ );
514
+ }
515
+ callerLoggers.lightning.info("lightning.pay_invoice.republish", { event_id: event.id });
516
+ const second = await attemptPayment(conn, method, event, pool, opts.timeoutMs);
517
+ if (second.kind === "settled") {
518
+ return settledOutcome(second, "republished");
519
+ }
520
+ if (second.kind === "rejected") throw second.error;
521
+ const final = await reconcile(conn, bolt11, pool, opts.timeoutMs);
522
+ if (final.kind === "settled") {
523
+ return settledOutcome(final, "republished");
524
+ }
525
+ throw new NwcError(
526
+ "payment_unknown",
527
+ "The wallet never answered and could not confirm whether the payment went through."
528
+ );
529
+ });
530
+ }
531
+ );
532
+ }
533
+ async function getBalance(conn, opts = {}) {
534
+ return callerLoggers.lightning.span(
535
+ "lightning.get_balance",
536
+ { relays: conn.relays.join(","), wallet_pubkey: conn.walletPubkey },
537
+ async () => {
538
+ const result = await sendNwcRequest(conn, "get_balance", {}, opts);
539
+ const balance = result.balance ?? 0;
540
+ callerLoggers.lightning.info("lightning.get_balance.result", { balance_msats: balance });
541
+ return balance;
542
+ }
543
+ );
544
+ }
545
+ async function getInfo(conn, opts = {}) {
546
+ return callerLoggers.lightning.span(
547
+ "lightning.get_info",
548
+ { relays: conn.relays.join(","), wallet_pubkey: conn.walletPubkey },
549
+ async () => {
550
+ const result = await sendNwcRequest(conn, "get_info", {}, opts);
551
+ const info = { alias: result.alias, methods: result.methods ?? [] };
552
+ const volunteered = extractWalletReportedBudget(result);
553
+ if (volunteered) info.walletReportedBudget = volunteered;
554
+ callerLoggers.lightning.info("lightning.get_info.result", {
555
+ alias: info.alias,
556
+ methods: info.methods,
557
+ wallet_reported_budget: volunteered !== void 0
558
+ });
559
+ return info;
560
+ }
561
+ );
562
+ }
563
+ async function makeInvoice(conn, params, opts = {}) {
564
+ return callerLoggers.lightning.span(
565
+ "lightning.make_invoice",
566
+ {
567
+ relays: conn.relays.join(","),
568
+ wallet_pubkey: conn.walletPubkey,
569
+ amount_msats: params.amountMsats
570
+ },
571
+ async () => {
572
+ const body = { amount: params.amountMsats };
573
+ if (params.description !== void 0) body.description = params.description;
574
+ if (params.expirySeconds !== void 0) body.expiry = params.expirySeconds;
575
+ const result = await sendNwcRequest(conn, "make_invoice", body, opts);
576
+ const tx = toTransaction(result);
577
+ if (!tx.invoice) {
578
+ throw new NwcError("missing_invoice", "Wallet did not return a BOLT11 invoice");
579
+ }
580
+ if (!tx.paymentHash) {
581
+ throw new NwcError("missing_payment_hash", "Wallet did not return a payment hash");
582
+ }
583
+ callerLoggers.lightning.info("lightning.make_invoice.result", {
584
+ payment_hash_hash: sha256hex(tx.paymentHash).slice(0, 16)
585
+ });
586
+ return tx;
587
+ }
588
+ );
589
+ }
590
+ async function lookupInvoice(conn, params, opts = {}) {
591
+ return callerLoggers.lightning.span(
592
+ "lightning.lookup_invoice",
593
+ {
594
+ relays: conn.relays.join(","),
595
+ wallet_pubkey: conn.walletPubkey,
596
+ payment_hash_hash: params.paymentHash ? sha256hex(params.paymentHash).slice(0, 16) : void 0
597
+ },
598
+ async () => {
599
+ const body = {};
600
+ if (params.paymentHash) body.payment_hash = params.paymentHash;
601
+ if (params.invoice) body.invoice = params.invoice;
602
+ if (!body.payment_hash && !body.invoice) {
603
+ throw new NwcError("bad_request", "lookupInvoice requires payment_hash or invoice");
604
+ }
605
+ const result = await sendNwcRequest(conn, "lookup_invoice", body, opts);
606
+ return toTransaction(result);
607
+ }
608
+ );
609
+ }
610
+ async function listTransactions(conn, options, opts = {}) {
611
+ return callerLoggers.lightning.span(
612
+ "lightning.list_transactions",
613
+ {
614
+ relays: conn.relays.join(","),
615
+ wallet_pubkey: conn.walletPubkey,
616
+ from: options.from,
617
+ limit: options.limit
618
+ },
619
+ async () => {
620
+ const body = {};
621
+ if (options.from !== void 0) body.from = options.from;
622
+ if (options.limit !== void 0) body.limit = options.limit;
623
+ const result = await sendNwcRequest(conn, "list_transactions", body, opts);
624
+ if (!Array.isArray(result.transactions)) {
625
+ throw new NwcError("invalid_response", "Wallet did not return a transaction list.");
626
+ }
627
+ const transactions = result.transactions.map(toTransactionSnapshot);
628
+ callerLoggers.lightning.info("lightning.list_transactions.result", {
629
+ transaction_count: transactions.length
630
+ });
631
+ return transactions;
632
+ }
633
+ );
634
+ }
635
+ var VOLUNTEERED_BUDGET_KEYS = [
636
+ "budget",
637
+ "budget_renewal",
638
+ "budget_msats",
639
+ "budget_amount",
640
+ "max_amount",
641
+ "remaining_budget",
642
+ "budget_remaining",
643
+ "renews_at"
644
+ ];
645
+ function extractWalletReportedBudget(result) {
646
+ const out = {};
647
+ for (const key of VOLUNTEERED_BUDGET_KEYS) {
648
+ const value = result[key];
649
+ if (value !== void 0 && value !== null) out[key] = value;
650
+ }
651
+ return Object.keys(out).length > 0 ? out : void 0;
652
+ }
653
+ async function attemptPayment(conn, method, event, pool, timeoutMs) {
654
+ let result;
655
+ try {
656
+ result = await publishAndAwait(conn, method, event, pool, timeoutMs);
657
+ } catch (err) {
658
+ if (err instanceof NwcError && err.code === "relay_unreachable") {
659
+ return { kind: "not_sent", error: err };
660
+ }
661
+ if (err instanceof NwcError && (err.code === "timeout" || err.code === "publish_rejected")) {
662
+ return { kind: "unknown" };
663
+ }
664
+ if (err instanceof NwcError && err.code === "invalid_response") return { kind: "unknown" };
665
+ if (err instanceof NwcError) return { kind: "rejected", error: err };
666
+ throw err;
667
+ }
668
+ if (!result.preimage) {
669
+ return {
670
+ kind: "rejected",
671
+ error: new NwcError("missing_preimage", "Wallet did not return a payment preimage.")
672
+ };
673
+ }
674
+ const feesPaidMsats = optionalFeeMsats(result.fees_paid);
675
+ return {
676
+ kind: "settled",
677
+ preimage: result.preimage,
678
+ ...feesPaidMsats !== void 0 ? { feesPaidMsats } : {}
679
+ };
680
+ }
681
+ async function reconcile(conn, bolt11, pool, timeoutMs) {
682
+ try {
683
+ const tx = await lookupInvoice(conn, { invoice: bolt11 }, { pool, timeoutMs });
684
+ if (tx.invoice !== bolt11) return { kind: "unknown" };
685
+ if (tx.settledAt != null && tx.preimage) {
686
+ return {
687
+ kind: "settled",
688
+ preimage: tx.preimage,
689
+ ...tx.feesPaidMsats !== void 0 ? { feesPaidMsats: tx.feesPaidMsats } : {}
690
+ };
691
+ }
692
+ if (tx.settledAt != null) {
693
+ return {
694
+ kind: "settled",
695
+ preimage: "",
696
+ ...tx.feesPaidMsats !== void 0 ? { feesPaidMsats: tx.feesPaidMsats } : {}
697
+ };
698
+ }
699
+ return { kind: "unpaid" };
700
+ } catch (err) {
701
+ if (err instanceof NwcError && err.code.toUpperCase() === "NOT_FOUND") {
702
+ return { kind: "unpaid" };
703
+ }
704
+ return { kind: "unknown" };
705
+ }
706
+ }
707
+ async function sendNwcRequest(conn, method, params, opts) {
708
+ return withPool(
709
+ opts.pool,
710
+ conn,
711
+ (pool) => publishAndAwait(conn, method, buildRequestEvent(conn, method, params), pool, opts.timeoutMs)
712
+ );
713
+ }
714
+ function buildRequestEvent(conn, method, params) {
715
+ const content = JSON.stringify({ method, params });
716
+ const encrypted = encrypt(conn.secret, conn.walletPubkey, content);
717
+ return finalizeEvent(
718
+ {
719
+ kind: 23194,
720
+ tags: [["p", conn.walletPubkey]],
721
+ content: encrypted,
722
+ created_at: Math.floor(Date.now() / 1e3)
723
+ },
724
+ conn.secret
725
+ );
726
+ }
727
+ async function publishAndAwait(conn, method, event, pool, timeoutMs) {
728
+ const relays = conn.relays;
729
+ const budgetMs = timeoutMs ?? nwcTimeoutMs();
730
+ const waiter = awaitResponse(pool, relays, conn.walletPubkey, event.id, budgetMs);
731
+ try {
732
+ await publishEvent(pool, relays, event);
733
+ } catch (err) {
734
+ waiter.cancel();
735
+ if (err instanceof NwcError) throw err;
736
+ throw new NwcError(
737
+ "publish_rejected",
738
+ `No relay accepted the request (${relays.join(", ")}): ${err instanceof Error ? err.message : String(err)}`
739
+ );
740
+ }
741
+ const response = await waiter.promise;
742
+ if (response === null) {
743
+ throw new NwcError("timeout", `Wallet did not respond within ${Math.round(budgetMs / 1e3)}s`);
744
+ }
745
+ let decrypted;
746
+ try {
747
+ decrypted = decrypt(conn.secret, conn.walletPubkey, response);
748
+ } catch {
749
+ throw new NwcError("invalid_response", "Wallet response could not be decrypted.");
750
+ }
751
+ let parsed;
752
+ try {
753
+ parsed = JSON.parse(decrypted);
754
+ } catch {
755
+ throw new NwcError("invalid_response", "Wallet returned a malformed response.");
756
+ }
757
+ if (parsed.error) {
758
+ throw new NwcError(
759
+ parsed.error.code ?? "wallet_error",
760
+ parsed.error.message ?? "Wallet error",
761
+ method
762
+ );
763
+ }
764
+ return parsed.result ?? {};
765
+ }
766
+ function awaitResponse(pool, relays, walletPubkey, eventId, timeoutMs) {
767
+ let settle;
768
+ const promise = new Promise((resolve) => {
769
+ const done = (value) => {
770
+ clearTimeout(timer);
771
+ sub.close();
772
+ resolve(value);
773
+ };
774
+ settle = done;
775
+ const timer = setTimeout(() => {
776
+ done(null);
777
+ }, timeoutMs);
778
+ const sub = pool.subscribeMany(
779
+ relays,
780
+ {
781
+ kinds: [23195],
782
+ authors: [walletPubkey],
783
+ "#e": [eventId]
784
+ },
785
+ {
786
+ onevent(ev) {
787
+ done(ev.content);
788
+ },
789
+ // required by nostr-tools
790
+ oneose() {
791
+ }
792
+ // eslint-disable-line @typescript-eslint/no-empty-function
793
+ }
794
+ );
795
+ });
796
+ return {
797
+ promise,
798
+ cancel: () => {
799
+ settle?.(null);
800
+ }
801
+ };
802
+ }
803
+ async function withPool(external, conn, fn) {
804
+ ensureWebSocketImplementation();
805
+ if (external) return fn(external);
806
+ const pool = new SimplePool();
807
+ try {
808
+ return await fn(pool);
809
+ } finally {
810
+ pool.close(conn.relays);
811
+ }
812
+ }
813
+ function toTransaction(raw) {
814
+ const feesPaidMsats = optionalFeeMsats(raw.fees_paid);
815
+ return {
816
+ type: raw.type ?? "incoming",
817
+ invoice: raw.invoice,
818
+ description: raw.description,
819
+ paymentHash: raw.payment_hash ?? "",
820
+ preimage: raw.preimage,
821
+ amountMsats: raw.amount ?? 0,
822
+ ...feesPaidMsats !== void 0 ? { feesPaidMsats } : {},
823
+ createdAt: raw.created_at ?? Math.floor(Date.now() / 1e3),
824
+ expiresAt: raw.expires_at,
825
+ settledAt: raw.settled_at ?? void 0
826
+ };
827
+ }
828
+ function toTransactionSnapshot(value) {
829
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
830
+ throw new NwcError("invalid_response", "Wallet returned a malformed transaction.");
831
+ }
832
+ const raw = value;
833
+ if (raw.type !== "incoming" && raw.type !== "outgoing") {
834
+ throw new NwcError("invalid_response", "Wallet returned an invalid transaction type.");
835
+ }
836
+ const amountMsats = requiredNonNegativeInteger(raw.amount, "amount");
837
+ const createdAt = requiredNonNegativeInteger(raw.created_at, "creation time");
838
+ const feesPaidMsats = optionalFeeMsats(raw.fees_paid);
839
+ if (raw.fees_paid != null && feesPaidMsats === void 0) {
840
+ throw new NwcError("invalid_response", "Wallet returned an invalid transaction fee.");
841
+ }
842
+ const settledAt = optionalNonNegativeInteger(raw.settled_at);
843
+ if (raw.settled_at != null && settledAt === void 0) {
844
+ throw new NwcError("invalid_response", "Wallet returned an invalid settlement time.");
845
+ }
846
+ const invoice = optionalPrivateString(raw.invoice, "invoice");
847
+ const paymentHash = optionalPrivateString(raw.payment_hash, "payment hash");
848
+ const reducedIdentity = {
849
+ type: raw.type,
850
+ invoice: invoice ?? null,
851
+ paymentHash: paymentHash ?? null,
852
+ amountMsats,
853
+ feesPaidMsats: feesPaidMsats ?? null,
854
+ createdAt,
855
+ settledAt: settledAt ?? null
856
+ };
857
+ return {
858
+ fingerprint: sha256hex(JSON.stringify(reducedIdentity)),
859
+ ...invoice ? { invoiceHash: sha256hex(invoice) } : {},
860
+ type: raw.type,
861
+ amountMsats,
862
+ ...feesPaidMsats !== void 0 ? { feesPaidMsats } : {},
863
+ createdAt,
864
+ ...settledAt !== void 0 ? { settledAt } : {}
865
+ };
866
+ }
867
+ function settledOutcome(payment, outcome) {
868
+ return {
869
+ preimage: payment.preimage,
870
+ outcome,
871
+ ...payment.feesPaidMsats !== void 0 ? { feesPaidMsats: payment.feesPaidMsats } : {}
872
+ };
873
+ }
874
+ function optionalFeeMsats(value) {
875
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
876
+ }
877
+ function optionalNonNegativeInteger(value) {
878
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
879
+ }
880
+ function requiredNonNegativeInteger(value, field) {
881
+ const parsed = optionalNonNegativeInteger(value);
882
+ if (parsed === void 0) {
883
+ throw new NwcError("invalid_response", `Wallet returned an invalid transaction ${field}.`);
884
+ }
885
+ return parsed;
886
+ }
887
+ function optionalPrivateString(value, field) {
888
+ if (value === void 0 || value === null) return void 0;
889
+ if (typeof value !== "string" || value.length === 0) {
890
+ throw new NwcError("invalid_response", `Wallet returned an invalid transaction ${field}.`);
891
+ }
892
+ return value;
893
+ }
894
+ async function publishEvent(pool, relays, event) {
895
+ const connections = await Promise.allSettled(relays.map((url) => pool.ensureRelay(url)));
896
+ const live = connections.flatMap((c) => c.status === "fulfilled" ? [c.value] : []);
897
+ if (live.length === 0) {
898
+ throw new NwcError(
899
+ "relay_unreachable",
900
+ `Could not connect to any of the wallet's relays (${relays.join(", ")}): ${describeRejections(
901
+ connections
902
+ )}`
903
+ );
904
+ }
905
+ try {
906
+ await Promise.any(live.map((relay) => relay.publish(event)));
907
+ } catch (err) {
908
+ throw new NwcError(
909
+ "publish_rejected",
910
+ `No relay accepted the request (${relays.join(", ")}): ${describeAggregate(err)}`
911
+ );
912
+ }
913
+ }
914
+ function describeRejections(results) {
915
+ const reasons = results.flatMap((r) => r.status === "rejected" ? [errorText(r.reason)] : []);
916
+ return reasons.length > 0 ? reasons.join("; ") : "no relays configured";
917
+ }
918
+ function describeAggregate(err) {
919
+ if (err instanceof AggregateError) {
920
+ return err.errors.map((e) => errorText(e)).join("; ");
921
+ }
922
+ return errorText(err);
923
+ }
924
+ function errorText(err) {
925
+ return err instanceof Error ? err.message : String(err);
926
+ }
927
+ function positiveSecondsEnv(name, fallbackSec) {
928
+ const raw = process.env[name];
929
+ if (!raw) return fallbackSec * 1e3;
930
+ const n = Number(raw);
931
+ if (!Number.isFinite(n) || n <= 0) return fallbackSec * 1e3;
932
+ return Math.floor(n) * 1e3;
933
+ }
934
+ function hexToBytes(hex) {
935
+ const bytes = new Uint8Array(hex.length / 2);
936
+ for (let i = 0; i < bytes.length; i++) {
937
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
938
+ }
939
+ return bytes;
940
+ }
941
+
942
+ // src/lib/nwc-backend.ts
943
+ var NwcBackend = class {
944
+ constructor(conn, opts = {}) {
945
+ this.conn = conn;
946
+ this.opts = opts;
947
+ }
948
+ conn;
949
+ opts;
950
+ async payInvoice(bolt11, _amountMsats) {
951
+ return payInvoiceReconciling(this.conn, bolt11, this.opts);
952
+ }
953
+ async getBalance() {
954
+ return getBalance(this.conn, this.opts);
955
+ }
956
+ async getInfo() {
957
+ return getInfo(this.conn, this.opts);
958
+ }
959
+ async createInvoice(params) {
960
+ const tx = await makeInvoice(this.conn, params, this.opts);
961
+ const expiresAtMs = tx.expiresAt ? tx.expiresAt * 1e3 : Date.now() + (params.expirySeconds ?? 3600) * 1e3;
962
+ return {
963
+ bolt11: tx.invoice ?? "",
964
+ paymentHash: tx.paymentHash,
965
+ amountMsats: tx.amountMsats || params.amountMsats,
966
+ expiresAt: expiresAtMs
967
+ };
968
+ }
969
+ async lookupInvoice(paymentHash) {
970
+ const tx = await lookupInvoice(this.conn, { paymentHash }, this.opts);
971
+ return {
972
+ settled: tx.settledAt != null,
973
+ preimage: tx.preimage,
974
+ settledAt: tx.settledAt != null ? tx.settledAt * 1e3 : void 0,
975
+ expiresAt: tx.expiresAt != null ? tx.expiresAt * 1e3 : void 0
976
+ };
977
+ }
978
+ async listTransactions(options) {
979
+ return listTransactions(this.conn, options, this.opts);
980
+ }
981
+ };
982
+
983
+ export {
984
+ RECOMMENDED_MINTS,
985
+ isTestMintUrl,
986
+ X402_SELF_RELAY_KEY,
987
+ X402_RECEIVER_AUTHORIZER_KEY,
988
+ X402_BATCH_SETTLEMENT_KEY,
989
+ X402_RPC_URL_KEY,
990
+ X402EnvValidationError,
991
+ validateX402Env,
992
+ SPANS_INSERTED_CHANNEL,
993
+ SpanWriter,
994
+ insertSpanBatch,
995
+ DbTransport,
996
+ HttpTransport,
997
+ sha256hex,
998
+ NwcError,
999
+ nwcTimeoutMs,
1000
+ nwcDedupWindowMs,
1001
+ withNwcPool,
1002
+ parseNwcUri,
1003
+ payInvoiceReconciling,
1004
+ getBalance,
1005
+ getInfo,
1006
+ makeInvoice,
1007
+ lookupInvoice,
1008
+ listTransactions,
1009
+ NwcBackend
1010
+ };