@ghostlytools/cli 1.3.0 → 1.3.1

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 (80) hide show
  1. package/README.md +9 -3
  2. package/dist/assets/{api-CrxVxtY1.js → api-Dq2QXQDk.js} +141 -40
  3. package/dist/assets/api-Dq2QXQDk.js.map +1 -0
  4. package/dist/assets/{api-BiaX6l6f.js → api-TiB5QSG9.js} +2 -2
  5. package/dist/assets/{apiKit-CajiDxdK.js → apiKit-CA2x5GJF.js} +7 -3
  6. package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
  7. package/dist/assets/{apps-CbfiCSIw.js → apps-BxV9i_Y4.js} +3 -4
  8. package/dist/assets/{apps-CbfiCSIw.js.map → apps-BxV9i_Y4.js.map} +1 -1
  9. package/dist/assets/{apps-t23gkc28.js → apps-C-T8jeji.js} +1 -1
  10. package/dist/assets/{arkade-D4BVJFEQ.js → arkade-C_qDmHnm.js} +2 -2
  11. package/dist/assets/{arkade-D4BVJFEQ.js.map → arkade-C_qDmHnm.js.map} +1 -1
  12. package/dist/assets/{backup-Cg52WeQi.js → backup-Bugnn3Ue.js} +1 -1
  13. package/dist/assets/{backup-BAMi2lAq.js → backup-CnMN2oUK.js} +8 -8
  14. package/dist/assets/backup-CnMN2oUK.js.map +1 -0
  15. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  16. package/dist/assets/bitcoind-CZchoh43.js +349 -0
  17. package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
  18. package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
  19. package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
  20. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
  21. package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
  22. package/dist/assets/{buttons-CX1pKB0j.js → buttons-CvNBzh2S.js} +4 -5
  23. package/dist/assets/{buttons-CX1pKB0j.js.map → buttons-CvNBzh2S.js.map} +1 -1
  24. package/dist/assets/{endpoint-Dn7H8qE1.js → endpoint-BOTFMpfS.js} +44 -12
  25. package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
  26. package/dist/assets/{envelope-BvU0fLEt.js → envelope-WjingYSc.js} +2 -2
  27. package/dist/assets/{envelope-BvU0fLEt.js.map → envelope-WjingYSc.js.map} +1 -1
  28. package/dist/assets/{errors-my6VJR_0.js → errors-DO5z6h_l.js} +29 -1
  29. package/dist/assets/errors-DO5z6h_l.js.map +1 -0
  30. package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
  31. package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
  32. package/dist/assets/{host-Cmd205Dc.js → host-CWt-jtqz.js} +41 -16
  33. package/dist/assets/host-CWt-jtqz.js.map +1 -0
  34. package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js → hyperdhtRelay-CPYwSLLS.js} +2 -3
  35. package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js.map → hyperdhtRelay-CPYwSLLS.js.map} +1 -1
  36. package/dist/assets/{irohWeb--YyEA-hX.js → irohWeb-4oChIOIp.js} +2 -3
  37. package/dist/assets/{irohWeb--YyEA-hX.js.map → irohWeb-4oChIOIp.js.map} +1 -1
  38. package/dist/assets/{main-BzNN6bkx.js → main-DbqcdShN.js} +142 -35
  39. package/dist/assets/main-DbqcdShN.js.map +1 -0
  40. package/dist/assets/{mainline-D5D5C-jT.js → mainline-ZkiOnevs.js} +2 -2
  41. package/dist/assets/{mainline-D5D5C-jT.js.map → mainline-ZkiOnevs.js.map} +1 -1
  42. package/dist/assets/{networkReason-DxZWHw6v.js → networkReason-CetKkTPo.js} +2 -2
  43. package/dist/assets/{networkReason-DxZWHw6v.js.map → networkReason-CetKkTPo.js.map} +1 -1
  44. package/dist/assets/nwc-CgkqyvsE.js +612 -0
  45. package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
  46. package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
  47. package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
  48. package/dist/assets/{profiles-B6F4oa9K.js → profiles-BHc9pEYg.js} +2 -2
  49. package/dist/assets/{profiles-B6F4oa9K.js.map → profiles-BHc9pEYg.js.map} +1 -1
  50. package/dist/assets/registry-DlAKO0Fy.js +3793 -0
  51. package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
  52. package/dist/assets/registry-OBJBnHVQ.js +2 -0
  53. package/dist/assets/resolve-CouHhCpJ.js +591 -0
  54. package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
  55. package/dist/assets/{server-CBCEYKMN.js → server-Ss0gJ-nw.js} +184 -1181
  56. package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
  57. package/dist/assets/{src-DhfGgAUu.js → src-DDzSERyc.js} +2249 -72
  58. package/dist/assets/src-DDzSERyc.js.map +1 -0
  59. package/dist/assets/{turnRefs-ChEfgbSX.js → turnRefs-B87dtrHM.js} +2 -2
  60. package/dist/assets/{turnRefs-ChEfgbSX.js.map → turnRefs-B87dtrHM.js.map} +1 -1
  61. package/dist/assets/types-C4_KafL_.js +122 -0
  62. package/dist/assets/types-C4_KafL_.js.map +1 -0
  63. package/dist/assets/{usdt-C1Xao2xH.js → usdt-Fj8CQO2z.js} +3 -3
  64. package/dist/assets/{usdt-C1Xao2xH.js.map → usdt-Fj8CQO2z.js.map} +1 -1
  65. package/dist/ghostly.mjs +1 -1
  66. package/package.json +1 -1
  67. package/dist/assets/api-CrxVxtY1.js.map +0 -1
  68. package/dist/assets/apiKit-CajiDxdK.js.map +0 -1
  69. package/dist/assets/backup-BAMi2lAq.js.map +0 -1
  70. package/dist/assets/buttons-CWl2yNJu.js +0 -2
  71. package/dist/assets/endpoint-Dn7H8qE1.js.map +0 -1
  72. package/dist/assets/errors-my6VJR_0.js.map +0 -1
  73. package/dist/assets/groupFiles-BNTSDaoq.js +0 -7685
  74. package/dist/assets/groupFiles-BNTSDaoq.js.map +0 -1
  75. package/dist/assets/groupFiles-DBbjOLuD.js +0 -2088
  76. package/dist/assets/groupFiles-DBbjOLuD.js.map +0 -1
  77. package/dist/assets/host-Cmd205Dc.js.map +0 -1
  78. package/dist/assets/main-BzNN6bkx.js.map +0 -1
  79. package/dist/assets/server-CBCEYKMN.js.map +0 -1
  80. package/dist/assets/src-DhfGgAUu.js.map +0 -1
