@openreceive/node 0.3.2 → 0.3.3
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/dist/{chunk-SDCOJGCF.js → chunk-ZYS6QECW.js} +965 -18
- package/dist/cli.d.ts +19 -1
- package/dist/cli.js +273 -9
- package/dist/index.js +35 -974
- package/package.json +3 -2
- package/skills/debug-openreceive-payment/SKILL.md +88 -0
- package/skills/integrate-openreceive/SKILL.md +116 -0
- package/skills/integrate-openreceive/references/node.md +480 -0
- package/skills/integrate-openreceive/references/rails.md +568 -0
|
@@ -1,3 +1,946 @@
|
|
|
1
|
+
// src/nwc/errors.ts
|
|
2
|
+
import {
|
|
3
|
+
isErrorCode,
|
|
4
|
+
isRetryableErrorCode,
|
|
5
|
+
OpenReceiveError
|
|
6
|
+
} from "@openreceive/core";
|
|
7
|
+
var WalletPreflightError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
summary;
|
|
10
|
+
constructor(code, message, summary) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "WalletPreflightError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.summary = summary;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var ReceiveCheckoutValidationError = class extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ReceiveCheckoutValidationError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var NWC_ERROR_CODE_ALIASES = {
|
|
24
|
+
ABORT_ERROR: "TIMEOUT",
|
|
25
|
+
BAD_REQUEST: "INVALID_REQUEST",
|
|
26
|
+
CONNECTION_ERROR: "WALLET_UNAVAILABLE",
|
|
27
|
+
EXPIRED: "INVOICE_EXPIRED",
|
|
28
|
+
FETCH_ERROR: "WALLET_UNAVAILABLE",
|
|
29
|
+
FORBIDDEN: "RESTRICTED",
|
|
30
|
+
INVOICE_NOT_FOUND: "NOT_FOUND",
|
|
31
|
+
INVALID_PARAMETER: "INVALID_REQUEST",
|
|
32
|
+
INVALID_PARAMETERS: "INVALID_REQUEST",
|
|
33
|
+
INVALID_PARAMS: "INVALID_REQUEST",
|
|
34
|
+
METHOD_NOT_FOUND: "UNSUPPORTED_METHOD",
|
|
35
|
+
NETWORK_ERROR: "WALLET_UNAVAILABLE",
|
|
36
|
+
NIP47_NETWORK_ERROR: "WALLET_UNAVAILABLE",
|
|
37
|
+
NOSTR_NETWORK_ERROR: "WALLET_UNAVAILABLE",
|
|
38
|
+
NOT_AUTHORIZED: "UNAUTHORIZED",
|
|
39
|
+
NOT_SUPPORTED: "UNSUPPORTED_METHOD",
|
|
40
|
+
NOTFOUND: "NOT_FOUND",
|
|
41
|
+
PERMISSION_DENIED: "RESTRICTED",
|
|
42
|
+
RELAY_CONNECTION_ERROR: "WALLET_UNAVAILABLE",
|
|
43
|
+
REQUEST_TIMEOUT: "TIMEOUT",
|
|
44
|
+
SERVICE_UNAVAILABLE: "WALLET_UNAVAILABLE",
|
|
45
|
+
TIMED_OUT: "TIMEOUT",
|
|
46
|
+
TIMEOUT_ERROR: "TIMEOUT",
|
|
47
|
+
UNKNOWN_METHOD: "UNSUPPORTED_METHOD",
|
|
48
|
+
UNSUPPORTED: "UNSUPPORTED_METHOD",
|
|
49
|
+
UNSUPPORTED_ENCRYPTION_MODE: "UNSUPPORTED_ENCRYPTION",
|
|
50
|
+
WALLET_OFFLINE: "WALLET_UNAVAILABLE",
|
|
51
|
+
WALLET_UNREACHABLE: "WALLET_UNAVAILABLE"
|
|
52
|
+
};
|
|
53
|
+
var OPENRECEIVE_ERROR_MESSAGES = {
|
|
54
|
+
NOT_IMPLEMENTED: "NWC wallet service does not implement this method.",
|
|
55
|
+
RESTRICTED: "NWC wallet service restricted this request.",
|
|
56
|
+
UNAUTHORIZED: "NWC wallet service rejected authorization.",
|
|
57
|
+
FORBIDDEN: "The host application did not authorize this request.",
|
|
58
|
+
RATE_LIMITED: "NWC wallet service rate limited this request.",
|
|
59
|
+
QUOTA_EXCEEDED: "NWC wallet service quota was exceeded.",
|
|
60
|
+
INTERNAL: "NWC wallet service returned an internal error.",
|
|
61
|
+
UNSUPPORTED_ENCRYPTION: "NWC wallet service does not support the required encryption mode.",
|
|
62
|
+
OTHER: "NWC wallet service returned an unknown error.",
|
|
63
|
+
NOT_FOUND: "NWC wallet service could not find the requested resource.",
|
|
64
|
+
TIMEOUT: "NWC wallet service request timed out.",
|
|
65
|
+
INVALID_REQUEST: "OpenReceive sent an invalid NWC wallet request.",
|
|
66
|
+
WALLET_UNAVAILABLE: "NWC wallet service is unavailable.",
|
|
67
|
+
INVOICE_EXPIRED: "NWC wallet reported that the invoice is expired.",
|
|
68
|
+
UNSUPPORTED_METHOD: "NWC wallet service does not support the requested method.",
|
|
69
|
+
CONFLICT: "NWC wallet service reported a conflicting request."
|
|
70
|
+
};
|
|
71
|
+
function normalizeNwcWalletError(error) {
|
|
72
|
+
if (error instanceof OpenReceiveError) return error;
|
|
73
|
+
const records = collectErrorRecords(error);
|
|
74
|
+
const code = knownErrorCode(error) ?? errorCodeFromRecords(records) ?? (typeof error === "string" ? normalizeNwcErrorCode(error) : void 0) ?? "OTHER";
|
|
75
|
+
const message = errorMessageFromRecords(records, error, code);
|
|
76
|
+
const retryable = firstBooleanField(records, ["retryable"]) ?? isRetryableErrorCode(code);
|
|
77
|
+
const requestId = firstStringField(records, ["request_id", "requestId"]);
|
|
78
|
+
const details = firstRecordField(records, ["details"]);
|
|
79
|
+
const body = {
|
|
80
|
+
code,
|
|
81
|
+
message,
|
|
82
|
+
retryable,
|
|
83
|
+
...requestId === void 0 ? {} : { request_id: requestId },
|
|
84
|
+
...details === void 0 ? {} : { details }
|
|
85
|
+
};
|
|
86
|
+
return new OpenReceiveError(body, { cause: error });
|
|
87
|
+
}
|
|
88
|
+
function knownErrorCode(error) {
|
|
89
|
+
if (error instanceof ReceiveCheckoutValidationError) {
|
|
90
|
+
return "INVALID_REQUEST";
|
|
91
|
+
}
|
|
92
|
+
if (error instanceof WalletPreflightError) {
|
|
93
|
+
if (error.code === "missing_required_method") return "UNSUPPORTED_METHOD";
|
|
94
|
+
if (error.code === "spend_capability_advertised") return "RESTRICTED";
|
|
95
|
+
if (error.code === "unsupported_encryption") return "UNSUPPORTED_ENCRYPTION";
|
|
96
|
+
return "WALLET_UNAVAILABLE";
|
|
97
|
+
}
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
function errorCodeFromRecords(records) {
|
|
101
|
+
for (const record of records) {
|
|
102
|
+
const directCode = normalizeNwcErrorCode(record.code) ?? normalizeNwcErrorCode(record.error_code) ?? normalizeNwcErrorCode(record.errorCode) ?? normalizeNwcErrorCode(record.type);
|
|
103
|
+
if (directCode !== void 0 && directCode !== "OTHER") return directCode;
|
|
104
|
+
const nameCode = normalizeNwcErrorCode(record.name);
|
|
105
|
+
if (nameCode !== void 0 && nameCode !== "OTHER") return nameCode;
|
|
106
|
+
if (directCode !== void 0) return directCode;
|
|
107
|
+
}
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
function normalizeNwcErrorCode(value) {
|
|
111
|
+
if (typeof value !== "string" || value.trim().length === 0) return void 0;
|
|
112
|
+
const normalized = normalizeNwcErrorCodeText(value);
|
|
113
|
+
return NWC_ERROR_CODE_ALIASES[normalized] ?? (isErrorCode(normalized) ? normalized : void 0);
|
|
114
|
+
}
|
|
115
|
+
function normalizeNwcErrorCodeText(value) {
|
|
116
|
+
return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
117
|
+
}
|
|
118
|
+
function errorMessageFromRecords(records, error, code) {
|
|
119
|
+
const message = firstStringField(records, ["message", "description", "reason"]);
|
|
120
|
+
if (message !== void 0 && normalizeNwcErrorCode(message) !== code) {
|
|
121
|
+
return message;
|
|
122
|
+
}
|
|
123
|
+
if (typeof error === "string" && normalizeNwcErrorCode(error) === void 0) {
|
|
124
|
+
const trimmed = error.trim();
|
|
125
|
+
if (trimmed.length > 0) return trimmed;
|
|
126
|
+
}
|
|
127
|
+
return OPENRECEIVE_ERROR_MESSAGES[code];
|
|
128
|
+
}
|
|
129
|
+
function collectErrorRecords(value) {
|
|
130
|
+
const records = [];
|
|
131
|
+
collectErrorRecordsInto(value, records, /* @__PURE__ */ new Set());
|
|
132
|
+
return records;
|
|
133
|
+
}
|
|
134
|
+
function collectErrorRecordsInto(value, records, seen) {
|
|
135
|
+
if (typeof value !== "object" || value === null || seen.has(value)) return;
|
|
136
|
+
seen.add(value);
|
|
137
|
+
const record = value;
|
|
138
|
+
records.push(record);
|
|
139
|
+
for (const key of ["error", "result", "cause", "data"]) {
|
|
140
|
+
collectErrorRecordsInto(record[key], records, seen);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function firstStringField(records, fields) {
|
|
144
|
+
for (const record of records) {
|
|
145
|
+
for (const field of fields) {
|
|
146
|
+
const value = record[field];
|
|
147
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
function firstBooleanField(records, fields) {
|
|
155
|
+
for (const record of records) {
|
|
156
|
+
for (const field of fields) {
|
|
157
|
+
const value = record[field];
|
|
158
|
+
if (typeof value === "boolean") return value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return void 0;
|
|
162
|
+
}
|
|
163
|
+
function firstRecordField(records, fields) {
|
|
164
|
+
for (const record of records) {
|
|
165
|
+
for (const field of fields) {
|
|
166
|
+
const value = record[field];
|
|
167
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/alby-nwc.ts
|
|
176
|
+
import {
|
|
177
|
+
compact,
|
|
178
|
+
formatSpendCapabilityRefusedMessage,
|
|
179
|
+
formatSpendCapabilityWarningMessage,
|
|
180
|
+
OpenReceiveError as OpenReceiveError2,
|
|
181
|
+
parseNwcUri
|
|
182
|
+
} from "@openreceive/core";
|
|
183
|
+
|
|
184
|
+
// src/nwc/normalize.ts
|
|
185
|
+
import {
|
|
186
|
+
OPENRECEIVE_MAX_AMOUNT_MSATS,
|
|
187
|
+
OPENRECEIVE_MIN_AMOUNT_MSATS,
|
|
188
|
+
OPENRECEIVE_NWC_METADATA_MAX_BYTES,
|
|
189
|
+
nonEmptyString,
|
|
190
|
+
recordOrEmpty
|
|
191
|
+
} from "@openreceive/core";
|
|
192
|
+
|
|
193
|
+
// src/hex.ts
|
|
194
|
+
var HEX_64 = /^[0-9a-fA-F]{64}$/;
|
|
195
|
+
|
|
196
|
+
// src/nwc/normalize.ts
|
|
197
|
+
var REQUIRED_RECEIVE_METHODS = ["make_invoice", "list_transactions"];
|
|
198
|
+
var SPEND_METHODS = [
|
|
199
|
+
"pay_invoice",
|
|
200
|
+
"multi_pay_invoice",
|
|
201
|
+
"pay_keysend",
|
|
202
|
+
"multi_pay_keysend"
|
|
203
|
+
];
|
|
204
|
+
function summarizeWalletCapabilities(connection, rawInfo, rawServiceInfo) {
|
|
205
|
+
const unwrappedInfo = unwrapNwcResult(rawInfo);
|
|
206
|
+
const info = recordOrEmpty(unwrappedInfo);
|
|
207
|
+
const serviceInfo = rawServiceInfo === void 0 ? void 0 : recordOrEmpty(unwrapNwcResult(rawServiceInfo));
|
|
208
|
+
const methods = normalizeStringList(
|
|
209
|
+
info.methods ?? info.capabilities ?? info.supported_methods ?? info.supportedMethods ?? (typeof unwrappedInfo === "string" ? unwrappedInfo : void 0)
|
|
210
|
+
).map(normalizeNwcMethodName);
|
|
211
|
+
const encryption = chooseEncryptionMode(
|
|
212
|
+
normalizeStringList(
|
|
213
|
+
serviceInfo?.encryption ?? serviceInfo?.encryptions ?? info.encryption ?? info.encryptions
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
const spendMethods = spendMethodsIn(methods);
|
|
217
|
+
const missingMethods = REQUIRED_RECEIVE_METHODS.filter((method) => !methods.includes(method));
|
|
218
|
+
const warnings = spendMethods.map(
|
|
219
|
+
(method) => `Wallet advertises spend method '${method}'; OpenReceive checkout will not expose it.`
|
|
220
|
+
);
|
|
221
|
+
return {
|
|
222
|
+
walletPubkey: connection.walletPubkey,
|
|
223
|
+
relays: [...connection.relays],
|
|
224
|
+
methods,
|
|
225
|
+
encryption,
|
|
226
|
+
spendCapabilityAdvertised: spendMethods.length > 0,
|
|
227
|
+
receiveCheckoutReady: missingMethods.length === 0,
|
|
228
|
+
warnings
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function spendMethodsIn(methods) {
|
|
232
|
+
return methods.filter(
|
|
233
|
+
(method) => SPEND_METHODS.includes(method)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
function toNip47MakeInvoiceParams(request) {
|
|
237
|
+
const params = {
|
|
238
|
+
amount: toSafeNumber(request.amount_msats, "amount_msats")
|
|
239
|
+
};
|
|
240
|
+
if (request.description !== void 0) params.description = request.description;
|
|
241
|
+
if (request.description_hash !== void 0) {
|
|
242
|
+
params.description_hash = request.description_hash;
|
|
243
|
+
}
|
|
244
|
+
if (request.expiry !== void 0) params.expiry = request.expiry;
|
|
245
|
+
if (request.metadata !== void 0) params.metadata = request.metadata;
|
|
246
|
+
return params;
|
|
247
|
+
}
|
|
248
|
+
function toNip47ListTransactionsParams(request) {
|
|
249
|
+
const params = {};
|
|
250
|
+
if (request.from !== void 0) params.from = request.from;
|
|
251
|
+
if (request.until !== void 0) params.until = request.until;
|
|
252
|
+
if (request.limit !== void 0) params.limit = request.limit;
|
|
253
|
+
if (request.offset !== void 0) params.offset = request.offset;
|
|
254
|
+
if (request.unpaid !== void 0) params.unpaid = request.unpaid;
|
|
255
|
+
if (request.type !== void 0) params.type = request.type;
|
|
256
|
+
return params;
|
|
257
|
+
}
|
|
258
|
+
function validateMakeInvoiceRequest(request) {
|
|
259
|
+
if (request.amount_msats < OPENRECEIVE_MIN_AMOUNT_MSATS) {
|
|
260
|
+
throw new ReceiveCheckoutValidationError("amount_msats must be at least 1000");
|
|
261
|
+
}
|
|
262
|
+
if (request.amount_msats > OPENRECEIVE_MAX_AMOUNT_MSATS) {
|
|
263
|
+
throw new ReceiveCheckoutValidationError("amount_msats exceeds JSON safe integer boundary");
|
|
264
|
+
}
|
|
265
|
+
if (request.description !== void 0 && request.description_hash !== void 0) {
|
|
266
|
+
throw new ReceiveCheckoutValidationError(
|
|
267
|
+
"At most one of description or description_hash may be present"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (request.description_hash !== void 0 && !HEX_64.test(request.description_hash)) {
|
|
271
|
+
throw new ReceiveCheckoutValidationError("description_hash must be 64 hex characters");
|
|
272
|
+
}
|
|
273
|
+
if (request.metadata !== void 0) {
|
|
274
|
+
const metadataBytes = byteLength(JSON.stringify(request.metadata));
|
|
275
|
+
if (metadataBytes > OPENRECEIVE_NWC_METADATA_MAX_BYTES) {
|
|
276
|
+
throw new ReceiveCheckoutValidationError(
|
|
277
|
+
`metadata must serialize below ${OPENRECEIVE_NWC_METADATA_MAX_BYTES} bytes`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function validateListTransactionsRequest(request) {
|
|
283
|
+
validateOptionalNonNegativeInteger(request.from, "from");
|
|
284
|
+
validateOptionalNonNegativeInteger(request.until, "until");
|
|
285
|
+
validateOptionalNonNegativeInteger(request.offset, "offset");
|
|
286
|
+
if (request.limit !== void 0 && (!Number.isSafeInteger(request.limit) || request.limit <= 0)) {
|
|
287
|
+
throw new ReceiveCheckoutValidationError("limit must be a positive safe integer");
|
|
288
|
+
}
|
|
289
|
+
if (request.from !== void 0 && request.until !== void 0 && request.from > request.until) {
|
|
290
|
+
throw new ReceiveCheckoutValidationError("from must be less than or equal to until");
|
|
291
|
+
}
|
|
292
|
+
if (request.type !== void 0 && request.type !== "incoming" && request.type !== "outgoing") {
|
|
293
|
+
throw new ReceiveCheckoutValidationError("type must be incoming or outgoing");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function validateOptionalNonNegativeInteger(value, field) {
|
|
297
|
+
if (value === void 0) return;
|
|
298
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
299
|
+
throw new ReceiveCheckoutValidationError(`${field} must be a non-negative safe integer`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function normalizeMakeInvoiceResult(rawResult) {
|
|
303
|
+
const result = recordOrEmpty(unwrapNwcResult(rawResult));
|
|
304
|
+
const invoice = requiredString(result.invoice, "invoice");
|
|
305
|
+
const paymentHash = requiredString(result.payment_hash ?? result.paymentHash, "payment_hash");
|
|
306
|
+
if (!HEX_64.test(paymentHash)) {
|
|
307
|
+
throw new TypeError("payment_hash must be 64 hex characters");
|
|
308
|
+
}
|
|
309
|
+
const amountMsats = toBigInt(result.amount_msats ?? result.amount, "amount_msats");
|
|
310
|
+
const createdAt = parseOptionalInteger(result.created_at ?? result.createdAt, "created_at");
|
|
311
|
+
const expiresAt = parseOptionalInteger(result.expires_at ?? result.expiresAt, "expires_at");
|
|
312
|
+
return {
|
|
313
|
+
invoice,
|
|
314
|
+
payment_hash: paymentHash,
|
|
315
|
+
amount_msats: amountMsats,
|
|
316
|
+
...createdAt === void 0 ? {} : { created_at: createdAt },
|
|
317
|
+
...expiresAt === void 0 ? {} : { expires_at: expiresAt }
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function normalizeListTransactionsResult(rawResult) {
|
|
321
|
+
const unwrapped = unwrapNwcResult(rawResult);
|
|
322
|
+
const result = recordOrEmpty(unwrapped);
|
|
323
|
+
let rawTransactions;
|
|
324
|
+
if (Array.isArray(result.transactions)) {
|
|
325
|
+
rawTransactions = result.transactions;
|
|
326
|
+
} else if (Array.isArray(unwrapped)) {
|
|
327
|
+
rawTransactions = unwrapped;
|
|
328
|
+
} else if ((unwrapped === void 0 || unwrapped === null || typeof unwrapped === "object") && Object.keys(result).length === 0) {
|
|
329
|
+
rawTransactions = [];
|
|
330
|
+
} else {
|
|
331
|
+
throw new TypeError("list_transactions returned an unrecognized result shape");
|
|
332
|
+
}
|
|
333
|
+
const transactions = [];
|
|
334
|
+
let skippedRows = 0;
|
|
335
|
+
for (const rawTransaction of rawTransactions) {
|
|
336
|
+
try {
|
|
337
|
+
transactions.push(normalizeNwcTransaction(rawTransaction));
|
|
338
|
+
} catch {
|
|
339
|
+
skippedRows += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (transactions.length === 0 && skippedRows > 0) {
|
|
343
|
+
throw new TypeError("list_transactions returned no usable rows");
|
|
344
|
+
}
|
|
345
|
+
return { transactions, skippedRows };
|
|
346
|
+
}
|
|
347
|
+
function normalizeNwcTransaction(rawTransaction) {
|
|
348
|
+
const result = recordOrEmpty(rawTransaction);
|
|
349
|
+
const normalized = {};
|
|
350
|
+
const type = normalizeTransactionType(result.type);
|
|
351
|
+
if (type !== void 0) normalized.type = type;
|
|
352
|
+
const invoice = nonEmptyString(result.invoice);
|
|
353
|
+
if (invoice !== void 0) normalized.invoice = invoice;
|
|
354
|
+
const paymentHash = nonEmptyString(result.payment_hash ?? result.paymentHash);
|
|
355
|
+
if (paymentHash !== void 0) {
|
|
356
|
+
if (!/^[0-9a-fA-F]{64}$/.test(paymentHash)) {
|
|
357
|
+
throw new TypeError("payment_hash must be 64 hexadecimal characters");
|
|
358
|
+
}
|
|
359
|
+
normalized.payment_hash = paymentHash;
|
|
360
|
+
}
|
|
361
|
+
if (result.amount_msats !== void 0 || result.amount !== void 0) {
|
|
362
|
+
normalized.amount_msats = toBigInt(result.amount_msats ?? result.amount, "amount_msats");
|
|
363
|
+
}
|
|
364
|
+
const transactionState = normalizeTransactionState(
|
|
365
|
+
result.transaction_state ?? result.transactionState ?? result.state
|
|
366
|
+
) ?? (result.settled === true || result.paid === true ? "settled" : void 0);
|
|
367
|
+
if (transactionState !== void 0) {
|
|
368
|
+
normalized.transaction_state = transactionState;
|
|
369
|
+
}
|
|
370
|
+
const createdAt = optionalUnixSeconds(result.created_at ?? result.createdAt, "created_at");
|
|
371
|
+
if (createdAt !== void 0) normalized.created_at = createdAt;
|
|
372
|
+
const expiresAt = optionalUnixSeconds(result.expires_at ?? result.expiresAt, "expires_at");
|
|
373
|
+
if (expiresAt !== void 0) normalized.expires_at = expiresAt;
|
|
374
|
+
const settledAt = optionalUnixSeconds(result.settled_at ?? result.settledAt, "settled_at");
|
|
375
|
+
if (settledAt !== void 0) normalized.settled_at = settledAt;
|
|
376
|
+
const preimage = nonEmptyString(result.preimage);
|
|
377
|
+
if (preimage !== void 0) normalized.preimage = preimage;
|
|
378
|
+
const description = nonEmptyString(result.description);
|
|
379
|
+
if (description !== void 0) normalized.description = description;
|
|
380
|
+
const descriptionHash = nonEmptyString(result.description_hash ?? result.descriptionHash);
|
|
381
|
+
if (descriptionHash !== void 0) normalized.description_hash = descriptionHash;
|
|
382
|
+
if (result.fees_paid !== void 0 || result.feesPaid !== void 0) {
|
|
383
|
+
normalized.fees_paid_msats = toBigInt(result.fees_paid ?? result.feesPaid, "fees_paid");
|
|
384
|
+
}
|
|
385
|
+
return normalized;
|
|
386
|
+
}
|
|
387
|
+
function normalizeNwcNotification(rawNotification) {
|
|
388
|
+
const record = recordOrEmpty(rawNotification);
|
|
389
|
+
const type = typeof record.notification_type === "string" ? record.notification_type : typeof record.notificationType === "string" ? record.notificationType : typeof record.type === "string" ? record.type : "unknown";
|
|
390
|
+
const payload = recordOrEmpty(record.notification);
|
|
391
|
+
let transaction;
|
|
392
|
+
if (Object.keys(payload).length > 0) {
|
|
393
|
+
try {
|
|
394
|
+
transaction = normalizeNwcTransaction(payload);
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const rawHash = payload.payment_hash ?? payload.paymentHash ?? record.payment_hash ?? record.paymentHash;
|
|
399
|
+
const paymentHash = typeof rawHash === "string" && rawHash.length > 0 ? rawHash : transaction?.payment_hash;
|
|
400
|
+
return {
|
|
401
|
+
type,
|
|
402
|
+
...paymentHash === void 0 ? {} : { payment_hash: paymentHash },
|
|
403
|
+
...transaction === void 0 ? {} : { transaction }
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function chooseEncryptionMode(encryptionModes) {
|
|
407
|
+
const normalized = encryptionModes.map((mode) => mode.toLowerCase().replace(/[- ]/g, "_"));
|
|
408
|
+
if (normalized.includes("nip44_v2") || normalized.includes("nip44") || normalized.includes("nip_44")) {
|
|
409
|
+
return "nip44_v2";
|
|
410
|
+
}
|
|
411
|
+
if (normalized.length === 0 || normalized.includes("nip04") || normalized.includes("nip_04")) {
|
|
412
|
+
return "nip04";
|
|
413
|
+
}
|
|
414
|
+
return void 0;
|
|
415
|
+
}
|
|
416
|
+
function normalizeStringList(value) {
|
|
417
|
+
if (Array.isArray(value)) {
|
|
418
|
+
return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
419
|
+
}
|
|
420
|
+
if (typeof value === "string") {
|
|
421
|
+
return value.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
|
|
422
|
+
}
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
function normalizeNwcMethodName(value) {
|
|
426
|
+
return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
427
|
+
}
|
|
428
|
+
function normalizeTransactionState(value) {
|
|
429
|
+
if (typeof value !== "string") return void 0;
|
|
430
|
+
const normalized = value.toLowerCase();
|
|
431
|
+
if (normalized === "pending" || normalized === "settled" || normalized === "expired" || normalized === "failed" || normalized === "accepted") {
|
|
432
|
+
return normalized;
|
|
433
|
+
}
|
|
434
|
+
return void 0;
|
|
435
|
+
}
|
|
436
|
+
function normalizeTransactionType(value) {
|
|
437
|
+
if (typeof value !== "string") return void 0;
|
|
438
|
+
const normalized = value.toLowerCase();
|
|
439
|
+
if (normalized === "incoming" || normalized === "outgoing") return normalized;
|
|
440
|
+
return void 0;
|
|
441
|
+
}
|
|
442
|
+
function parseOptionalInteger(value, fieldName) {
|
|
443
|
+
if (value === void 0 || value === null) return void 0;
|
|
444
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
445
|
+
throw new TypeError(`${fieldName} must be a non-negative safe integer`);
|
|
446
|
+
}
|
|
447
|
+
return value;
|
|
448
|
+
}
|
|
449
|
+
function optionalUnixSeconds(value, fieldName) {
|
|
450
|
+
if (value === void 0 || value === null) return void 0;
|
|
451
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
452
|
+
throw new TypeError(`${fieldName} must be a non-negative number of Unix seconds`);
|
|
453
|
+
}
|
|
454
|
+
return Math.floor(value);
|
|
455
|
+
}
|
|
456
|
+
function requiredString(value, fieldName) {
|
|
457
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
458
|
+
throw new TypeError(`${fieldName} must be a non-empty string`);
|
|
459
|
+
}
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
function toBigInt(value, fieldName) {
|
|
463
|
+
if (typeof value === "bigint") return value;
|
|
464
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
465
|
+
return BigInt(value);
|
|
466
|
+
}
|
|
467
|
+
if (typeof value === "string" && /^[0-9]+$/.test(value)) {
|
|
468
|
+
return BigInt(value);
|
|
469
|
+
}
|
|
470
|
+
throw new TypeError(`${fieldName} must be an integer`);
|
|
471
|
+
}
|
|
472
|
+
function toSafeNumber(value, fieldName) {
|
|
473
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
474
|
+
throw new ReceiveCheckoutValidationError(`${fieldName} exceeds JSON safe integer boundary`);
|
|
475
|
+
}
|
|
476
|
+
return Number(value);
|
|
477
|
+
}
|
|
478
|
+
function byteLength(value) {
|
|
479
|
+
return new TextEncoder().encode(value).length;
|
|
480
|
+
}
|
|
481
|
+
function unwrapNwcResult(value) {
|
|
482
|
+
const record = recordOrEmpty(value);
|
|
483
|
+
return record.result ?? value;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/nwc/transport.ts
|
|
487
|
+
import { createRequire } from "module";
|
|
488
|
+
import { pathToFileURL } from "url";
|
|
489
|
+
import { recordOrEmpty as recordOrEmpty2 } from "@openreceive/core";
|
|
490
|
+
var require2 = createRequire(import.meta.url);
|
|
491
|
+
async function callRequiredMethod(client, names, request) {
|
|
492
|
+
for (const name of names) {
|
|
493
|
+
const method = client[name];
|
|
494
|
+
if (typeof method === "function") {
|
|
495
|
+
return await method.call(
|
|
496
|
+
client,
|
|
497
|
+
request
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
throw new WalletPreflightError(
|
|
502
|
+
"wallet_unavailable",
|
|
503
|
+
`NWC client does not expose ${names.join(" or ")}.`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
async function createDefaultAlbyNwcClient(connectionString) {
|
|
507
|
+
ensureNodeWebSocket();
|
|
508
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
509
|
+
const namespace = recordOrEmpty2(
|
|
510
|
+
await dynamicImport(pathToFileURL(require2.resolve("@getalby/sdk/nwc")).href)
|
|
511
|
+
);
|
|
512
|
+
const Constructor = namespace.NWCClient;
|
|
513
|
+
if (typeof Constructor !== "function") {
|
|
514
|
+
throw new WalletPreflightError(
|
|
515
|
+
"wallet_unavailable",
|
|
516
|
+
"@getalby/sdk/nwc did not expose NWCClient."
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
const NWCClientConstructor = Constructor;
|
|
520
|
+
return new NWCClientConstructor({
|
|
521
|
+
nostrWalletConnectUrl: connectionString
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
function ensureNodeWebSocket() {
|
|
525
|
+
if (globalThis.WebSocket !== void 0) return;
|
|
526
|
+
throw new WalletPreflightError(
|
|
527
|
+
"wallet_unavailable",
|
|
528
|
+
"OpenReceive requires Node 22 or newer with the built-in WebSocket API."
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
async function closeNwcNotificationSubscription(subscription) {
|
|
532
|
+
if (typeof subscription !== "function") {
|
|
533
|
+
throw new TypeError("NWC subscribeNotifications must resolve to an unsubscribe function.");
|
|
534
|
+
}
|
|
535
|
+
await subscription();
|
|
536
|
+
}
|
|
537
|
+
function delay(milliseconds) {
|
|
538
|
+
return new Promise((resolve) => {
|
|
539
|
+
setTimeout(resolve, milliseconds);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/alby-nwc.ts
|
|
544
|
+
var AlbyNwcReceiveClient = class {
|
|
545
|
+
/**
|
|
546
|
+
* Safe, loggable view of the connection (wallet pubkey, relays, lud16, and
|
|
547
|
+
* the secret-redacted URI). The parsed connection with the client secret is
|
|
548
|
+
* deliberately private: `console.log(client)` or host error serialization
|
|
549
|
+
* must never leak the secret.
|
|
550
|
+
*/
|
|
551
|
+
connection;
|
|
552
|
+
#connection;
|
|
553
|
+
#connectionString;
|
|
554
|
+
#client;
|
|
555
|
+
#clientPromise;
|
|
556
|
+
#clientFactory;
|
|
557
|
+
#preflightSummary;
|
|
558
|
+
#preflightPromise;
|
|
559
|
+
#requirePreflight;
|
|
560
|
+
#logger;
|
|
561
|
+
#allowSpendCapableWallet;
|
|
562
|
+
#spendCapabilityWarningDelayMs;
|
|
563
|
+
#spendCapabilityWarning;
|
|
564
|
+
#closed = false;
|
|
565
|
+
constructor(options) {
|
|
566
|
+
this.#connectionString = options.connectionString;
|
|
567
|
+
this.#connection = parseNwcUri(options.connectionString);
|
|
568
|
+
this.connection = {
|
|
569
|
+
walletPubkey: this.#connection.walletPubkey,
|
|
570
|
+
relays: [...this.#connection.relays],
|
|
571
|
+
...this.#connection.lud16 === void 0 ? {} : { lud16: this.#connection.lud16 },
|
|
572
|
+
redacted: this.#connection.redacted
|
|
573
|
+
};
|
|
574
|
+
this.#client = options.client;
|
|
575
|
+
this.#clientFactory = options.clientFactory;
|
|
576
|
+
this.#requirePreflight = options.requirePreflight ?? true;
|
|
577
|
+
this.#logger = options.logger;
|
|
578
|
+
this.#allowSpendCapableWallet = options.allowSpendCapableWallet ?? false;
|
|
579
|
+
this.#spendCapabilityWarningDelayMs = options.spendCapabilityWarningDelayMs ?? 0;
|
|
580
|
+
this.#spendCapabilityWarning = options.spendCapabilityWarning ?? ((message) => {
|
|
581
|
+
if (this.#logger === void 0) console.error(message);
|
|
582
|
+
else this.#log("error", "nwc.spend_capability_warning", message);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
#log(level, event, message, fields = {}) {
|
|
586
|
+
if (this.#logger === void 0) return;
|
|
587
|
+
try {
|
|
588
|
+
this.#logger({
|
|
589
|
+
level,
|
|
590
|
+
event,
|
|
591
|
+
message,
|
|
592
|
+
wallet_pubkey: this.connection.walletPubkey,
|
|
593
|
+
...fields
|
|
594
|
+
});
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async preflight() {
|
|
599
|
+
const client = await this.getClient();
|
|
600
|
+
const hasGetInfo = typeof client.getInfo === "function" || typeof client.get_info === "function";
|
|
601
|
+
const hasInfoEvent = typeof client.getWalletServiceInfo === "function";
|
|
602
|
+
const rawInfo = hasGetInfo || !hasInfoEvent ? await this.#fetchWalletInfo(
|
|
603
|
+
"get_info",
|
|
604
|
+
() => callRequiredMethod(client, ["getInfo", "get_info"], {})
|
|
605
|
+
) : void 0;
|
|
606
|
+
const rawServiceInfo = hasInfoEvent ? await this.#fetchWalletInfo(
|
|
607
|
+
"info_event",
|
|
608
|
+
() => client.getWalletServiceInfo()
|
|
609
|
+
) : void 0;
|
|
610
|
+
if (rawInfo === void 0) {
|
|
611
|
+
this.#log(
|
|
612
|
+
"warn",
|
|
613
|
+
"nwc.info_event.methods_fallback",
|
|
614
|
+
"NWC client exposes no get_info; proving receive-only from the service-wide info event (kind 13194) rather than this connection's own method list."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
const summary = summarizeWalletCapabilities(
|
|
618
|
+
this.#connection,
|
|
619
|
+
rawInfo ?? rawServiceInfo,
|
|
620
|
+
rawServiceInfo
|
|
621
|
+
);
|
|
622
|
+
this.#log("debug", "nwc.preflight.summarized", "NWC wallet capabilities summarized.", {
|
|
623
|
+
methods_source: rawInfo === void 0 ? "info_event" : "get_info",
|
|
624
|
+
methods: summary.methods,
|
|
625
|
+
encryption: summary.encryption,
|
|
626
|
+
receive_checkout_ready: summary.receiveCheckoutReady,
|
|
627
|
+
spend_capability_advertised: summary.spendCapabilityAdvertised
|
|
628
|
+
});
|
|
629
|
+
if (!summary.receiveCheckoutReady) {
|
|
630
|
+
throw new WalletPreflightError(
|
|
631
|
+
"missing_required_method",
|
|
632
|
+
"NWC wallet must advertise make_invoice and list_transactions for receive checkout.",
|
|
633
|
+
summary
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
if (summary.encryption !== "nip04" && summary.encryption !== "nip44_v2") {
|
|
637
|
+
throw new WalletPreflightError(
|
|
638
|
+
"unsupported_encryption",
|
|
639
|
+
"NWC wallet must support NIP-04 or NIP-44 v2 encryption.",
|
|
640
|
+
summary
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (summary.spendCapabilityAdvertised) {
|
|
644
|
+
const spendMethods = spendMethodsIn(summary.methods);
|
|
645
|
+
if (!this.#allowSpendCapableWallet) {
|
|
646
|
+
this.#log(
|
|
647
|
+
"error",
|
|
648
|
+
"nwc.spend_capability_advertised",
|
|
649
|
+
"NWC connection advertises send-payment methods; refusing to boot without an explicit override.",
|
|
650
|
+
{ methods: spendMethods }
|
|
651
|
+
);
|
|
652
|
+
throw new WalletPreflightError(
|
|
653
|
+
"spend_capability_advertised",
|
|
654
|
+
formatSpendCapabilityRefusedMessage({ spendMethods }),
|
|
655
|
+
summary
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const warning = formatSpendCapabilityWarningMessage({
|
|
659
|
+
spendMethods
|
|
660
|
+
});
|
|
661
|
+
this.#log(
|
|
662
|
+
"error",
|
|
663
|
+
"nwc.spend_capability_advertised",
|
|
664
|
+
"NWC connection advertises send-payment methods; continuing because the spend-capable override is set.",
|
|
665
|
+
{ methods: spendMethods }
|
|
666
|
+
);
|
|
667
|
+
try {
|
|
668
|
+
this.#spendCapabilityWarning(warning);
|
|
669
|
+
} catch {
|
|
670
|
+
}
|
|
671
|
+
if (this.#spendCapabilityWarningDelayMs > 0) {
|
|
672
|
+
await delay(this.#spendCapabilityWarningDelayMs);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
this.#preflightSummary = summary;
|
|
676
|
+
return summary;
|
|
677
|
+
}
|
|
678
|
+
async makeInvoice(request) {
|
|
679
|
+
await this.ensurePreflight();
|
|
680
|
+
validateMakeInvoiceRequest(request);
|
|
681
|
+
this.#log(
|
|
682
|
+
"debug",
|
|
683
|
+
"nwc.make_invoice.requested",
|
|
684
|
+
"Calling NWC wallet make_invoice.",
|
|
685
|
+
compact({
|
|
686
|
+
method: "make_invoice",
|
|
687
|
+
// String form: msats above 2^53 would silently round through Number().
|
|
688
|
+
amount_msats: request.amount_msats.toString(),
|
|
689
|
+
expiry: request.expiry,
|
|
690
|
+
has_description: request.description !== void 0,
|
|
691
|
+
has_description_hash: request.description_hash !== void 0
|
|
692
|
+
})
|
|
693
|
+
);
|
|
694
|
+
const startedAt = Date.now();
|
|
695
|
+
let rawResult;
|
|
696
|
+
try {
|
|
697
|
+
rawResult = await callRequiredMethod(
|
|
698
|
+
await this.getClient(),
|
|
699
|
+
["makeInvoice", "make_invoice"],
|
|
700
|
+
toNip47MakeInvoiceParams(request)
|
|
701
|
+
);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
const normalized = normalizeNwcWalletError(error);
|
|
704
|
+
this.#log("error", "nwc.make_invoice.failed", "NWC wallet make_invoice failed.", {
|
|
705
|
+
method: "make_invoice",
|
|
706
|
+
duration_ms: Date.now() - startedAt,
|
|
707
|
+
error_code: normalized.code,
|
|
708
|
+
error_message: normalized.message
|
|
709
|
+
});
|
|
710
|
+
throw normalized;
|
|
711
|
+
}
|
|
712
|
+
let result;
|
|
713
|
+
try {
|
|
714
|
+
result = normalizeMakeInvoiceResult(rawResult);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
const normalized = normalizeNwcWalletError(error);
|
|
717
|
+
this.#log("error", "nwc.make_invoice.failed", "NWC wallet make_invoice failed.", {
|
|
718
|
+
method: "make_invoice",
|
|
719
|
+
duration_ms: Date.now() - startedAt,
|
|
720
|
+
error_code: normalized.code,
|
|
721
|
+
error_message: normalized.message
|
|
722
|
+
});
|
|
723
|
+
throw normalized;
|
|
724
|
+
}
|
|
725
|
+
this.#log("debug", "nwc.make_invoice.completed", "NWC wallet make_invoice completed.", {
|
|
726
|
+
method: "make_invoice",
|
|
727
|
+
duration_ms: Date.now() - startedAt,
|
|
728
|
+
payment_hash: result.payment_hash,
|
|
729
|
+
amount_msats: result.amount_msats.toString()
|
|
730
|
+
});
|
|
731
|
+
return result;
|
|
732
|
+
}
|
|
733
|
+
async listTransactions(request) {
|
|
734
|
+
await this.ensurePreflight();
|
|
735
|
+
validateListTransactionsRequest(request);
|
|
736
|
+
this.#log(
|
|
737
|
+
"debug",
|
|
738
|
+
"nwc.list_transactions.requested",
|
|
739
|
+
"Calling NWC wallet list_transactions.",
|
|
740
|
+
compact({
|
|
741
|
+
method: "list_transactions",
|
|
742
|
+
from: request.from,
|
|
743
|
+
until: request.until,
|
|
744
|
+
limit: request.limit,
|
|
745
|
+
offset: request.offset,
|
|
746
|
+
unpaid: request.unpaid,
|
|
747
|
+
type: request.type
|
|
748
|
+
})
|
|
749
|
+
);
|
|
750
|
+
const startedAt = Date.now();
|
|
751
|
+
let rawResult;
|
|
752
|
+
try {
|
|
753
|
+
rawResult = await callRequiredMethod(
|
|
754
|
+
await this.getClient(),
|
|
755
|
+
["listTransactions", "list_transactions"],
|
|
756
|
+
toNip47ListTransactionsParams(request)
|
|
757
|
+
);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
const normalized = normalizeNwcWalletError(error);
|
|
760
|
+
this.#log("error", "nwc.list_transactions.failed", "NWC wallet list_transactions failed.", {
|
|
761
|
+
method: "list_transactions",
|
|
762
|
+
duration_ms: Date.now() - startedAt,
|
|
763
|
+
error_code: normalized.code,
|
|
764
|
+
error_message: normalized.message
|
|
765
|
+
});
|
|
766
|
+
throw normalized;
|
|
767
|
+
}
|
|
768
|
+
let result;
|
|
769
|
+
try {
|
|
770
|
+
result = normalizeListTransactionsResult(rawResult);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
const normalized = normalizeNwcWalletError(error);
|
|
773
|
+
this.#log("error", "nwc.list_transactions.failed", "NWC wallet list_transactions failed.", {
|
|
774
|
+
method: "list_transactions",
|
|
775
|
+
duration_ms: Date.now() - startedAt,
|
|
776
|
+
error_code: normalized.code,
|
|
777
|
+
error_message: normalized.message
|
|
778
|
+
});
|
|
779
|
+
throw normalized;
|
|
780
|
+
}
|
|
781
|
+
if (result.skippedRows > 0) {
|
|
782
|
+
this.#log(
|
|
783
|
+
"warn",
|
|
784
|
+
"nwc.list_transactions.rows_skipped",
|
|
785
|
+
"Skipped wallet transaction rows that could not be normalized.",
|
|
786
|
+
{
|
|
787
|
+
method: "list_transactions",
|
|
788
|
+
skipped_count: result.skippedRows
|
|
789
|
+
}
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
this.#log(
|
|
793
|
+
"debug",
|
|
794
|
+
"nwc.list_transactions.completed",
|
|
795
|
+
"NWC wallet list_transactions completed.",
|
|
796
|
+
{
|
|
797
|
+
method: "list_transactions",
|
|
798
|
+
duration_ms: Date.now() - startedAt,
|
|
799
|
+
transaction_count: result.transactions.length
|
|
800
|
+
}
|
|
801
|
+
);
|
|
802
|
+
return {
|
|
803
|
+
transactions: result.transactions,
|
|
804
|
+
...result.skippedRows === 0 ? {} : { skippedRows: result.skippedRows }
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Opt-in NWC-02 notification subscription, limited to `payment_received`.
|
|
809
|
+
* Notifications are authenticated wallet data: the handler receives the
|
|
810
|
+
* payload normalized like a `list_transactions` row so a payload satisfying
|
|
811
|
+
* the settlement rule can settle its pending attempt directly, while
|
|
812
|
+
* anything less only wakes reconciliation. Direct settlement assumes the
|
|
813
|
+
* client binds notification decryption to the connection's wallet pubkey
|
|
814
|
+
* (the bundled @getalby/sdk does). Resolves to an unsubscribe function.
|
|
815
|
+
* Throws an OpenReceiveError with code `UNSUPPORTED_METHOD` when the
|
|
816
|
+
* underlying wallet client does not support NWC notifications.
|
|
817
|
+
*/
|
|
818
|
+
async subscribeNotifications(handler) {
|
|
819
|
+
const client = await this.getClient();
|
|
820
|
+
const subscribe = client.subscribeNotifications;
|
|
821
|
+
if (typeof subscribe !== "function") {
|
|
822
|
+
throw new OpenReceiveError2({
|
|
823
|
+
code: "UNSUPPORTED_METHOD",
|
|
824
|
+
message: "NWC wallet client does not support NWC notifications (subscribeNotifications). Keep polling reconciliation.",
|
|
825
|
+
retryable: false
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
this.#log(
|
|
829
|
+
"debug",
|
|
830
|
+
"nwc.notifications.subscribe.requested",
|
|
831
|
+
"Subscribing to NWC-02 payment_received notifications.",
|
|
832
|
+
{ notification_types: ["payment_received"] }
|
|
833
|
+
);
|
|
834
|
+
let subscription;
|
|
835
|
+
try {
|
|
836
|
+
subscription = await subscribe.call(
|
|
837
|
+
client,
|
|
838
|
+
(rawNotification) => {
|
|
839
|
+
const notification = normalizeNwcNotification(rawNotification);
|
|
840
|
+
if (notification.type !== "payment_received") return;
|
|
841
|
+
this.#log(
|
|
842
|
+
"debug",
|
|
843
|
+
"nwc.notifications.received",
|
|
844
|
+
"NWC payment_received notification received.",
|
|
845
|
+
compact({
|
|
846
|
+
notification_type: notification.type,
|
|
847
|
+
payment_hash: notification.payment_hash
|
|
848
|
+
})
|
|
849
|
+
);
|
|
850
|
+
try {
|
|
851
|
+
handler(notification);
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
["payment_received"]
|
|
856
|
+
);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
const normalized = normalizeNwcWalletError(error);
|
|
859
|
+
this.#log(
|
|
860
|
+
"error",
|
|
861
|
+
"nwc.notifications.subscribe.failed",
|
|
862
|
+
"NWC notification subscription failed.",
|
|
863
|
+
{ error_code: normalized.code, error_message: normalized.message }
|
|
864
|
+
);
|
|
865
|
+
throw normalized;
|
|
866
|
+
}
|
|
867
|
+
this.#log(
|
|
868
|
+
"info",
|
|
869
|
+
"nwc.notifications.subscribe.completed",
|
|
870
|
+
"Subscribed to NWC-02 payment_received notifications; polling reconciliation remains the safety net."
|
|
871
|
+
);
|
|
872
|
+
return async () => {
|
|
873
|
+
await closeNwcNotificationSubscription(subscription);
|
|
874
|
+
this.#log(
|
|
875
|
+
"info",
|
|
876
|
+
"nwc.notifications.unsubscribed",
|
|
877
|
+
"Unsubscribed from NWC-02 wallet notifications."
|
|
878
|
+
);
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
async #fetchWalletInfo(source, call) {
|
|
882
|
+
const method = source === "get_info" ? "get_info" : "getWalletServiceInfo";
|
|
883
|
+
const label = source === "get_info" ? "get_info" : "info event (kind 13194)";
|
|
884
|
+
this.#log("debug", `nwc.${source}.requested`, `Fetching NWC wallet ${label}.`, { method });
|
|
885
|
+
const startedAt = Date.now();
|
|
886
|
+
try {
|
|
887
|
+
const raw = await call();
|
|
888
|
+
this.#log("debug", `nwc.${source}.completed`, `NWC wallet ${label} loaded.`, {
|
|
889
|
+
method,
|
|
890
|
+
duration_ms: Date.now() - startedAt
|
|
891
|
+
});
|
|
892
|
+
return raw;
|
|
893
|
+
} catch (error) {
|
|
894
|
+
const normalized = normalizeNwcWalletError(error);
|
|
895
|
+
this.#log("error", `nwc.${source}.failed`, `NWC wallet ${label} fetch failed.`, {
|
|
896
|
+
method,
|
|
897
|
+
duration_ms: Date.now() - startedAt,
|
|
898
|
+
error_code: normalized.code,
|
|
899
|
+
error_message: normalized.message
|
|
900
|
+
});
|
|
901
|
+
throw normalized;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async close() {
|
|
905
|
+
if (this.#closed) return;
|
|
906
|
+
this.#closed = true;
|
|
907
|
+
const client = this.#client ?? await this.#clientPromise?.catch(() => void 0);
|
|
908
|
+
this.#client = void 0;
|
|
909
|
+
this.#clientPromise = void 0;
|
|
910
|
+
await client?.close?.();
|
|
911
|
+
}
|
|
912
|
+
async ensurePreflight() {
|
|
913
|
+
if (!this.#requirePreflight || this.#preflightSummary !== void 0) return;
|
|
914
|
+
this.#preflightPromise ??= this.preflight();
|
|
915
|
+
try {
|
|
916
|
+
await this.#preflightPromise;
|
|
917
|
+
} catch (error) {
|
|
918
|
+
this.#preflightPromise = void 0;
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async getClient() {
|
|
923
|
+
if (this.#closed) {
|
|
924
|
+
throw new Error("OpenReceive NWC receive client is closed; create a new client.");
|
|
925
|
+
}
|
|
926
|
+
if (this.#client !== void 0) return this.#client;
|
|
927
|
+
this.#clientPromise ??= (async () => {
|
|
928
|
+
const client = this.#clientFactory !== void 0 ? await this.#clientFactory(this.#connection) : await createDefaultAlbyNwcClient(this.#connectionString);
|
|
929
|
+
this.#client = client;
|
|
930
|
+
return client;
|
|
931
|
+
})();
|
|
932
|
+
try {
|
|
933
|
+
return await this.#clientPromise;
|
|
934
|
+
} catch (error) {
|
|
935
|
+
this.#clientPromise = void 0;
|
|
936
|
+
throw error;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
function createNwcReceiveClient(options) {
|
|
941
|
+
return new AlbyNwcReceiveClient(options);
|
|
942
|
+
}
|
|
943
|
+
|
|
1
944
|
// src/swap/assets.ts
|
|
2
945
|
var OPENRECEIVE_SWAP_PAY_IN_ASSETS = [
|
|
3
946
|
"SOL_SOL",
|
|
@@ -92,13 +1035,13 @@ function isLightningNetwork(value) {
|
|
|
92
1035
|
}
|
|
93
1036
|
|
|
94
1037
|
// src/swap/fixedfloat.ts
|
|
95
|
-
import { ceilDiv as ceilDiv2, OPENRECEIVE_SATS_PER_BTC as OPENRECEIVE_SATS_PER_BTC2, recordOrEmpty as
|
|
1038
|
+
import { ceilDiv as ceilDiv2, OPENRECEIVE_SATS_PER_BTC as OPENRECEIVE_SATS_PER_BTC2, recordOrEmpty as recordOrEmpty6, unixSeconds } from "@openreceive/core";
|
|
96
1039
|
|
|
97
1040
|
// src/swap/fixedfloat-currencies.ts
|
|
98
|
-
import { recordOrEmpty as
|
|
1041
|
+
import { recordOrEmpty as recordOrEmpty4 } from "@openreceive/core";
|
|
99
1042
|
|
|
100
1043
|
// src/swap/fixedfloat-fields.ts
|
|
101
|
-
import { recordOrEmpty } from "@openreceive/core";
|
|
1044
|
+
import { recordOrEmpty as recordOrEmpty3 } from "@openreceive/core";
|
|
102
1045
|
function optionalCoercedString(value) {
|
|
103
1046
|
if (typeof value === "string" && value.length > 0) return value;
|
|
104
1047
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
@@ -111,7 +1054,7 @@ function optionalStringField(record, field) {
|
|
|
111
1054
|
function optionalNestedString(value, path) {
|
|
112
1055
|
let current = value;
|
|
113
1056
|
for (const key of path) {
|
|
114
|
-
current =
|
|
1057
|
+
current = recordOrEmpty3(current)[key];
|
|
115
1058
|
}
|
|
116
1059
|
return optionalCoercedString(current);
|
|
117
1060
|
}
|
|
@@ -127,7 +1070,7 @@ function optionalStringArrayField(record, field) {
|
|
|
127
1070
|
const string = optionalCoercedString(value);
|
|
128
1071
|
return string === void 0 ? [] : [string];
|
|
129
1072
|
}
|
|
130
|
-
function
|
|
1073
|
+
function requiredString2(value, field) {
|
|
131
1074
|
const string = optionalCoercedString(value);
|
|
132
1075
|
if (string === void 0) {
|
|
133
1076
|
throw new Error(`FixedFloat response missing ${field}.`);
|
|
@@ -442,11 +1385,11 @@ function fixedFloatRatePairKeys(resolution) {
|
|
|
442
1385
|
return keys;
|
|
443
1386
|
}
|
|
444
1387
|
function readFixedFloatCurrencies(data) {
|
|
445
|
-
const record =
|
|
1388
|
+
const record = recordOrEmpty4(data);
|
|
446
1389
|
const items = Array.isArray(data) ? data : Array.isArray(record.ccies) ? record.ccies : Array.isArray(record.currencies) ? record.currencies : [];
|
|
447
1390
|
const currencies = [];
|
|
448
1391
|
for (const item of items) {
|
|
449
|
-
const record2 =
|
|
1392
|
+
const record2 = recordOrEmpty4(item);
|
|
450
1393
|
const code = optionalStringField(record2, "code") ?? optionalStringField(record2, "ticker");
|
|
451
1394
|
const coin = optionalStringField(record2, "coin") ?? optionalStringField(record2, "currency") ?? optionalStringField(record2, "symbol");
|
|
452
1395
|
const network = optionalStringField(record2, "network") ?? optionalStringField(record2, "chain") ?? optionalStringField(record2, "networkName") ?? optionalStringField(record2, "name");
|
|
@@ -464,9 +1407,9 @@ function readFixedFloatCurrencies(data) {
|
|
|
464
1407
|
}
|
|
465
1408
|
|
|
466
1409
|
// src/swap/fixedfloat-orders.ts
|
|
467
|
-
import { recordOrEmpty as
|
|
1410
|
+
import { recordOrEmpty as recordOrEmpty5 } from "@openreceive/core";
|
|
468
1411
|
function normalizeFixedFloatOrder(data, input) {
|
|
469
|
-
const fields = extractFixedFloatOrderFields(
|
|
1412
|
+
const fields = extractFixedFloatOrderFields(recordOrEmpty5(data), input);
|
|
470
1413
|
const { attention, attention_reason } = fields.status;
|
|
471
1414
|
return {
|
|
472
1415
|
provider: input.provider,
|
|
@@ -498,7 +1441,7 @@ function readFixedFloatOrderFee(record) {
|
|
|
498
1441
|
return { currency: "USD", pay_in_fiat: payInFiat, payout_fiat: payoutFiat };
|
|
499
1442
|
}
|
|
500
1443
|
function requiredOrderField(record, field, fallback, label) {
|
|
501
|
-
return optionalStringField(record, field) ?? fallback ??
|
|
1444
|
+
return optionalStringField(record, field) ?? fallback ?? requiredString2(record[field], label);
|
|
502
1445
|
}
|
|
503
1446
|
function requiredExpiresAt(expiresAt) {
|
|
504
1447
|
if (expiresAt === void 0) {
|
|
@@ -516,8 +1459,8 @@ function persistedStatus(fallback) {
|
|
|
516
1459
|
}
|
|
517
1460
|
function extractFixedFloatOrderFields(record, input) {
|
|
518
1461
|
const fallback = input.fallback;
|
|
519
|
-
const from =
|
|
520
|
-
const emergency =
|
|
1462
|
+
const from = recordOrEmpty5(record.from);
|
|
1463
|
+
const emergency = recordOrEmpty5(record.emergency);
|
|
521
1464
|
const refundTxId = optionalNestedString(record, ["back", "tx", "id"]) ?? optionalNestedString(record, ["refund", "tx", "id"]) ?? fallback?.refund_tx_id;
|
|
522
1465
|
const rawStatus = optionalStringField(record, "status");
|
|
523
1466
|
const status = rawStatus === void 0 && fallback !== void 0 ? persistedStatus(fallback) : normalizeFixedFloatStatus(rawStatus ?? "NEW", emergency, refundTxId);
|
|
@@ -532,7 +1475,7 @@ function extractFixedFloatOrderFields(record, input) {
|
|
|
532
1475
|
// body the one we already persisted stands. A create body without either is
|
|
533
1476
|
// a provider contract break, not something to paper over with now + 10min.
|
|
534
1477
|
expiresAt: requiredExpiresAt(
|
|
535
|
-
readUnixSeconds(
|
|
1478
|
+
readUnixSeconds(recordOrEmpty5(record.time).expiration) ?? fallback?.expires_at
|
|
536
1479
|
),
|
|
537
1480
|
depositMemo: optionalStringField(from, "tag") ?? fallback?.deposit_memo,
|
|
538
1481
|
depositTxId: optionalNestedString(record, ["from", "tx", "id"]) ?? fallback?.deposit_tx_id,
|
|
@@ -1064,7 +2007,7 @@ var FixedFloatProvider = class {
|
|
|
1064
2007
|
direction: "to",
|
|
1065
2008
|
amount: amountMsatsToBtcString(invoiceAmountMsats)
|
|
1066
2009
|
});
|
|
1067
|
-
return readFixedFloatOrderFee(
|
|
2010
|
+
return readFixedFloatOrderFee(recordOrEmpty6(data));
|
|
1068
2011
|
} catch {
|
|
1069
2012
|
return void 0;
|
|
1070
2013
|
}
|
|
@@ -1175,7 +2118,7 @@ function amountMsatsToBtcString(amountMsats) {
|
|
|
1175
2118
|
}
|
|
1176
2119
|
|
|
1177
2120
|
// src/lsc-uri.ts
|
|
1178
|
-
import { compact } from "@openreceive/core";
|
|
2121
|
+
import { compact as compact2 } from "@openreceive/core";
|
|
1179
2122
|
var LSC_URI_PROTOCOL = "lightning+swapconnect:";
|
|
1180
2123
|
var LSC_ENV_NAMES = ["LSC_URI_PRIMARY", "LSC_URI_BACKUP"];
|
|
1181
2124
|
var LSC_QUERY_PARAMETERS = /* @__PURE__ */ new Set(["key", "secret"]);
|
|
@@ -1264,7 +2207,7 @@ function readLscConnectionsFromEnvironment(environment = process.env) {
|
|
|
1264
2207
|
function createLscSwapProvidersFromEnvironment(environment = process.env, options = {}) {
|
|
1265
2208
|
return readLscConnectionsFromEnvironment(environment).map(
|
|
1266
2209
|
(connection) => fixedFloatCompatibleSwapProvider(
|
|
1267
|
-
|
|
2210
|
+
compact2({
|
|
1268
2211
|
id: connection.providerId,
|
|
1269
2212
|
baseUrl: connection.baseUrl,
|
|
1270
2213
|
key: connection.key,
|
|
@@ -1314,7 +2257,7 @@ function providerIdFromUri(uri) {
|
|
|
1314
2257
|
}
|
|
1315
2258
|
|
|
1316
2259
|
// src/service/logging.ts
|
|
1317
|
-
import { compact as
|
|
2260
|
+
import { compact as compact3, isRecord } from "@openreceive/core";
|
|
1318
2261
|
function emitLog(options, level, event, message, fields = {}) {
|
|
1319
2262
|
emitEvent(options, {
|
|
1320
2263
|
level,
|
|
@@ -1375,7 +2318,7 @@ function redactSecrets(value) {
|
|
|
1375
2318
|
}
|
|
1376
2319
|
function summarizeSwapProviderApiRequest(entry) {
|
|
1377
2320
|
const body = isRecord(entry.body) ? entry.body : void 0;
|
|
1378
|
-
return
|
|
2321
|
+
return compact3({
|
|
1379
2322
|
provider: entry.provider,
|
|
1380
2323
|
path: entry.path,
|
|
1381
2324
|
reference: optionalLogString(body?.id),
|
|
@@ -1460,6 +2403,10 @@ function optionalLogNumber(value) {
|
|
|
1460
2403
|
}
|
|
1461
2404
|
|
|
1462
2405
|
export {
|
|
2406
|
+
WalletPreflightError,
|
|
2407
|
+
ReceiveCheckoutValidationError,
|
|
2408
|
+
HEX_64,
|
|
2409
|
+
createNwcReceiveClient,
|
|
1463
2410
|
OPENRECEIVE_SWAP_PAY_IN_ASSETS,
|
|
1464
2411
|
isSwapPayInAsset,
|
|
1465
2412
|
listSwapAssetInfo,
|