@ghostlytools/cli 1.2.4 → 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.
- package/README.md +25 -9
- package/dist/assets/api-Dq2QXQDk.js +4172 -0
- package/dist/assets/api-Dq2QXQDk.js.map +1 -0
- package/dist/assets/api-TiB5QSG9.js +3 -0
- package/dist/assets/apiKit-CA2x5GJF.js +200 -0
- package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
- package/dist/assets/apps-BxV9i_Y4.js +786 -0
- package/dist/assets/apps-BxV9i_Y4.js.map +1 -0
- package/dist/assets/apps-C-T8jeji.js +2 -0
- package/dist/assets/{arkade-CdnKGDbg.js → arkade-C_qDmHnm.js} +2 -2
- package/dist/assets/{arkade-CdnKGDbg.js.map → arkade-C_qDmHnm.js.map} +1 -1
- package/dist/assets/backup-Bugnn3Ue.js +2 -0
- package/dist/assets/backup-CnMN2oUK.js +731 -0
- package/dist/assets/backup-CnMN2oUK.js.map +1 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoind-CZchoh43.js +349 -0
- package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
- package/dist/assets/buttons-CvNBzh2S.js +496 -0
- package/dist/assets/buttons-CvNBzh2S.js.map +1 -0
- package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-BOTFMpfS.js} +56 -18
- package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
- package/dist/assets/envelope-WjingYSc.js +90 -0
- package/dist/assets/envelope-WjingYSc.js.map +1 -0
- package/dist/assets/errors-DO5z6h_l.js +105 -0
- package/dist/assets/errors-DO5z6h_l.js.map +1 -0
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
- package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
- package/dist/assets/fileBytes-CgbibpbV.js +2 -0
- package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
- package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
- package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
- package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
- package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
- package/dist/assets/host-CWt-jtqz.js +4673 -0
- package/dist/assets/host-CWt-jtqz.js.map +1 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-C1p0nkle.js → hyperdhtRelay-CPYwSLLS.js} +25 -10
- package/dist/assets/hyperdhtRelay-CPYwSLLS.js.map +1 -0
- package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
- package/dist/assets/idb-B5L8wuOm.js.map +1 -0
- package/dist/assets/irohWeb-4oChIOIp.js +544 -0
- package/dist/assets/irohWeb-4oChIOIp.js.map +1 -0
- package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
- package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
- package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
- package/dist/assets/main-DbqcdShN.js +4409 -0
- package/dist/assets/main-DbqcdShN.js.map +1 -0
- package/dist/assets/{mainline-BH6s8HVi.js → mainline-ZkiOnevs.js} +11 -6
- package/dist/assets/mainline-ZkiOnevs.js.map +1 -0
- package/dist/assets/mints-BrlY773j.js +74 -0
- package/dist/assets/mints-BrlY773j.js.map +1 -0
- package/dist/assets/mints-CqTdA4Gk.js +2 -0
- package/dist/assets/networkReason-CetKkTPo.js +154 -0
- package/dist/assets/networkReason-CetKkTPo.js.map +1 -0
- package/dist/assets/nwc-CgkqyvsE.js +612 -0
- package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
- package/dist/assets/profiles-BHc9pEYg.js +139 -0
- package/dist/assets/profiles-BHc9pEYg.js.map +1 -0
- package/dist/assets/registry-DlAKO0Fy.js +3793 -0
- package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
- package/dist/assets/registry-OBJBnHVQ.js +2 -0
- package/dist/assets/resolve-CouHhCpJ.js +591 -0
- package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
- package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
- package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
- package/dist/assets/{server-0zfNpABQ.js → server-Ss0gJ-nw.js} +1275 -1404
- package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
- package/dist/assets/{src-CbLkHfz8.js → src-DDzSERyc.js} +882 -134
- package/dist/assets/src-DDzSERyc.js.map +1 -0
- package/dist/assets/turnRefs-B87dtrHM.js +323 -0
- package/dist/assets/turnRefs-B87dtrHM.js.map +1 -0
- package/dist/assets/types-C4_KafL_.js +122 -0
- package/dist/assets/types-C4_KafL_.js.map +1 -0
- package/dist/assets/{usdt-Qf4UuygS.js → usdt-Fj8CQO2z.js} +3 -3
- package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-Fj8CQO2z.js.map} +1 -1
- package/dist/ghostly.mjs +4 -22077
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
- package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
- package/dist/assets/hyperdhtRelay-C1p0nkle.js.map +0 -1
- package/dist/assets/idb-CTF6LF5B.js.map +0 -1
- package/dist/assets/mainline-BH6s8HVi.js.map +0 -1
- package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
- package/dist/assets/server-0zfNpABQ.js.map +0 -1
- package/dist/assets/src-CbLkHfz8.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
|