@@ -0,0 +1,612 @@
1
+ import { Fn as decodeBolt11 } from "./src-DDzSERyc.js";
2
+ import { t as NothingSpentError } from "./types-C4_KafL_.js";
3
+ import { n as parseNwcUri } from "./nwcProvider-4eVz_8HP.js";
4
+ import { sha256 } from "@noble/hashes/sha2.js";
5
+ import * as nip04 from "nostr-tools/nip04";
6
+ import * as nip44 from "nostr-tools/nip44";
7
+ import { finalizeEvent, getPublicKey, verifyEvent } from "nostr-tools/pure";
8
+ //#region ../browser/src/engine/paymentAdapters/providers/nwc.ts
9
+ /**
10
+ * Lightning through a wallet reached over Nostr Wallet Connect (NIP-47): Alby Hub, a node's NWC service, a
11
+ * hosted wallet. The person pastes the wallet's `nostr+walletconnect://` URI; requests are signed with the
12
+ * key it carries, encrypted to the wallet (NIP-44, or NIP-04 when its info event offers nothing else) and
13
+ * sent through its relay; the answers come back the same way. That wallet holds the sats, applies its own
14
+ * fee limit, and is custodial or not depending on what it is.
15
+ *
16
+ * Money safety (PROVIDERS.md): a request that never reached the relay, or an error code the wallet answers
17
+ * before it tries to pay, is `NothingSpentError`. A `pay_invoice` whose answer does not come back in time,
18
+ * or any other error, is an unknown outcome, and `paymentStatus` asks `lookup_invoice` until it ends.
19
+ */
20
+ var KIND = {
21
+ info: 13194,
22
+ request: 23194,
23
+ response: 23195
24
+ };
25
+ /** A relay message, an encrypted answer, an info event, an invoice: nothing bigger is read. */
26
+ var MAX_MESSAGE = 262144;
27
+ var MAX_CONTENT = 65536;
28
+ var MAX_INFO = 4096;
29
+ var MAX_INVOICE = 7089;
30
+ var CONNECT_MS = 1e4;
31
+ var ACK_MS = 1e4;
32
+ var REQUEST_MS = 2e4;
33
+ /** A payment may take a while to route; an answer after this is an unknown outcome, looked up later. */
34
+ var PAY_MS = 9e4;
35
+ /** The wallet must not start a payment it receives after this (NIP-47 `expiration`). */
36
+ var PAY_EXPIRY_S = 60;
37
+ /** A payment the wallet never heard of is failed once its invoice can no longer be paid, with room for clocks. */
38
+ var EXPIRED_MARGIN_MS = 6e5;
39
+ var INVOICE_EXPIRY_S = 3600;
40
+ /** Error codes a wallet answers before it tries to pay: nothing can have left it. (`BAD_REQUEST`: Alby Hub, an invoice it cannot read.) */
41
+ var REFUSED_BEFORE_PAYING = /* @__PURE__ */ new Set([
42
+ "RATE_LIMITED",
43
+ "NOT_IMPLEMENTED",
44
+ "INSUFFICIENT_BALANCE",
45
+ "QUOTA_EXCEEDED",
46
+ "RESTRICTED",
47
+ "UNAUTHORIZED",
48
+ "UNSUPPORTED_ENCRYPTION",
49
+ "BAD_REQUEST"
50
+ ]);
51
+ var NETWORKS = {
52
+ mainnet: "bitcoin",
53
+ bitcoin: "bitcoin",
54
+ testnet: "testnet",
55
+ testnet3: "testnet",
56
+ testnet4: "testnet",
57
+ signet: "signet",
58
+ mutinynet: "mutinynet",
59
+ regtest: "regtest"
60
+ };
61
+ var HEX64 = /^[0-9a-f]{64}$/;
62
+ var hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
63
+ var bytes = (text) => Uint8Array.from(text.match(/../g), (b) => parseInt(b, 16));
64
+ var now = () => Math.floor(Date.now() / 1e3);
65
+ var isMsat = (value) => Number.isSafeInteger(value) && value >= 0;
66
+ /** The request never reached the relay (not connected, refused by it): the wallet cannot have seen it. */
67
+ var NotDelivered = class extends Error {
68
+ constructor(message) {
69
+ super(message);
70
+ this.name = "NotDelivered";
71
+ }
72
+ };
73
+ /** An error the wallet answered, with its NIP-47 code. */
74
+ var NwcError = class extends Error {
75
+ code;
76
+ constructor(code, message) {
77
+ super(message);
78
+ this.code = code;
79
+ this.name = "NwcError";
80
+ }
81
+ };
82
+ /** No answer in time. The request may or may not have been acted on. */
83
+ var NoAnswer = class extends Error {
84
+ constructor(method) {
85
+ super(`The wallet did not answer ${method} in time`);
86
+ this.name = "NoAnswer";
87
+ }
88
+ };
89
+ /**
90
+ * A minimal NIP-01 client for one relay: it knows exactly whether an event was written to an open socket
91
+ * and whether the relay acknowledged it, which is what tells "nothing sent" from "unknown" apart.
92
+ * It reconnects when asked to send again, and subscriptions follow it.
93
+ */
94
+ var RelayConnection = class {
95
+ url;
96
+ WebSocketImpl;
97
+ socket;
98
+ opening;
99
+ subs = /* @__PURE__ */ new Map();
100
+ acks = /* @__PURE__ */ new Map();
101
+ closed = false;
102
+ constructor(url, WebSocketImpl = globalThis.WebSocket) {
103
+ this.url = url;
104
+ this.WebSocketImpl = WebSocketImpl;
105
+ }
106
+ open() {
107
+ if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("The wallet connection is closed"));
108
+ if (this.socket?.readyState === 1) return Promise.resolve(this.socket);
109
+ if (!this.opening) {
110
+ const opening = this.connectSocket();
111
+ this.opening = opening;
112
+ opening.catch(() => {}).finally(() => {
113
+ if (this.opening === opening) this.opening = void 0;
114
+ });
115
+ }
116
+ return this.opening;
117
+ }
118
+ connectSocket() {
119
+ return new Promise((resolve, reject) => {
120
+ let settled = false;
121
+ const socket = new this.WebSocketImpl(this.url);
122
+ const done = (error) => {
123
+ if (settled) return;
124
+ settled = true;
125
+ clearTimeout(timer);
126
+ if (error) {
127
+ try {
128
+ socket.close();
129
+ } catch {}
130
+ reject(error);
131
+ return;
132
+ }
133
+ if (this.closed) {
134
+ socket.close();
135
+ reject(/* @__PURE__ */ new Error("The wallet connection is closed"));
136
+ return;
137
+ }
138
+ this.socket = socket;
139
+ for (const [id, sub] of this.subs) this.write([
140
+ "REQ",
141
+ id,
142
+ sub.filter
143
+ ]);
144
+ resolve(socket);
145
+ };
146
+ const timer = setTimeout(() => done(/* @__PURE__ */ new Error("The wallet's relay did not answer")), CONNECT_MS);
147
+ socket.onopen = () => done();
148
+ socket.onerror = () => done(/* @__PURE__ */ new Error("Could not reach the wallet's relay"));
149
+ socket.onclose = () => {
150
+ done(/* @__PURE__ */ new Error("The wallet's relay closed the connection"));
151
+ if (this.socket !== socket) return;
152
+ this.socket = void 0;
153
+ for (const [id, ack] of this.acks) {
154
+ this.acks.delete(id);
155
+ ack.fail(/* @__PURE__ */ new Error("The relay connection dropped before it confirmed the request"));
156
+ }
157
+ };
158
+ socket.onmessage = (message) => {
159
+ if (this.socket === socket) this.receive(message.data);
160
+ };
161
+ });
162
+ }
163
+ subscribe(id, filter, sub) {
164
+ this.subs.set(id, {
165
+ ...sub,
166
+ filter
167
+ });
168
+ this.write([
169
+ "REQ",
170
+ id,
171
+ filter
172
+ ]);
173
+ }
174
+ unsubscribe(id) {
175
+ if (this.subs.delete(id)) this.write(["CLOSE", id]);
176
+ }
177
+ /**
178
+ * Events matching `filter` the relay has stored, until it says that is all (or `ms` pass). With
179
+ * `waitForOne`, a relay that has none yet is listened to until one arrives live, within `ms`.
180
+ */
181
+ async fetch(filter, { ms = CONNECT_MS, waitForOne = false } = {}) {
182
+ await this.open();
183
+ const id = `q${hex(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(6)))}`, events = [];
184
+ try {
185
+ await new Promise((resolve) => {
186
+ const timer = setTimeout(resolve, ms);
187
+ let stored = false;
188
+ const finish = () => {
189
+ clearTimeout(timer);
190
+ resolve();
191
+ };
192
+ this.subscribe(id, filter, {
193
+ event: (e) => {
194
+ if (events.length < 20) events.push(e);
195
+ if (stored) finish();
196
+ },
197
+ eose: () => {
198
+ stored = true;
199
+ if (events.length || !waitForOne) finish();
200
+ }
201
+ });
202
+ });
203
+ } finally {
204
+ this.unsubscribe(id);
205
+ }
206
+ return events;
207
+ }
208
+ /**
209
+ * Resolves once the relay accepted the event. `NotDelivered` when it certainly did not reach it (no open
210
+ * socket, refused); any other rejection means it may have.
211
+ */
212
+ async publish(event) {
213
+ let socket;
214
+ try {
215
+ socket = await this.open();
216
+ } catch (error) {
217
+ throw new NotDelivered(error instanceof Error ? error.message : "Could not reach the wallet's relay");
218
+ }
219
+ return new Promise((resolve, reject) => {
220
+ const timer = setTimeout(() => {
221
+ this.acks.delete(event.id);
222
+ reject(/* @__PURE__ */ new Error("The relay did not confirm the request"));
223
+ }, ACK_MS);
224
+ this.acks.set(event.id, {
225
+ ok: () => {
226
+ clearTimeout(timer);
227
+ resolve();
228
+ },
229
+ fail: (error) => {
230
+ clearTimeout(timer);
231
+ reject(error);
232
+ }
233
+ });
234
+ if (socket.readyState !== 1 || socket !== this.socket) {
235
+ this.acks.delete(event.id);
236
+ clearTimeout(timer);
237
+ reject(new NotDelivered("The relay connection closed"));
238
+ return;
239
+ }
240
+ socket.send(JSON.stringify(["EVENT", event]));
241
+ });
242
+ }
243
+ close() {
244
+ this.closed = true;
245
+ for (const [id, ack] of this.acks) {
246
+ this.acks.delete(id);
247
+ ack.fail(/* @__PURE__ */ new Error("The wallet connection is closed"));
248
+ }
249
+ this.subs.clear();
250
+ try {
251
+ this.socket?.close();
252
+ } catch {}
253
+ this.socket = void 0;
254
+ }
255
+ write(message) {
256
+ if (this.socket?.readyState === 1) this.socket.send(JSON.stringify(message));
257
+ }
258
+ receive(data) {
259
+ if (typeof data !== "string" || data.length > MAX_MESSAGE) return;
260
+ let message;
261
+ try {
262
+ message = JSON.parse(data);
263
+ } catch {
264
+ return;
265
+ }
266
+ if (!Array.isArray(message) || typeof message[1] !== "string") return;
267
+ const [type, id] = message;
268
+ if (type === "EVENT") this.subs.get(id)?.event(message[2]);
269
+ else if (type === "EOSE") this.subs.get(id)?.eose?.();
270
+ else if (type === "OK") {
271
+ const ack = this.acks.get(id);
272
+ if (!ack) return;
273
+ this.acks.delete(id);
274
+ const reason = typeof message[3] === "string" ? message[3].slice(0, 120) : "";
275
+ if (message[2] === true || reason.startsWith("duplicate:")) ack.ok();
276
+ else ack.fail(new NotDelivered(`The relay refused the request${reason ? ` (${reason})` : ""}`));
277
+ }
278
+ }
279
+ };
280
+ var NwcLightning = class NwcLightning {
281
+ connection;
282
+ relay;
283
+ encryption;
284
+ options;
285
+ /** From the info event, then narrowed to what this connection may do (`get_info`). */
286
+ capabilities;
287
+ clientPubkey;
288
+ subscription = `nwc${hex(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(6)))}`;
289
+ pending = /* @__PURE__ */ new Map();
290
+ conversationKey;
291
+ network = "bitcoin";
292
+ alias;
293
+ closed = false;
294
+ constructor(connection, relay, encryption, methods, options) {
295
+ this.connection = connection;
296
+ this.relay = relay;
297
+ this.encryption = encryption;
298
+ this.options = options;
299
+ this.clientPubkey = getPublicKey(connection.secret);
300
+ if (encryption === "nip44_v2") this.conversationKey = nip44.v2.utils.getConversationKey(connection.secret, connection.walletPubkey);
301
+ this.capabilities = this.capabilitiesOf(methods);
302
+ }
303
+ /**
304
+ * Reaches the wallet through the first relay that answers, reads its info event (the methods and the
305
+ * encryption it offers), listens for its answers, and asks `get_info` for its network.
306
+ */
307
+ static async connect(uri, options = {}) {
308
+ const connection = parseNwcUri(uri);
309
+ options.signal?.throwIfAborted();
310
+ let relay, lastError;
311
+ for (const url of connection.relays) {
312
+ const candidate = new RelayConnection(url, options.WebSocket);
313
+ try {
314
+ await candidate.open();
315
+ relay = candidate;
316
+ break;
317
+ } catch (error) {
318
+ candidate.close();
319
+ lastError = error;
320
+ }
321
+ }
322
+ if (!relay) throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Could not reach the wallet's relay");
323
+ let wallet;
324
+ try {
325
+ const info = NwcLightning.newestInfo(await relay.fetch({
326
+ kinds: [KIND.info],
327
+ authors: [connection.walletPubkey],
328
+ limit: 5
329
+ }, {
330
+ waitForOne: true,
331
+ ms: options.infoMs
332
+ }), connection.walletPubkey);
333
+ if (!info) throw new Error(`The wallet has not published its NWC info on ${new URL(relay.url).host}: check the URI, or that the wallet is running`);
334
+ const offered = (info.tags.find((t) => t[0] === "encryption")?.slice(1).join(" ") ?? "").split(/\s+/).filter(Boolean);
335
+ const encryption = offered.includes("nip44_v2") ? "nip44_v2" : !offered.length || offered.includes("nip04") ? "nip04" : void 0;
336
+ if (!encryption) throw new Error("The wallet offers no encryption Ghostly speaks (NIP-44 or NIP-04)");
337
+ const methods = new Set(info.content.split(/\s+/).filter(Boolean));
338
+ wallet = new NwcLightning(connection, relay, encryption, methods, options);
339
+ wallet.listen();
340
+ if (options.signal) {
341
+ const opened = wallet;
342
+ if (options.signal.aborted) throw options.signal.reason;
343
+ options.signal.addEventListener("abort", () => void opened.close(), { once: true });
344
+ }
345
+ await wallet.readInfo(methods);
346
+ return wallet;
347
+ } catch (error) {
348
+ if (wallet) await wallet.close();
349
+ else relay.close();
350
+ throw error;
351
+ }
352
+ }
353
+ static newestInfo(events, walletPubkey) {
354
+ return events.filter((e) => e && e.kind === KIND.info && e.pubkey === walletPubkey && typeof e.content === "string" && e.content.length <= MAX_INFO && Array.isArray(e.tags) && verifyEvent(e)).sort((a, b) => b.created_at - a.created_at)[0];
355
+ }
356
+ capabilitiesOf(methods) {
357
+ const lookup = methods.has("lookup_invoice");
358
+ return {
359
+ receive: methods.has("make_invoice"),
360
+ send: methods.has("pay_invoice") && lookup,
361
+ balance: methods.has("get_balance"),
362
+ lookup
363
+ };
364
+ }
365
+ /**
366
+ * `get_info` says the network and the methods this connection may use (the info event lists the
367
+ * service's). Without it the wallet is taken to be on Bitcoin: a test wallet is then refused in the
368
+ * Testnet mode and its invoices in the Mainnet one, while a real-money wallet never passes for a test one.
369
+ */
370
+ async readInfo(offered) {
371
+ if (!offered.has("get_info")) return;
372
+ let info;
373
+ try {
374
+ info = await this.request("get_info", {});
375
+ } catch (error) {
376
+ if (error instanceof NwcError && [
377
+ "RESTRICTED",
378
+ "UNAUTHORIZED",
379
+ "NOT_IMPLEMENTED"
380
+ ].includes(error.code)) return;
381
+ throw error;
382
+ }
383
+ if (info.network !== void 0) {
384
+ const network = typeof info.network === "string" ? NETWORKS[info.network.toLowerCase()] : void 0;
385
+ if (!network) throw new Error("The wallet is on a network Ghostly does not know");
386
+ this.network = network;
387
+ }
388
+ if (typeof info.alias === "string" && info.alias.trim()) this.alias = info.alias.trim().slice(0, 40);
389
+ if (Array.isArray(info.methods) && info.methods.every((m) => typeof m === "string")) this.capabilities = this.capabilitiesOf(new Set(info.methods));
390
+ }
391
+ listen() {
392
+ this.relay.subscribe(this.subscription, {
393
+ kinds: [KIND.response],
394
+ authors: [this.connection.walletPubkey],
395
+ "#p": [this.clientPubkey],
396
+ since: now() - 60
397
+ }, { event: (e) => this.answer(e) });
398
+ }
399
+ encrypt(text) {
400
+ return this.conversationKey ? nip44.v2.encrypt(text, this.conversationKey) : nip04.encrypt(this.connection.secret, this.connection.walletPubkey, text);
401
+ }
402
+ decrypt(content) {
403
+ if (this.conversationKey && !content.includes("?iv=")) return nip44.v2.decrypt(content, this.conversationKey);
404
+ return nip04.decrypt(this.connection.secret, this.connection.walletPubkey, content);
405
+ }
406
+ /**
407
+ * Sends one request and waits for its answer. `NotDelivered` when it never reached the relay; `NwcError`
408
+ * for an error the wallet answered; `NoAnswer` (or a relay error) when the outcome is unknown.
409
+ */
410
+ async request(method, params, { ms = this.options.requestMs ?? REQUEST_MS, expiresIn } = {}) {
411
+ if (this.closed) throw new NotDelivered("The wallet connection is closed");
412
+ const tags = [["p", this.connection.walletPubkey]];
413
+ if (this.conversationKey) tags.push(["encryption", "nip44_v2"]);
414
+ if (expiresIn) tags.push(["expiration", String(now() + expiresIn)]);
415
+ const event = finalizeEvent({
416
+ kind: KIND.request,
417
+ created_at: now(),
418
+ tags,
419
+ content: this.encrypt(JSON.stringify({
420
+ method,
421
+ params
422
+ }))
423
+ }, this.connection.secret);
424
+ const answer = new Promise((resolve, reject) => {
425
+ const timer = setTimeout(() => {
426
+ this.pending.delete(event.id);
427
+ reject(new NoAnswer(method));
428
+ }, ms);
429
+ this.pending.set(event.id, {
430
+ method,
431
+ resolve,
432
+ reject,
433
+ timer
434
+ });
435
+ });
436
+ const delivered = this.relay.publish(event).then(() => void 0, (error) => {
437
+ const waiting = this.pending.get(event.id);
438
+ if (!(error instanceof NotDelivered) || !waiting) return void 0;
439
+ clearTimeout(waiting.timer);
440
+ this.pending.delete(event.id);
441
+ return error;
442
+ });
443
+ return Promise.race([answer, delivered.then((error) => error ? Promise.reject(error) : answer)]);
444
+ }
445
+ /** Reads an answer: from the wallet, signed, about a request of ours, of the method asked. Anything else is dropped. */
446
+ answer(raw) {
447
+ const event = raw;
448
+ if (!event || typeof event !== "object" || event.kind !== KIND.response || event.pubkey !== this.connection.walletPubkey) return;
449
+ if (typeof event.content !== "string" || event.content.length > MAX_CONTENT || !Array.isArray(event.tags)) return;
450
+ const ref = event.tags.find((t) => Array.isArray(t) && t[0] === "e")?.[1];
451
+ const pending = typeof ref === "string" ? this.pending.get(ref) : void 0;
452
+ if (!pending || !event.tags.some((t) => Array.isArray(t) && t[0] === "p" && t[1] === this.clientPubkey)) return;
453
+ if (!verifyEvent(event)) return;
454
+ let body;
455
+ try {
456
+ body = JSON.parse(this.decrypt(event.content));
457
+ } catch {
458
+ return;
459
+ }
460
+ if (!body || typeof body !== "object" || body.result_type !== pending.method) return;
461
+ clearTimeout(pending.timer);
462
+ this.pending.delete(ref);
463
+ if (body.error) {
464
+ const code = typeof body.error.code === "string" ? body.error.code.slice(0, 40) : "OTHER";
465
+ const message = typeof body.error.message === "string" && body.error.message.trim() ? body.error.message.trim().slice(0, 200) : code;
466
+ pending.reject(new NwcError(code, message));
467
+ } else if (body.result && typeof body.result === "object" && !Array.isArray(body.result)) pending.resolve(body.result);
468
+ else pending.reject(new NwcError("OTHER", "The wallet sent an empty answer"));
469
+ }
470
+ async lookup(paymentHash) {
471
+ const tx = await this.request("lookup_invoice", { payment_hash: paymentHash });
472
+ if (tx.payment_hash !== void 0 && String(tx.payment_hash).toLowerCase() !== paymentHash) throw new Error("The wallet answered about another invoice");
473
+ return tx;
474
+ }
475
+ async info() {
476
+ let balance;
477
+ if (this.capabilities.balance) {
478
+ const result = await this.request("get_balance", {});
479
+ if (!isMsat(result.balance)) throw new Error("The wallet returned an invalid balance");
480
+ balance = Math.floor(result.balance / 1e3);
481
+ }
482
+ return {
483
+ network: this.network,
484
+ alias: `${this.alias ?? "NWC wallet"} via ${new URL(this.relay.url).host}`,
485
+ balance
486
+ };
487
+ }
488
+ async createInvoice(amount, memo) {
489
+ if (!Number.isSafeInteger(amount) || amount <= 0 || !Number.isSafeInteger(amount * 1e3)) throw new Error("Invalid amount");
490
+ const result = await this.request("make_invoice", {
491
+ amount: amount * 1e3,
492
+ ...memo ? { description: memo.slice(0, 200) } : {},
493
+ expiry: INVOICE_EXPIRY_S
494
+ });
495
+ const text = typeof result.invoice === "string" && result.invoice.length <= MAX_INVOICE ? result.invoice : "";
496
+ const decoded = decodeBolt11(text);
497
+ if (!decoded?.paymentHash || decoded.amountMsat !== BigInt(amount) * 1000n) throw new Error("The wallet returned an invoice that does not match the request");
498
+ if (result.payment_hash !== void 0 && String(result.payment_hash).toLowerCase() !== decoded.paymentHash) throw new Error("The wallet returned an invoice that does not match its payment hash");
499
+ return {
500
+ invoice: decoded.invoice,
501
+ paymentHash: decoded.paymentHash,
502
+ amount,
503
+ expiresAt: decoded.expiresAt * 1e3
504
+ };
505
+ }
506
+ async invoiceStatus(invoice) {
507
+ let tx;
508
+ try {
509
+ tx = await this.lookup(invoice.paymentHash);
510
+ } catch (error) {
511
+ if (error instanceof NwcError && error.code === "NOT_FOUND" && invoice.expiresAt < Date.now()) return { state: "expired" };
512
+ throw error;
513
+ }
514
+ const expired = invoice.expiresAt < Date.now();
515
+ if (tx.type === "outgoing") return { state: expired ? "expired" : "open" };
516
+ const state = tx.state ?? (tx.settled_at ? "settled" : "pending");
517
+ if (state === "settled") return {
518
+ state: "paid",
519
+ amount: isMsat(tx.amount) ? Math.floor(tx.amount / 1e3) : invoice.amount
520
+ };
521
+ if (state === "expired" || state === "failed") return { state: "expired" };
522
+ return { state: expired ? "expired" : "open" };
523
+ }
524
+ /**
525
+ * Pays through the wallet. NIP-47 carries no fee limit: the wallet applies its own, and the fee it
526
+ * reports is passed on as it is.
527
+ */
528
+ async payInvoice(invoice, _maxFee) {
529
+ const decoded = decodeBolt11(invoice);
530
+ if (!decoded?.paymentHash || decoded.amountSat === null) throw new NothingSpentError("That invoice cannot be paid through NWC");
531
+ if (!this.capabilities.send) throw new NothingSpentError("This wallet connection may not pay and look up invoices");
532
+ let result;
533
+ try {
534
+ result = await this.request("pay_invoice", { invoice: decoded.invoice }, {
535
+ ms: this.options.payMs ?? PAY_MS,
536
+ expiresIn: PAY_EXPIRY_S
537
+ });
538
+ } catch (error) {
539
+ if (error instanceof NotDelivered) throw new NothingSpentError(`The payment did not reach the wallet: ${error.message}`);
540
+ if (error instanceof NwcError && REFUSED_BEFORE_PAYING.has(error.code)) throw new NothingSpentError(`The wallet refused the payment: ${error.message}`);
541
+ if (error instanceof NwcError) return this.confirmFailure(decoded.invoice, decoded.paymentHash, error);
542
+ throw error;
543
+ }
544
+ const preimage = typeof result.preimage === "string" ? result.preimage.toLowerCase() : "";
545
+ if (!HEX64.test(preimage) || hex(sha256(bytes(preimage))) !== decoded.paymentHash) throw new Error("The wallet said it paid, without a proof that matches the invoice");
546
+ return {
547
+ state: "paid",
548
+ preimage,
549
+ fee: isMsat(result.fees_paid) ? Math.ceil(result.fees_paid / 1e3) : void 0
550
+ };
551
+ }
552
+ /**
553
+ * An error after the wallet may have tried (`PAYMENT_FAILED` "may be due to a timeout", `INTERNAL`,
554
+ * `OTHER`…): its own record decides. Only a payment it records as failed is `NothingSpentError`; one it
555
+ * records as paid is paid; anything else is pending, or unknown when it cannot be asked.
556
+ */
557
+ async confirmFailure(invoice, paymentHash, failure) {
558
+ let status;
559
+ try {
560
+ status = await this.paymentStatus({
561
+ invoice,
562
+ paymentHash
563
+ });
564
+ } catch {
565
+ throw new Error(`The wallet said the payment failed (${failure.message}), and cannot confirm it yet`);
566
+ }
567
+ if (status.state === "failed") throw new NothingSpentError(`The payment failed: ${failure.message}`);
568
+ return status.state === "paid" ? {
569
+ state: "paid",
570
+ fee: status.fee,
571
+ preimage: status.preimage
572
+ } : { state: "pending" };
573
+ }
574
+ async paymentStatus(payment) {
575
+ let tx;
576
+ try {
577
+ tx = await this.lookup(payment.paymentHash);
578
+ } catch (error) {
579
+ if (!(error instanceof NwcError && error.code === "NOT_FOUND")) throw error;
580
+ return { state: (decodeBolt11(payment.invoice)?.expiresAt ?? Infinity) * 1e3 + EXPIRED_MARGIN_MS < Date.now() ? "failed" : "pending" };
581
+ }
582
+ if (tx.type === "incoming") return { state: "pending" };
583
+ const preimage = typeof tx.preimage === "string" && HEX64.test(tx.preimage.toLowerCase()) ? tx.preimage.toLowerCase() : void 0;
584
+ const proven = !!preimage && hex(sha256(bytes(preimage))) === payment.paymentHash;
585
+ const state = tx.state ?? (tx.settled_at || proven ? "settled" : "pending");
586
+ const fee = isMsat(tx.fees_paid) ? Math.ceil(tx.fees_paid / 1e3) : void 0;
587
+ if (state === "settled") {
588
+ if (preimage && !proven) throw new Error("The wallet's proof of payment does not match the invoice");
589
+ return {
590
+ state: "paid",
591
+ fee,
592
+ preimage
593
+ };
594
+ }
595
+ if (state === "failed" || state === "expired") return { state: "failed" };
596
+ return { state: "pending" };
597
+ }
598
+ async close() {
599
+ if (this.closed) return;
600
+ this.closed = true;
601
+ for (const [id, pending] of this.pending) {
602
+ clearTimeout(pending.timer);
603
+ this.pending.delete(id);
604
+ pending.reject(/* @__PURE__ */ new Error("The wallet connection is closed"));
605
+ }
606
+ this.relay.close();
607
+ }
608
+ };
609
+ //#endregion
610
+ export { NwcLightning };
611
+
612
+ //# sourceMappingURL=nwc-CgkqyvsE.js.map