@openreceive/node 0.2.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/LICENSE +21 -0
- package/README.md +7 -0
- package/bin/openreceive.mjs +22 -0
- package/dist/chunk-6HALOJYF.js +1482 -0
- package/dist/cli.d.ts +63 -0
- package/dist/cli.js +1009 -0
- package/dist/index.d.ts +774 -0
- package/dist/index.js +2510 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2510 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OPENRECEIVE_SWAP_PAY_IN_ASSETS,
|
|
3
|
+
TransientSwapCache,
|
|
4
|
+
createLscSwapProvidersFromEnvironment,
|
|
5
|
+
createNwcEndpointLogger,
|
|
6
|
+
emitLog,
|
|
7
|
+
fixedFloatCompatibleSwapProvider,
|
|
8
|
+
fixedFloatProvider,
|
|
9
|
+
formatLscUri,
|
|
10
|
+
isSwapPayInAsset,
|
|
11
|
+
listSwapAssetInfo,
|
|
12
|
+
sanitizeEvent,
|
|
13
|
+
summarizeSwapProviderApiRequest,
|
|
14
|
+
summarizeSwapProviderApiResponse
|
|
15
|
+
} from "./chunk-6HALOJYF.js";
|
|
16
|
+
|
|
17
|
+
// src/index.ts
|
|
18
|
+
import { OpenReceiveError as OpenReceiveError4 } from "@openreceive/core";
|
|
19
|
+
|
|
20
|
+
// src/alby-nwc.ts
|
|
21
|
+
import {
|
|
22
|
+
compact,
|
|
23
|
+
formatSpendCapabilityRefusedMessage,
|
|
24
|
+
formatSpendCapabilityWarningMessage,
|
|
25
|
+
OpenReceiveError as OpenReceiveError2,
|
|
26
|
+
parseNwcUri
|
|
27
|
+
} from "@openreceive/core";
|
|
28
|
+
|
|
29
|
+
// src/nwc/errors.ts
|
|
30
|
+
import {
|
|
31
|
+
isErrorCode,
|
|
32
|
+
isRetryableErrorCode,
|
|
33
|
+
OpenReceiveError
|
|
34
|
+
} from "@openreceive/core";
|
|
35
|
+
var WalletPreflightError = class extends Error {
|
|
36
|
+
code;
|
|
37
|
+
summary;
|
|
38
|
+
constructor(code, message, summary) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "WalletPreflightError";
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.summary = summary;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var ReceiveCheckoutValidationError = class extends Error {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "ReceiveCheckoutValidationError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var NWC_ERROR_CODE_ALIASES = {
|
|
52
|
+
ABORT_ERROR: "TIMEOUT",
|
|
53
|
+
BAD_REQUEST: "INVALID_REQUEST",
|
|
54
|
+
CONNECTION_ERROR: "WALLET_UNAVAILABLE",
|
|
55
|
+
EXPIRED: "INVOICE_EXPIRED",
|
|
56
|
+
FETCH_ERROR: "WALLET_UNAVAILABLE",
|
|
57
|
+
FORBIDDEN: "RESTRICTED",
|
|
58
|
+
INVOICE_NOT_FOUND: "NOT_FOUND",
|
|
59
|
+
INVALID_PARAMETER: "INVALID_REQUEST",
|
|
60
|
+
INVALID_PARAMETERS: "INVALID_REQUEST",
|
|
61
|
+
INVALID_PARAMS: "INVALID_REQUEST",
|
|
62
|
+
METHOD_NOT_FOUND: "UNSUPPORTED_METHOD",
|
|
63
|
+
NETWORK_ERROR: "WALLET_UNAVAILABLE",
|
|
64
|
+
NIP47_NETWORK_ERROR: "WALLET_UNAVAILABLE",
|
|
65
|
+
NOSTR_NETWORK_ERROR: "WALLET_UNAVAILABLE",
|
|
66
|
+
NOT_AUTHORIZED: "UNAUTHORIZED",
|
|
67
|
+
NOT_SUPPORTED: "UNSUPPORTED_METHOD",
|
|
68
|
+
NOTFOUND: "NOT_FOUND",
|
|
69
|
+
PERMISSION_DENIED: "RESTRICTED",
|
|
70
|
+
RELAY_CONNECTION_ERROR: "WALLET_UNAVAILABLE",
|
|
71
|
+
REQUEST_TIMEOUT: "TIMEOUT",
|
|
72
|
+
SERVICE_UNAVAILABLE: "WALLET_UNAVAILABLE",
|
|
73
|
+
TIMED_OUT: "TIMEOUT",
|
|
74
|
+
TIMEOUT_ERROR: "TIMEOUT",
|
|
75
|
+
UNKNOWN_METHOD: "UNSUPPORTED_METHOD",
|
|
76
|
+
UNSUPPORTED: "UNSUPPORTED_METHOD",
|
|
77
|
+
UNSUPPORTED_ENCRYPTION_MODE: "UNSUPPORTED_ENCRYPTION",
|
|
78
|
+
WALLET_OFFLINE: "WALLET_UNAVAILABLE",
|
|
79
|
+
WALLET_UNREACHABLE: "WALLET_UNAVAILABLE"
|
|
80
|
+
};
|
|
81
|
+
var OPENRECEIVE_ERROR_MESSAGES = {
|
|
82
|
+
NOT_IMPLEMENTED: "NWC wallet service does not implement this method.",
|
|
83
|
+
RESTRICTED: "NWC wallet service restricted this request.",
|
|
84
|
+
UNAUTHORIZED: "NWC wallet service rejected authorization.",
|
|
85
|
+
FORBIDDEN: "The host application did not authorize this request.",
|
|
86
|
+
RATE_LIMITED: "NWC wallet service rate limited this request.",
|
|
87
|
+
QUOTA_EXCEEDED: "NWC wallet service quota was exceeded.",
|
|
88
|
+
INTERNAL: "NWC wallet service returned an internal error.",
|
|
89
|
+
UNSUPPORTED_ENCRYPTION: "NWC wallet service does not support the required encryption mode.",
|
|
90
|
+
OTHER: "NWC wallet service returned an unknown error.",
|
|
91
|
+
NOT_FOUND: "NWC wallet service could not find the requested resource.",
|
|
92
|
+
TIMEOUT: "NWC wallet service request timed out.",
|
|
93
|
+
INVALID_REQUEST: "OpenReceive sent an invalid NWC wallet request.",
|
|
94
|
+
WALLET_UNAVAILABLE: "NWC wallet service is unavailable.",
|
|
95
|
+
INVOICE_EXPIRED: "NWC wallet reported that the invoice is expired.",
|
|
96
|
+
UNSUPPORTED_METHOD: "NWC wallet service does not support the requested method.",
|
|
97
|
+
CONFLICT: "NWC wallet service reported a conflicting request."
|
|
98
|
+
};
|
|
99
|
+
function normalizeNwcWalletError(error) {
|
|
100
|
+
if (error instanceof OpenReceiveError) return error;
|
|
101
|
+
const records = collectErrorRecords(error);
|
|
102
|
+
const code = knownErrorCode(error) ?? errorCodeFromRecords(records) ?? (typeof error === "string" ? normalizeNwcErrorCode(error) : void 0) ?? "OTHER";
|
|
103
|
+
const message = errorMessageFromRecords(records, error, code);
|
|
104
|
+
const retryable = firstBooleanField(records, ["retryable"]) ?? isRetryableErrorCode(code);
|
|
105
|
+
const requestId = firstStringField(records, ["request_id", "requestId"]);
|
|
106
|
+
const details = firstRecordField(records, ["details"]);
|
|
107
|
+
const body = {
|
|
108
|
+
code,
|
|
109
|
+
message,
|
|
110
|
+
retryable,
|
|
111
|
+
...requestId === void 0 ? {} : { request_id: requestId },
|
|
112
|
+
...details === void 0 ? {} : { details }
|
|
113
|
+
};
|
|
114
|
+
return new OpenReceiveError(body, { cause: error });
|
|
115
|
+
}
|
|
116
|
+
function knownErrorCode(error) {
|
|
117
|
+
if (error instanceof ReceiveCheckoutValidationError) {
|
|
118
|
+
return "INVALID_REQUEST";
|
|
119
|
+
}
|
|
120
|
+
if (error instanceof WalletPreflightError) {
|
|
121
|
+
if (error.code === "missing_required_method") return "UNSUPPORTED_METHOD";
|
|
122
|
+
if (error.code === "spend_capability_advertised") return "RESTRICTED";
|
|
123
|
+
if (error.code === "unsupported_encryption") return "UNSUPPORTED_ENCRYPTION";
|
|
124
|
+
return "WALLET_UNAVAILABLE";
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
function errorCodeFromRecords(records) {
|
|
129
|
+
for (const record of records) {
|
|
130
|
+
const directCode = normalizeNwcErrorCode(record.code) ?? normalizeNwcErrorCode(record.error_code) ?? normalizeNwcErrorCode(record.errorCode) ?? normalizeNwcErrorCode(record.type);
|
|
131
|
+
if (directCode !== void 0 && directCode !== "OTHER") return directCode;
|
|
132
|
+
const nameCode = normalizeNwcErrorCode(record.name);
|
|
133
|
+
if (nameCode !== void 0 && nameCode !== "OTHER") return nameCode;
|
|
134
|
+
if (directCode !== void 0) return directCode;
|
|
135
|
+
}
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
function normalizeNwcErrorCode(value) {
|
|
139
|
+
if (typeof value !== "string" || value.trim().length === 0) return void 0;
|
|
140
|
+
const normalized = normalizeNwcErrorCodeText(value);
|
|
141
|
+
return NWC_ERROR_CODE_ALIASES[normalized] ?? (isErrorCode(normalized) ? normalized : void 0);
|
|
142
|
+
}
|
|
143
|
+
function normalizeNwcErrorCodeText(value) {
|
|
144
|
+
return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
145
|
+
}
|
|
146
|
+
function errorMessageFromRecords(records, error, code) {
|
|
147
|
+
const message = firstStringField(records, ["message", "description", "reason"]);
|
|
148
|
+
if (message !== void 0 && normalizeNwcErrorCode(message) !== code) {
|
|
149
|
+
return message;
|
|
150
|
+
}
|
|
151
|
+
if (typeof error === "string" && normalizeNwcErrorCode(error) === void 0) {
|
|
152
|
+
const trimmed = error.trim();
|
|
153
|
+
if (trimmed.length > 0) return trimmed;
|
|
154
|
+
}
|
|
155
|
+
return OPENRECEIVE_ERROR_MESSAGES[code];
|
|
156
|
+
}
|
|
157
|
+
function collectErrorRecords(value) {
|
|
158
|
+
const records = [];
|
|
159
|
+
collectErrorRecordsInto(value, records, /* @__PURE__ */ new Set());
|
|
160
|
+
return records;
|
|
161
|
+
}
|
|
162
|
+
function collectErrorRecordsInto(value, records, seen) {
|
|
163
|
+
if (typeof value !== "object" || value === null || seen.has(value)) return;
|
|
164
|
+
seen.add(value);
|
|
165
|
+
const record = value;
|
|
166
|
+
records.push(record);
|
|
167
|
+
for (const key of ["error", "result", "cause", "data"]) {
|
|
168
|
+
collectErrorRecordsInto(record[key], records, seen);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function firstStringField(records, fields) {
|
|
172
|
+
for (const record of records) {
|
|
173
|
+
for (const field of fields) {
|
|
174
|
+
const value = record[field];
|
|
175
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return void 0;
|
|
181
|
+
}
|
|
182
|
+
function firstBooleanField(records, fields) {
|
|
183
|
+
for (const record of records) {
|
|
184
|
+
for (const field of fields) {
|
|
185
|
+
const value = record[field];
|
|
186
|
+
if (typeof value === "boolean") return value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
function firstRecordField(records, fields) {
|
|
192
|
+
for (const record of records) {
|
|
193
|
+
for (const field of fields) {
|
|
194
|
+
const value = record[field];
|
|
195
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return void 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/nwc/normalize.ts
|
|
204
|
+
import {
|
|
205
|
+
OPENRECEIVE_MAX_AMOUNT_MSATS,
|
|
206
|
+
OPENRECEIVE_MIN_AMOUNT_MSATS,
|
|
207
|
+
OPENRECEIVE_NWC_METADATA_MAX_BYTES,
|
|
208
|
+
nonEmptyString,
|
|
209
|
+
recordOrEmpty
|
|
210
|
+
} from "@openreceive/core";
|
|
211
|
+
|
|
212
|
+
// src/hex.ts
|
|
213
|
+
var HEX_64 = /^[0-9a-fA-F]{64}$/;
|
|
214
|
+
|
|
215
|
+
// src/nwc/normalize.ts
|
|
216
|
+
var REQUIRED_RECEIVE_METHODS = ["make_invoice", "list_transactions"];
|
|
217
|
+
var SPEND_METHODS = [
|
|
218
|
+
"pay_invoice",
|
|
219
|
+
"multi_pay_invoice",
|
|
220
|
+
"pay_keysend",
|
|
221
|
+
"multi_pay_keysend"
|
|
222
|
+
];
|
|
223
|
+
function summarizeWalletCapabilities(connection, rawInfo, rawServiceInfo) {
|
|
224
|
+
const unwrappedInfo = unwrapNwcResult(rawInfo);
|
|
225
|
+
const info = recordOrEmpty(unwrappedInfo);
|
|
226
|
+
const serviceInfo = rawServiceInfo === void 0 ? void 0 : recordOrEmpty(unwrapNwcResult(rawServiceInfo));
|
|
227
|
+
const methods = normalizeStringList(
|
|
228
|
+
info.methods ?? info.capabilities ?? info.supported_methods ?? info.supportedMethods ?? (typeof unwrappedInfo === "string" ? unwrappedInfo : void 0)
|
|
229
|
+
).map(normalizeNwcMethodName);
|
|
230
|
+
const encryption = chooseEncryptionMode(
|
|
231
|
+
normalizeStringList(
|
|
232
|
+
serviceInfo?.encryption ?? serviceInfo?.encryptions ?? info.encryption ?? info.encryptions
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
const spendMethods = spendMethodsIn(methods);
|
|
236
|
+
const missingMethods = REQUIRED_RECEIVE_METHODS.filter((method) => !methods.includes(method));
|
|
237
|
+
const warnings = spendMethods.map(
|
|
238
|
+
(method) => `Wallet advertises spend method '${method}'; OpenReceive checkout will not expose it.`
|
|
239
|
+
);
|
|
240
|
+
return {
|
|
241
|
+
walletPubkey: connection.walletPubkey,
|
|
242
|
+
relays: [...connection.relays],
|
|
243
|
+
methods,
|
|
244
|
+
encryption,
|
|
245
|
+
spendCapabilityAdvertised: spendMethods.length > 0,
|
|
246
|
+
receiveCheckoutReady: missingMethods.length === 0,
|
|
247
|
+
warnings
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function spendMethodsIn(methods) {
|
|
251
|
+
return methods.filter(
|
|
252
|
+
(method) => SPEND_METHODS.includes(method)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
function toNip47MakeInvoiceParams(request) {
|
|
256
|
+
const params = {
|
|
257
|
+
amount: toSafeNumber(request.amount_msats, "amount_msats")
|
|
258
|
+
};
|
|
259
|
+
if (request.description !== void 0) params.description = request.description;
|
|
260
|
+
if (request.description_hash !== void 0) {
|
|
261
|
+
params.description_hash = request.description_hash;
|
|
262
|
+
}
|
|
263
|
+
if (request.expiry !== void 0) params.expiry = request.expiry;
|
|
264
|
+
if (request.metadata !== void 0) params.metadata = request.metadata;
|
|
265
|
+
return params;
|
|
266
|
+
}
|
|
267
|
+
function toNip47ListTransactionsParams(request) {
|
|
268
|
+
const params = {};
|
|
269
|
+
if (request.from !== void 0) params.from = request.from;
|
|
270
|
+
if (request.until !== void 0) params.until = request.until;
|
|
271
|
+
if (request.limit !== void 0) params.limit = request.limit;
|
|
272
|
+
if (request.offset !== void 0) params.offset = request.offset;
|
|
273
|
+
if (request.unpaid !== void 0) params.unpaid = request.unpaid;
|
|
274
|
+
if (request.type !== void 0) params.type = request.type;
|
|
275
|
+
return params;
|
|
276
|
+
}
|
|
277
|
+
function validateMakeInvoiceRequest(request) {
|
|
278
|
+
if (request.amount_msats < OPENRECEIVE_MIN_AMOUNT_MSATS) {
|
|
279
|
+
throw new ReceiveCheckoutValidationError("amount_msats must be at least 1000");
|
|
280
|
+
}
|
|
281
|
+
if (request.amount_msats > OPENRECEIVE_MAX_AMOUNT_MSATS) {
|
|
282
|
+
throw new ReceiveCheckoutValidationError("amount_msats exceeds JSON safe integer boundary");
|
|
283
|
+
}
|
|
284
|
+
if (request.description !== void 0 && request.description_hash !== void 0) {
|
|
285
|
+
throw new ReceiveCheckoutValidationError(
|
|
286
|
+
"At most one of description or description_hash may be present"
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (request.description_hash !== void 0 && !HEX_64.test(request.description_hash)) {
|
|
290
|
+
throw new ReceiveCheckoutValidationError("description_hash must be 64 hex characters");
|
|
291
|
+
}
|
|
292
|
+
if (request.metadata !== void 0) {
|
|
293
|
+
const metadataBytes = byteLength(JSON.stringify(request.metadata));
|
|
294
|
+
if (metadataBytes > OPENRECEIVE_NWC_METADATA_MAX_BYTES) {
|
|
295
|
+
throw new ReceiveCheckoutValidationError(
|
|
296
|
+
`metadata must serialize below ${OPENRECEIVE_NWC_METADATA_MAX_BYTES} bytes`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function validateListTransactionsRequest(request) {
|
|
302
|
+
validateOptionalNonNegativeInteger(request.from, "from");
|
|
303
|
+
validateOptionalNonNegativeInteger(request.until, "until");
|
|
304
|
+
validateOptionalNonNegativeInteger(request.offset, "offset");
|
|
305
|
+
if (request.limit !== void 0 && (!Number.isSafeInteger(request.limit) || request.limit <= 0)) {
|
|
306
|
+
throw new ReceiveCheckoutValidationError("limit must be a positive safe integer");
|
|
307
|
+
}
|
|
308
|
+
if (request.from !== void 0 && request.until !== void 0 && request.from > request.until) {
|
|
309
|
+
throw new ReceiveCheckoutValidationError("from must be less than or equal to until");
|
|
310
|
+
}
|
|
311
|
+
if (request.type !== void 0 && request.type !== "incoming" && request.type !== "outgoing") {
|
|
312
|
+
throw new ReceiveCheckoutValidationError("type must be incoming or outgoing");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function validateOptionalNonNegativeInteger(value, field) {
|
|
316
|
+
if (value === void 0) return;
|
|
317
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
318
|
+
throw new ReceiveCheckoutValidationError(`${field} must be a non-negative safe integer`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function normalizeMakeInvoiceResult(rawResult) {
|
|
322
|
+
const result = recordOrEmpty(unwrapNwcResult(rawResult));
|
|
323
|
+
const invoice = requiredString(result.invoice, "invoice");
|
|
324
|
+
const paymentHash = requiredString(result.payment_hash ?? result.paymentHash, "payment_hash");
|
|
325
|
+
if (!HEX_64.test(paymentHash)) {
|
|
326
|
+
throw new TypeError("payment_hash must be 64 hex characters");
|
|
327
|
+
}
|
|
328
|
+
const amountMsats = toBigInt(result.amount_msats ?? result.amount, "amount_msats");
|
|
329
|
+
const createdAt = parseOptionalInteger(result.created_at ?? result.createdAt, "created_at");
|
|
330
|
+
const expiresAt = parseOptionalInteger(result.expires_at ?? result.expiresAt, "expires_at");
|
|
331
|
+
return {
|
|
332
|
+
invoice,
|
|
333
|
+
payment_hash: paymentHash,
|
|
334
|
+
amount_msats: amountMsats,
|
|
335
|
+
...createdAt === void 0 ? {} : { created_at: createdAt },
|
|
336
|
+
...expiresAt === void 0 ? {} : { expires_at: expiresAt }
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function normalizeListTransactionsResult(rawResult) {
|
|
340
|
+
const unwrapped = unwrapNwcResult(rawResult);
|
|
341
|
+
const result = recordOrEmpty(unwrapped);
|
|
342
|
+
let rawTransactions;
|
|
343
|
+
if (Array.isArray(result.transactions)) {
|
|
344
|
+
rawTransactions = result.transactions;
|
|
345
|
+
} else if (Array.isArray(unwrapped)) {
|
|
346
|
+
rawTransactions = unwrapped;
|
|
347
|
+
} else if ((unwrapped === void 0 || unwrapped === null || typeof unwrapped === "object") && Object.keys(result).length === 0) {
|
|
348
|
+
rawTransactions = [];
|
|
349
|
+
} else {
|
|
350
|
+
throw new TypeError("list_transactions returned an unrecognized result shape");
|
|
351
|
+
}
|
|
352
|
+
const transactions = [];
|
|
353
|
+
let skippedRows = 0;
|
|
354
|
+
for (const rawTransaction of rawTransactions) {
|
|
355
|
+
try {
|
|
356
|
+
transactions.push(normalizeNwcTransaction(rawTransaction));
|
|
357
|
+
} catch {
|
|
358
|
+
skippedRows += 1;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (transactions.length === 0 && skippedRows > 0) {
|
|
362
|
+
throw new TypeError("list_transactions returned no usable rows");
|
|
363
|
+
}
|
|
364
|
+
return { transactions, skippedRows };
|
|
365
|
+
}
|
|
366
|
+
function normalizeNwcTransaction(rawTransaction) {
|
|
367
|
+
const result = recordOrEmpty(rawTransaction);
|
|
368
|
+
const normalized = {};
|
|
369
|
+
const type = normalizeTransactionType(result.type);
|
|
370
|
+
if (type !== void 0) normalized.type = type;
|
|
371
|
+
const invoice = nonEmptyString(result.invoice);
|
|
372
|
+
if (invoice !== void 0) normalized.invoice = invoice;
|
|
373
|
+
const paymentHash = nonEmptyString(result.payment_hash ?? result.paymentHash);
|
|
374
|
+
if (paymentHash !== void 0) {
|
|
375
|
+
if (!/^[0-9a-fA-F]{64}$/.test(paymentHash)) {
|
|
376
|
+
throw new TypeError("payment_hash must be 64 hexadecimal characters");
|
|
377
|
+
}
|
|
378
|
+
normalized.payment_hash = paymentHash;
|
|
379
|
+
}
|
|
380
|
+
if (result.amount_msats !== void 0 || result.amount !== void 0) {
|
|
381
|
+
normalized.amount_msats = toBigInt(result.amount_msats ?? result.amount, "amount_msats");
|
|
382
|
+
}
|
|
383
|
+
const transactionState = normalizeTransactionState(
|
|
384
|
+
result.transaction_state ?? result.transactionState ?? result.state
|
|
385
|
+
) ?? (result.settled === true || result.paid === true ? "settled" : void 0);
|
|
386
|
+
if (transactionState !== void 0) {
|
|
387
|
+
normalized.transaction_state = transactionState;
|
|
388
|
+
}
|
|
389
|
+
const createdAt = optionalUnixSeconds(result.created_at ?? result.createdAt, "created_at");
|
|
390
|
+
if (createdAt !== void 0) normalized.created_at = createdAt;
|
|
391
|
+
const expiresAt = optionalUnixSeconds(result.expires_at ?? result.expiresAt, "expires_at");
|
|
392
|
+
if (expiresAt !== void 0) normalized.expires_at = expiresAt;
|
|
393
|
+
const settledAt = optionalUnixSeconds(result.settled_at ?? result.settledAt, "settled_at");
|
|
394
|
+
if (settledAt !== void 0) normalized.settled_at = settledAt;
|
|
395
|
+
const preimage = nonEmptyString(result.preimage);
|
|
396
|
+
if (preimage !== void 0) normalized.preimage = preimage;
|
|
397
|
+
const description = nonEmptyString(result.description);
|
|
398
|
+
if (description !== void 0) normalized.description = description;
|
|
399
|
+
const descriptionHash = nonEmptyString(result.description_hash ?? result.descriptionHash);
|
|
400
|
+
if (descriptionHash !== void 0) normalized.description_hash = descriptionHash;
|
|
401
|
+
if (result.fees_paid !== void 0 || result.feesPaid !== void 0) {
|
|
402
|
+
normalized.fees_paid_msats = toBigInt(result.fees_paid ?? result.feesPaid, "fees_paid");
|
|
403
|
+
}
|
|
404
|
+
return normalized;
|
|
405
|
+
}
|
|
406
|
+
function normalizeNwcNotification(rawNotification) {
|
|
407
|
+
const record = recordOrEmpty(rawNotification);
|
|
408
|
+
const type = typeof record.notification_type === "string" ? record.notification_type : typeof record.notificationType === "string" ? record.notificationType : typeof record.type === "string" ? record.type : "unknown";
|
|
409
|
+
const payload = recordOrEmpty(record.notification);
|
|
410
|
+
let transaction;
|
|
411
|
+
if (Object.keys(payload).length > 0) {
|
|
412
|
+
try {
|
|
413
|
+
transaction = normalizeNwcTransaction(payload);
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const rawHash = payload.payment_hash ?? payload.paymentHash ?? record.payment_hash ?? record.paymentHash;
|
|
418
|
+
const paymentHash = typeof rawHash === "string" && rawHash.length > 0 ? rawHash : transaction?.payment_hash;
|
|
419
|
+
return {
|
|
420
|
+
type,
|
|
421
|
+
...paymentHash === void 0 ? {} : { payment_hash: paymentHash },
|
|
422
|
+
...transaction === void 0 ? {} : { transaction }
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function chooseEncryptionMode(encryptionModes) {
|
|
426
|
+
const normalized = encryptionModes.map((mode) => mode.toLowerCase().replace(/[- ]/g, "_"));
|
|
427
|
+
if (normalized.includes("nip44_v2") || normalized.includes("nip44") || normalized.includes("nip_44")) {
|
|
428
|
+
return "nip44_v2";
|
|
429
|
+
}
|
|
430
|
+
if (normalized.length === 0 || normalized.includes("nip04") || normalized.includes("nip_04")) {
|
|
431
|
+
return "nip04";
|
|
432
|
+
}
|
|
433
|
+
return void 0;
|
|
434
|
+
}
|
|
435
|
+
function normalizeStringList(value) {
|
|
436
|
+
if (Array.isArray(value)) {
|
|
437
|
+
return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
438
|
+
}
|
|
439
|
+
if (typeof value === "string") {
|
|
440
|
+
return value.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
|
|
441
|
+
}
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
function normalizeNwcMethodName(value) {
|
|
445
|
+
return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
446
|
+
}
|
|
447
|
+
function normalizeTransactionState(value) {
|
|
448
|
+
if (typeof value !== "string") return void 0;
|
|
449
|
+
const normalized = value.toLowerCase();
|
|
450
|
+
if (normalized === "pending" || normalized === "settled" || normalized === "expired" || normalized === "failed" || normalized === "accepted") {
|
|
451
|
+
return normalized;
|
|
452
|
+
}
|
|
453
|
+
return void 0;
|
|
454
|
+
}
|
|
455
|
+
function normalizeTransactionType(value) {
|
|
456
|
+
if (typeof value !== "string") return void 0;
|
|
457
|
+
const normalized = value.toLowerCase();
|
|
458
|
+
if (normalized === "incoming" || normalized === "outgoing") return normalized;
|
|
459
|
+
return void 0;
|
|
460
|
+
}
|
|
461
|
+
function parseOptionalInteger(value, fieldName) {
|
|
462
|
+
if (value === void 0 || value === null) return void 0;
|
|
463
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
464
|
+
throw new TypeError(`${fieldName} must be a non-negative safe integer`);
|
|
465
|
+
}
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
function optionalUnixSeconds(value, fieldName) {
|
|
469
|
+
if (value === void 0 || value === null) return void 0;
|
|
470
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
471
|
+
throw new TypeError(`${fieldName} must be a non-negative number of Unix seconds`);
|
|
472
|
+
}
|
|
473
|
+
return Math.floor(value);
|
|
474
|
+
}
|
|
475
|
+
function requiredString(value, fieldName) {
|
|
476
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
477
|
+
throw new TypeError(`${fieldName} must be a non-empty string`);
|
|
478
|
+
}
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
function toBigInt(value, fieldName) {
|
|
482
|
+
if (typeof value === "bigint") return value;
|
|
483
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
484
|
+
return BigInt(value);
|
|
485
|
+
}
|
|
486
|
+
if (typeof value === "string" && /^[0-9]+$/.test(value)) {
|
|
487
|
+
return BigInt(value);
|
|
488
|
+
}
|
|
489
|
+
throw new TypeError(`${fieldName} must be an integer`);
|
|
490
|
+
}
|
|
491
|
+
function toSafeNumber(value, fieldName) {
|
|
492
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
493
|
+
throw new ReceiveCheckoutValidationError(`${fieldName} exceeds JSON safe integer boundary`);
|
|
494
|
+
}
|
|
495
|
+
return Number(value);
|
|
496
|
+
}
|
|
497
|
+
function byteLength(value) {
|
|
498
|
+
return new TextEncoder().encode(value).length;
|
|
499
|
+
}
|
|
500
|
+
function unwrapNwcResult(value) {
|
|
501
|
+
const record = recordOrEmpty(value);
|
|
502
|
+
return record.result ?? value;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/nwc/transport.ts
|
|
506
|
+
import { createRequire } from "module";
|
|
507
|
+
import { pathToFileURL } from "url";
|
|
508
|
+
import { recordOrEmpty as recordOrEmpty2 } from "@openreceive/core";
|
|
509
|
+
var require2 = createRequire(import.meta.url);
|
|
510
|
+
async function callRequiredMethod(client, names, request) {
|
|
511
|
+
for (const name of names) {
|
|
512
|
+
const method = client[name];
|
|
513
|
+
if (typeof method === "function") {
|
|
514
|
+
return await method.call(
|
|
515
|
+
client,
|
|
516
|
+
request
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
throw new WalletPreflightError(
|
|
521
|
+
"wallet_unavailable",
|
|
522
|
+
`NWC client does not expose ${names.join(" or ")}.`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
async function createDefaultAlbyNwcClient(connectionString) {
|
|
526
|
+
ensureNodeWebSocket();
|
|
527
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
528
|
+
const namespace = recordOrEmpty2(
|
|
529
|
+
await dynamicImport(pathToFileURL(require2.resolve("@getalby/sdk/nwc")).href)
|
|
530
|
+
);
|
|
531
|
+
const Constructor = namespace.NWCClient;
|
|
532
|
+
if (typeof Constructor !== "function") {
|
|
533
|
+
throw new WalletPreflightError(
|
|
534
|
+
"wallet_unavailable",
|
|
535
|
+
"@getalby/sdk/nwc did not expose NWCClient."
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
const NWCClientConstructor = Constructor;
|
|
539
|
+
return new NWCClientConstructor({
|
|
540
|
+
nostrWalletConnectUrl: connectionString
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function ensureNodeWebSocket() {
|
|
544
|
+
if (globalThis.WebSocket !== void 0) return;
|
|
545
|
+
throw new WalletPreflightError(
|
|
546
|
+
"wallet_unavailable",
|
|
547
|
+
"OpenReceive requires Node 22 or newer with the built-in WebSocket API."
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
async function closeNwcNotificationSubscription(subscription) {
|
|
551
|
+
if (typeof subscription !== "function") {
|
|
552
|
+
throw new TypeError("NWC subscribeNotifications must resolve to an unsubscribe function.");
|
|
553
|
+
}
|
|
554
|
+
await subscription();
|
|
555
|
+
}
|
|
556
|
+
function delay(milliseconds) {
|
|
557
|
+
return new Promise((resolve) => {
|
|
558
|
+
setTimeout(resolve, milliseconds);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/alby-nwc.ts
|
|
563
|
+
var AlbyNwcReceiveClient = class {
|
|
564
|
+
/**
|
|
565
|
+
* Safe, loggable view of the connection (wallet pubkey, relays, lud16, and
|
|
566
|
+
* the secret-redacted URI). The parsed connection with the client secret is
|
|
567
|
+
* deliberately private: `console.log(client)` or host error serialization
|
|
568
|
+
* must never leak the secret.
|
|
569
|
+
*/
|
|
570
|
+
connection;
|
|
571
|
+
#connection;
|
|
572
|
+
#connectionString;
|
|
573
|
+
#client;
|
|
574
|
+
#clientPromise;
|
|
575
|
+
#clientFactory;
|
|
576
|
+
#preflightSummary;
|
|
577
|
+
#preflightPromise;
|
|
578
|
+
#requirePreflight;
|
|
579
|
+
#logger;
|
|
580
|
+
#allowSpendCapableWallet;
|
|
581
|
+
#spendCapabilityWarningDelayMs;
|
|
582
|
+
#spendCapabilityWarning;
|
|
583
|
+
#closed = false;
|
|
584
|
+
constructor(options) {
|
|
585
|
+
this.#connectionString = options.connectionString;
|
|
586
|
+
this.#connection = parseNwcUri(options.connectionString);
|
|
587
|
+
this.connection = {
|
|
588
|
+
walletPubkey: this.#connection.walletPubkey,
|
|
589
|
+
relays: [...this.#connection.relays],
|
|
590
|
+
...this.#connection.lud16 === void 0 ? {} : { lud16: this.#connection.lud16 },
|
|
591
|
+
redacted: this.#connection.redacted
|
|
592
|
+
};
|
|
593
|
+
this.#client = options.client;
|
|
594
|
+
this.#clientFactory = options.clientFactory;
|
|
595
|
+
this.#requirePreflight = options.requirePreflight ?? true;
|
|
596
|
+
this.#logger = options.logger;
|
|
597
|
+
this.#allowSpendCapableWallet = options.allowSpendCapableWallet ?? false;
|
|
598
|
+
this.#spendCapabilityWarningDelayMs = options.spendCapabilityWarningDelayMs ?? 0;
|
|
599
|
+
this.#spendCapabilityWarning = options.spendCapabilityWarning ?? ((message) => {
|
|
600
|
+
if (this.#logger === void 0) console.error(message);
|
|
601
|
+
else this.#log("error", "nwc.spend_capability_warning", message);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
#log(level, event, message, fields = {}) {
|
|
605
|
+
if (this.#logger === void 0) return;
|
|
606
|
+
try {
|
|
607
|
+
this.#logger({
|
|
608
|
+
level,
|
|
609
|
+
event,
|
|
610
|
+
message,
|
|
611
|
+
wallet_pubkey: this.connection.walletPubkey,
|
|
612
|
+
...fields
|
|
613
|
+
});
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async preflight() {
|
|
618
|
+
const client = await this.getClient();
|
|
619
|
+
const hasGetInfo = typeof client.getInfo === "function" || typeof client.get_info === "function";
|
|
620
|
+
const hasInfoEvent = typeof client.getWalletServiceInfo === "function";
|
|
621
|
+
const rawInfo = hasGetInfo || !hasInfoEvent ? await this.#fetchWalletInfo(
|
|
622
|
+
"get_info",
|
|
623
|
+
() => callRequiredMethod(client, ["getInfo", "get_info"], {})
|
|
624
|
+
) : void 0;
|
|
625
|
+
const rawServiceInfo = hasInfoEvent ? await this.#fetchWalletInfo(
|
|
626
|
+
"info_event",
|
|
627
|
+
() => client.getWalletServiceInfo()
|
|
628
|
+
) : void 0;
|
|
629
|
+
if (rawInfo === void 0) {
|
|
630
|
+
this.#log(
|
|
631
|
+
"warn",
|
|
632
|
+
"nwc.info_event.methods_fallback",
|
|
633
|
+
"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."
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
const summary = summarizeWalletCapabilities(
|
|
637
|
+
this.#connection,
|
|
638
|
+
rawInfo ?? rawServiceInfo,
|
|
639
|
+
rawServiceInfo
|
|
640
|
+
);
|
|
641
|
+
this.#log("debug", "nwc.preflight.summarized", "NWC wallet capabilities summarized.", {
|
|
642
|
+
methods_source: rawInfo === void 0 ? "info_event" : "get_info",
|
|
643
|
+
methods: summary.methods,
|
|
644
|
+
encryption: summary.encryption,
|
|
645
|
+
receive_checkout_ready: summary.receiveCheckoutReady,
|
|
646
|
+
spend_capability_advertised: summary.spendCapabilityAdvertised
|
|
647
|
+
});
|
|
648
|
+
if (!summary.receiveCheckoutReady) {
|
|
649
|
+
throw new WalletPreflightError(
|
|
650
|
+
"missing_required_method",
|
|
651
|
+
"NWC wallet must advertise make_invoice and list_transactions for receive checkout.",
|
|
652
|
+
summary
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
if (summary.encryption !== "nip04" && summary.encryption !== "nip44_v2") {
|
|
656
|
+
throw new WalletPreflightError(
|
|
657
|
+
"unsupported_encryption",
|
|
658
|
+
"NWC wallet must support NIP-04 or NIP-44 v2 encryption.",
|
|
659
|
+
summary
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (summary.spendCapabilityAdvertised) {
|
|
663
|
+
const spendMethods = spendMethodsIn(summary.methods);
|
|
664
|
+
if (!this.#allowSpendCapableWallet) {
|
|
665
|
+
this.#log(
|
|
666
|
+
"error",
|
|
667
|
+
"nwc.spend_capability_advertised",
|
|
668
|
+
"NWC connection advertises send-payment methods; refusing to boot without an explicit override.",
|
|
669
|
+
{ methods: spendMethods }
|
|
670
|
+
);
|
|
671
|
+
throw new WalletPreflightError(
|
|
672
|
+
"spend_capability_advertised",
|
|
673
|
+
formatSpendCapabilityRefusedMessage({ spendMethods }),
|
|
674
|
+
summary
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const warning = formatSpendCapabilityWarningMessage({
|
|
678
|
+
spendMethods
|
|
679
|
+
});
|
|
680
|
+
this.#log(
|
|
681
|
+
"error",
|
|
682
|
+
"nwc.spend_capability_advertised",
|
|
683
|
+
"NWC connection advertises send-payment methods; continuing because the spend-capable override is set.",
|
|
684
|
+
{ methods: spendMethods }
|
|
685
|
+
);
|
|
686
|
+
try {
|
|
687
|
+
this.#spendCapabilityWarning(warning);
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
if (this.#spendCapabilityWarningDelayMs > 0) {
|
|
691
|
+
await delay(this.#spendCapabilityWarningDelayMs);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
this.#preflightSummary = summary;
|
|
695
|
+
return summary;
|
|
696
|
+
}
|
|
697
|
+
async makeInvoice(request) {
|
|
698
|
+
await this.ensurePreflight();
|
|
699
|
+
validateMakeInvoiceRequest(request);
|
|
700
|
+
this.#log(
|
|
701
|
+
"debug",
|
|
702
|
+
"nwc.make_invoice.requested",
|
|
703
|
+
"Calling NWC wallet make_invoice.",
|
|
704
|
+
compact({
|
|
705
|
+
method: "make_invoice",
|
|
706
|
+
// String form: msats above 2^53 would silently round through Number().
|
|
707
|
+
amount_msats: request.amount_msats.toString(),
|
|
708
|
+
expiry: request.expiry,
|
|
709
|
+
has_description: request.description !== void 0,
|
|
710
|
+
has_description_hash: request.description_hash !== void 0
|
|
711
|
+
})
|
|
712
|
+
);
|
|
713
|
+
const startedAt = Date.now();
|
|
714
|
+
let rawResult;
|
|
715
|
+
try {
|
|
716
|
+
rawResult = await callRequiredMethod(
|
|
717
|
+
await this.getClient(),
|
|
718
|
+
["makeInvoice", "make_invoice"],
|
|
719
|
+
toNip47MakeInvoiceParams(request)
|
|
720
|
+
);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
const normalized = normalizeNwcWalletError(error);
|
|
723
|
+
this.#log("error", "nwc.make_invoice.failed", "NWC wallet make_invoice failed.", {
|
|
724
|
+
method: "make_invoice",
|
|
725
|
+
duration_ms: Date.now() - startedAt,
|
|
726
|
+
error_code: normalized.code,
|
|
727
|
+
error_message: normalized.message
|
|
728
|
+
});
|
|
729
|
+
throw normalized;
|
|
730
|
+
}
|
|
731
|
+
let result;
|
|
732
|
+
try {
|
|
733
|
+
result = normalizeMakeInvoiceResult(rawResult);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
const normalized = normalizeNwcWalletError(error);
|
|
736
|
+
this.#log("error", "nwc.make_invoice.failed", "NWC wallet make_invoice failed.", {
|
|
737
|
+
method: "make_invoice",
|
|
738
|
+
duration_ms: Date.now() - startedAt,
|
|
739
|
+
error_code: normalized.code,
|
|
740
|
+
error_message: normalized.message
|
|
741
|
+
});
|
|
742
|
+
throw normalized;
|
|
743
|
+
}
|
|
744
|
+
this.#log("debug", "nwc.make_invoice.completed", "NWC wallet make_invoice completed.", {
|
|
745
|
+
method: "make_invoice",
|
|
746
|
+
duration_ms: Date.now() - startedAt,
|
|
747
|
+
payment_hash: result.payment_hash,
|
|
748
|
+
amount_msats: result.amount_msats.toString()
|
|
749
|
+
});
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
async listTransactions(request) {
|
|
753
|
+
await this.ensurePreflight();
|
|
754
|
+
validateListTransactionsRequest(request);
|
|
755
|
+
this.#log(
|
|
756
|
+
"debug",
|
|
757
|
+
"nwc.list_transactions.requested",
|
|
758
|
+
"Calling NWC wallet list_transactions.",
|
|
759
|
+
compact({
|
|
760
|
+
method: "list_transactions",
|
|
761
|
+
from: request.from,
|
|
762
|
+
until: request.until,
|
|
763
|
+
limit: request.limit,
|
|
764
|
+
offset: request.offset,
|
|
765
|
+
unpaid: request.unpaid,
|
|
766
|
+
type: request.type
|
|
767
|
+
})
|
|
768
|
+
);
|
|
769
|
+
const startedAt = Date.now();
|
|
770
|
+
let rawResult;
|
|
771
|
+
try {
|
|
772
|
+
rawResult = await callRequiredMethod(
|
|
773
|
+
await this.getClient(),
|
|
774
|
+
["listTransactions", "list_transactions"],
|
|
775
|
+
toNip47ListTransactionsParams(request)
|
|
776
|
+
);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
const normalized = normalizeNwcWalletError(error);
|
|
779
|
+
this.#log("error", "nwc.list_transactions.failed", "NWC wallet list_transactions failed.", {
|
|
780
|
+
method: "list_transactions",
|
|
781
|
+
duration_ms: Date.now() - startedAt,
|
|
782
|
+
error_code: normalized.code,
|
|
783
|
+
error_message: normalized.message
|
|
784
|
+
});
|
|
785
|
+
throw normalized;
|
|
786
|
+
}
|
|
787
|
+
let result;
|
|
788
|
+
try {
|
|
789
|
+
result = normalizeListTransactionsResult(rawResult);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
const normalized = normalizeNwcWalletError(error);
|
|
792
|
+
this.#log("error", "nwc.list_transactions.failed", "NWC wallet list_transactions failed.", {
|
|
793
|
+
method: "list_transactions",
|
|
794
|
+
duration_ms: Date.now() - startedAt,
|
|
795
|
+
error_code: normalized.code,
|
|
796
|
+
error_message: normalized.message
|
|
797
|
+
});
|
|
798
|
+
throw normalized;
|
|
799
|
+
}
|
|
800
|
+
if (result.skippedRows > 0) {
|
|
801
|
+
this.#log(
|
|
802
|
+
"warn",
|
|
803
|
+
"nwc.list_transactions.rows_skipped",
|
|
804
|
+
"Skipped wallet transaction rows that could not be normalized.",
|
|
805
|
+
{
|
|
806
|
+
method: "list_transactions",
|
|
807
|
+
skipped_count: result.skippedRows
|
|
808
|
+
}
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
this.#log(
|
|
812
|
+
"debug",
|
|
813
|
+
"nwc.list_transactions.completed",
|
|
814
|
+
"NWC wallet list_transactions completed.",
|
|
815
|
+
{
|
|
816
|
+
method: "list_transactions",
|
|
817
|
+
duration_ms: Date.now() - startedAt,
|
|
818
|
+
transaction_count: result.transactions.length
|
|
819
|
+
}
|
|
820
|
+
);
|
|
821
|
+
return {
|
|
822
|
+
transactions: result.transactions,
|
|
823
|
+
...result.skippedRows === 0 ? {} : { skippedRows: result.skippedRows }
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Opt-in NWC-02 notification subscription, limited to `payment_received`.
|
|
828
|
+
* Notifications are authenticated wallet data: the handler receives the
|
|
829
|
+
* payload normalized like a `list_transactions` row so a payload satisfying
|
|
830
|
+
* the settlement rule can settle its pending attempt directly, while
|
|
831
|
+
* anything less only wakes reconciliation. Direct settlement assumes the
|
|
832
|
+
* client binds notification decryption to the connection's wallet pubkey
|
|
833
|
+
* (the bundled @getalby/sdk does). Resolves to an unsubscribe function.
|
|
834
|
+
* Throws an OpenReceiveError with code `UNSUPPORTED_METHOD` when the
|
|
835
|
+
* underlying wallet client does not support NWC notifications.
|
|
836
|
+
*/
|
|
837
|
+
async subscribeNotifications(handler) {
|
|
838
|
+
const client = await this.getClient();
|
|
839
|
+
const subscribe = client.subscribeNotifications;
|
|
840
|
+
if (typeof subscribe !== "function") {
|
|
841
|
+
throw new OpenReceiveError2({
|
|
842
|
+
code: "UNSUPPORTED_METHOD",
|
|
843
|
+
message: "NWC wallet client does not support NWC notifications (subscribeNotifications). Keep polling reconciliation.",
|
|
844
|
+
retryable: false
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
this.#log(
|
|
848
|
+
"debug",
|
|
849
|
+
"nwc.notifications.subscribe.requested",
|
|
850
|
+
"Subscribing to NWC-02 payment_received notifications.",
|
|
851
|
+
{ notification_types: ["payment_received"] }
|
|
852
|
+
);
|
|
853
|
+
let subscription;
|
|
854
|
+
try {
|
|
855
|
+
subscription = await subscribe.call(
|
|
856
|
+
client,
|
|
857
|
+
(rawNotification) => {
|
|
858
|
+
const notification = normalizeNwcNotification(rawNotification);
|
|
859
|
+
if (notification.type !== "payment_received") return;
|
|
860
|
+
this.#log(
|
|
861
|
+
"debug",
|
|
862
|
+
"nwc.notifications.received",
|
|
863
|
+
"NWC payment_received notification received.",
|
|
864
|
+
compact({
|
|
865
|
+
notification_type: notification.type,
|
|
866
|
+
payment_hash: notification.payment_hash
|
|
867
|
+
})
|
|
868
|
+
);
|
|
869
|
+
try {
|
|
870
|
+
handler(notification);
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
["payment_received"]
|
|
875
|
+
);
|
|
876
|
+
} catch (error) {
|
|
877
|
+
const normalized = normalizeNwcWalletError(error);
|
|
878
|
+
this.#log(
|
|
879
|
+
"error",
|
|
880
|
+
"nwc.notifications.subscribe.failed",
|
|
881
|
+
"NWC notification subscription failed.",
|
|
882
|
+
{ error_code: normalized.code, error_message: normalized.message }
|
|
883
|
+
);
|
|
884
|
+
throw normalized;
|
|
885
|
+
}
|
|
886
|
+
this.#log(
|
|
887
|
+
"info",
|
|
888
|
+
"nwc.notifications.subscribe.completed",
|
|
889
|
+
"Subscribed to NWC-02 payment_received notifications; polling reconciliation remains the safety net."
|
|
890
|
+
);
|
|
891
|
+
return async () => {
|
|
892
|
+
await closeNwcNotificationSubscription(subscription);
|
|
893
|
+
this.#log(
|
|
894
|
+
"info",
|
|
895
|
+
"nwc.notifications.unsubscribed",
|
|
896
|
+
"Unsubscribed from NWC-02 wallet notifications."
|
|
897
|
+
);
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
async #fetchWalletInfo(source, call) {
|
|
901
|
+
const method = source === "get_info" ? "get_info" : "getWalletServiceInfo";
|
|
902
|
+
const label = source === "get_info" ? "get_info" : "info event (kind 13194)";
|
|
903
|
+
this.#log("debug", `nwc.${source}.requested`, `Fetching NWC wallet ${label}.`, { method });
|
|
904
|
+
const startedAt = Date.now();
|
|
905
|
+
try {
|
|
906
|
+
const raw = await call();
|
|
907
|
+
this.#log("debug", `nwc.${source}.completed`, `NWC wallet ${label} loaded.`, {
|
|
908
|
+
method,
|
|
909
|
+
duration_ms: Date.now() - startedAt
|
|
910
|
+
});
|
|
911
|
+
return raw;
|
|
912
|
+
} catch (error) {
|
|
913
|
+
const normalized = normalizeNwcWalletError(error);
|
|
914
|
+
this.#log("error", `nwc.${source}.failed`, `NWC wallet ${label} fetch failed.`, {
|
|
915
|
+
method,
|
|
916
|
+
duration_ms: Date.now() - startedAt,
|
|
917
|
+
error_code: normalized.code,
|
|
918
|
+
error_message: normalized.message
|
|
919
|
+
});
|
|
920
|
+
throw normalized;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async close() {
|
|
924
|
+
if (this.#closed) return;
|
|
925
|
+
this.#closed = true;
|
|
926
|
+
const client = this.#client ?? await this.#clientPromise?.catch(() => void 0);
|
|
927
|
+
this.#client = void 0;
|
|
928
|
+
this.#clientPromise = void 0;
|
|
929
|
+
await client?.close?.();
|
|
930
|
+
}
|
|
931
|
+
async ensurePreflight() {
|
|
932
|
+
if (!this.#requirePreflight || this.#preflightSummary !== void 0) return;
|
|
933
|
+
this.#preflightPromise ??= this.preflight();
|
|
934
|
+
try {
|
|
935
|
+
await this.#preflightPromise;
|
|
936
|
+
} catch (error) {
|
|
937
|
+
this.#preflightPromise = void 0;
|
|
938
|
+
throw error;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async getClient() {
|
|
942
|
+
if (this.#closed) {
|
|
943
|
+
throw new Error("OpenReceive NWC receive client is closed; create a new client.");
|
|
944
|
+
}
|
|
945
|
+
if (this.#client !== void 0) return this.#client;
|
|
946
|
+
this.#clientPromise ??= (async () => {
|
|
947
|
+
const client = this.#clientFactory !== void 0 ? await this.#clientFactory(this.#connection) : await createDefaultAlbyNwcClient(this.#connectionString);
|
|
948
|
+
this.#client = client;
|
|
949
|
+
return client;
|
|
950
|
+
})();
|
|
951
|
+
try {
|
|
952
|
+
return await this.#clientPromise;
|
|
953
|
+
} catch (error) {
|
|
954
|
+
this.#clientPromise = void 0;
|
|
955
|
+
throw error;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
function createNwcReceiveClient(options) {
|
|
960
|
+
return new AlbyNwcReceiveClient(options);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/service.ts
|
|
964
|
+
import {
|
|
965
|
+
formatInvalidNwcMessage,
|
|
966
|
+
formatMissingNwcMessage,
|
|
967
|
+
NwcUriParseError,
|
|
968
|
+
OPENRECEIVE_NWC_CODE_HELP_URL,
|
|
969
|
+
OpenReceiveError as OpenReceiveError3,
|
|
970
|
+
parseNwcUri as parseNwcUri2,
|
|
971
|
+
reconcilePaymentAttempts,
|
|
972
|
+
unixSeconds
|
|
973
|
+
} from "@openreceive/core";
|
|
974
|
+
|
|
975
|
+
// src/config-error.ts
|
|
976
|
+
var ConfigError = class extends Error {
|
|
977
|
+
code;
|
|
978
|
+
hint;
|
|
979
|
+
cause;
|
|
980
|
+
constructor(input) {
|
|
981
|
+
super(input.message);
|
|
982
|
+
this.name = "ConfigError";
|
|
983
|
+
this.code = input.code;
|
|
984
|
+
this.hint = input.hint;
|
|
985
|
+
this.cause = input.cause;
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// src/service/file-logger.ts
|
|
990
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync, unlinkSync } from "fs";
|
|
991
|
+
import { appendFile } from "fs/promises";
|
|
992
|
+
import path from "path";
|
|
993
|
+
import { compact as compact3 } from "@openreceive/core";
|
|
994
|
+
|
|
995
|
+
// src/console-logger.ts
|
|
996
|
+
import { compact as compact2 } from "@openreceive/core";
|
|
997
|
+
|
|
998
|
+
// src/log-level.ts
|
|
999
|
+
var LOG_LEVEL_ORDER = {
|
|
1000
|
+
debug: 10,
|
|
1001
|
+
info: 20,
|
|
1002
|
+
warn: 30,
|
|
1003
|
+
error: 40
|
|
1004
|
+
};
|
|
1005
|
+
var LOG_LEVEL_ALIASES = {
|
|
1006
|
+
debug: "debug",
|
|
1007
|
+
info: "info",
|
|
1008
|
+
warn: "warn",
|
|
1009
|
+
warning: "warn",
|
|
1010
|
+
error: "error"
|
|
1011
|
+
};
|
|
1012
|
+
function logLevelOrder(level) {
|
|
1013
|
+
return LOG_LEVEL_ORDER[level];
|
|
1014
|
+
}
|
|
1015
|
+
function parseLogLevel(value) {
|
|
1016
|
+
if (value === void 0 || value === null) return void 0;
|
|
1017
|
+
const normalized = value.trim().toLowerCase();
|
|
1018
|
+
if (normalized.length === 0) return void 0;
|
|
1019
|
+
return LOG_LEVEL_ALIASES[normalized];
|
|
1020
|
+
}
|
|
1021
|
+
function resolveLogLevel(value, fallback = "info") {
|
|
1022
|
+
return parseLogLevel(value) ?? fallback;
|
|
1023
|
+
}
|
|
1024
|
+
function readLogLevelFromEnvironment(env = process.env) {
|
|
1025
|
+
return resolveLogLevel(env.LOG_LEVEL);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/console-logger.ts
|
|
1029
|
+
function createConsoleLogger(options = {}) {
|
|
1030
|
+
const prefix = options.prefix ?? "openreceive";
|
|
1031
|
+
const target = options.console ?? console;
|
|
1032
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1033
|
+
return (entry) => {
|
|
1034
|
+
const minLevel = options.minLevel === void 0 ? readLogLevelFromEnvironment() : resolveLogLevel(options.minLevel);
|
|
1035
|
+
if (logLevelOrder(entry.level) < logLevelOrder(minLevel)) return;
|
|
1036
|
+
const { level, event, message, ...fields } = entry;
|
|
1037
|
+
const method = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "debug" : "info";
|
|
1038
|
+
const sink = target[method] ?? target.log;
|
|
1039
|
+
sink.call(
|
|
1040
|
+
target,
|
|
1041
|
+
formatConsoleLogLine({
|
|
1042
|
+
at: now().toISOString(),
|
|
1043
|
+
level,
|
|
1044
|
+
prefix,
|
|
1045
|
+
event,
|
|
1046
|
+
message
|
|
1047
|
+
}),
|
|
1048
|
+
compact2(fields)
|
|
1049
|
+
);
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function createAppConsoleLogger(options) {
|
|
1053
|
+
const target = options.console ?? console;
|
|
1054
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1055
|
+
return (event, message, fields = {}, level = "info") => {
|
|
1056
|
+
const minLevel = options.minLevel === void 0 ? readLogLevelFromEnvironment() : resolveLogLevel(options.minLevel);
|
|
1057
|
+
if (logLevelOrder(level) < logLevelOrder(minLevel)) return;
|
|
1058
|
+
const method = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "debug" : "info";
|
|
1059
|
+
const sink = target[method] ?? target.log;
|
|
1060
|
+
sink.call(
|
|
1061
|
+
target,
|
|
1062
|
+
formatConsoleLogLine({
|
|
1063
|
+
at: now().toISOString(),
|
|
1064
|
+
level,
|
|
1065
|
+
prefix: options.prefix,
|
|
1066
|
+
event,
|
|
1067
|
+
message
|
|
1068
|
+
}),
|
|
1069
|
+
compact2(fields)
|
|
1070
|
+
);
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function formatConsoleLogLine(input) {
|
|
1074
|
+
return `[${input.at}] ${input.level.toUpperCase()} [${input.prefix}] ${input.event}: ${input.message}`;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/service/file-logger.ts
|
|
1078
|
+
var OPENRECEIVE_LOGGING_DEFAULTS = {
|
|
1079
|
+
enabled: false,
|
|
1080
|
+
directory: "./logs",
|
|
1081
|
+
filename: "openreceive.log",
|
|
1082
|
+
maxFileSizeMb: 10,
|
|
1083
|
+
maxFiles: 5,
|
|
1084
|
+
level: "info"
|
|
1085
|
+
};
|
|
1086
|
+
function createFileLogger(config) {
|
|
1087
|
+
const logPath = path.join(config.directory, config.filename);
|
|
1088
|
+
let initialized = false;
|
|
1089
|
+
let currentBytes = 0;
|
|
1090
|
+
let buffered = [];
|
|
1091
|
+
let draining;
|
|
1092
|
+
let scheduled;
|
|
1093
|
+
let exitHooked = false;
|
|
1094
|
+
const ensureReady = () => {
|
|
1095
|
+
if (initialized) return;
|
|
1096
|
+
initialized = true;
|
|
1097
|
+
mkdirSync(config.directory, { recursive: true });
|
|
1098
|
+
currentBytes = existsSync(logPath) ? statSync(logPath).size : 0;
|
|
1099
|
+
};
|
|
1100
|
+
const rotate = () => {
|
|
1101
|
+
if (config.maxFiles <= 1) {
|
|
1102
|
+
if (existsSync(logPath)) unlinkSync(logPath);
|
|
1103
|
+
currentBytes = 0;
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const oldest = `${logPath}.${config.maxFiles - 1}`;
|
|
1107
|
+
if (existsSync(oldest)) unlinkSync(oldest);
|
|
1108
|
+
for (let index = config.maxFiles - 2; index >= 1; index--) {
|
|
1109
|
+
const from = `${logPath}.${index}`;
|
|
1110
|
+
if (existsSync(from)) renameSync(from, `${logPath}.${index + 1}`);
|
|
1111
|
+
}
|
|
1112
|
+
if (existsSync(logPath)) renameSync(logPath, `${logPath}.1`);
|
|
1113
|
+
currentBytes = 0;
|
|
1114
|
+
};
|
|
1115
|
+
const write = async () => {
|
|
1116
|
+
while (buffered.length > 0) {
|
|
1117
|
+
const lines = buffered;
|
|
1118
|
+
buffered = [];
|
|
1119
|
+
try {
|
|
1120
|
+
ensureReady();
|
|
1121
|
+
let batch = "";
|
|
1122
|
+
for (const line of lines) {
|
|
1123
|
+
const lineBytes = Buffer.byteLength(line);
|
|
1124
|
+
if (currentBytes > 0 && currentBytes + lineBytes > config.maxFileSizeBytes) {
|
|
1125
|
+
if (batch.length > 0) await appendFile(logPath, batch);
|
|
1126
|
+
batch = "";
|
|
1127
|
+
rotate();
|
|
1128
|
+
}
|
|
1129
|
+
batch += line;
|
|
1130
|
+
currentBytes += lineBytes;
|
|
1131
|
+
}
|
|
1132
|
+
if (batch.length > 0) await appendFile(logPath, batch);
|
|
1133
|
+
} catch {
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
const drain = () => {
|
|
1138
|
+
draining ??= write().finally(() => {
|
|
1139
|
+
draining = void 0;
|
|
1140
|
+
});
|
|
1141
|
+
return draining;
|
|
1142
|
+
};
|
|
1143
|
+
const drainSync = () => {
|
|
1144
|
+
if (buffered.length === 0) return;
|
|
1145
|
+
const lines = buffered;
|
|
1146
|
+
buffered = [];
|
|
1147
|
+
try {
|
|
1148
|
+
ensureReady();
|
|
1149
|
+
appendFileSync(logPath, lines.join(""));
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
const schedule = () => {
|
|
1154
|
+
if (!exitHooked) {
|
|
1155
|
+
exitHooked = true;
|
|
1156
|
+
process.once("exit", drainSync);
|
|
1157
|
+
}
|
|
1158
|
+
if (scheduled !== void 0) return;
|
|
1159
|
+
scheduled = setTimeout(() => {
|
|
1160
|
+
scheduled = void 0;
|
|
1161
|
+
void drain();
|
|
1162
|
+
}, 0);
|
|
1163
|
+
scheduled.unref();
|
|
1164
|
+
};
|
|
1165
|
+
const logger = (entry) => {
|
|
1166
|
+
if (logLevelOrder(entry.level) < logLevelOrder(config.minLevel)) return;
|
|
1167
|
+
const { level: entryLevel, event, message, ...rest } = entry;
|
|
1168
|
+
buffered.push(
|
|
1169
|
+
`${JSON.stringify({
|
|
1170
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1171
|
+
level: entryLevel,
|
|
1172
|
+
event,
|
|
1173
|
+
message,
|
|
1174
|
+
...rest
|
|
1175
|
+
})}
|
|
1176
|
+
`
|
|
1177
|
+
);
|
|
1178
|
+
schedule();
|
|
1179
|
+
};
|
|
1180
|
+
logger.flush = async () => {
|
|
1181
|
+
while (buffered.length > 0 || draining !== void 0) {
|
|
1182
|
+
await drain();
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
return logger;
|
|
1186
|
+
}
|
|
1187
|
+
function createFileLoggerFromConfig(config) {
|
|
1188
|
+
if ((config?.enabled ?? OPENRECEIVE_LOGGING_DEFAULTS.enabled) !== true) return void 0;
|
|
1189
|
+
const defaults = OPENRECEIVE_LOGGING_DEFAULTS;
|
|
1190
|
+
const maxFileSizeMb = config?.maxFileSizeMb ?? defaults.maxFileSizeMb;
|
|
1191
|
+
const maxFiles = config?.maxFiles ?? defaults.maxFiles;
|
|
1192
|
+
return createFileLogger({
|
|
1193
|
+
directory: config?.directory ?? defaults.directory,
|
|
1194
|
+
filename: config?.filename ?? defaults.filename,
|
|
1195
|
+
maxFileSizeBytes: Math.max(1024, Math.round(maxFileSizeMb * 1024 * 1024)),
|
|
1196
|
+
maxFiles: Math.max(1, Math.floor(maxFiles)),
|
|
1197
|
+
minLevel: parseLogLevel(config?.level) ?? readLogLevelFromEnvironment()
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
function composeLoggers(...loggers) {
|
|
1201
|
+
const active = loggers.filter((logger) => logger !== void 0);
|
|
1202
|
+
if (active.length === 0) return void 0;
|
|
1203
|
+
if (active.length === 1) return active[0];
|
|
1204
|
+
return (entry) => {
|
|
1205
|
+
for (const logger of active) {
|
|
1206
|
+
try {
|
|
1207
|
+
logger(entry);
|
|
1208
|
+
} catch {
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
function attachLogging(options) {
|
|
1214
|
+
const fileLogger = createFileLoggerFromConfig(options.logging);
|
|
1215
|
+
const consoleEnabled = options.logging?.console ?? options.logger === void 0;
|
|
1216
|
+
const consoleLogger = consoleEnabled ? createConsoleLogger(
|
|
1217
|
+
compact3({
|
|
1218
|
+
prefix: options.logging?.prefix ?? "openreceive",
|
|
1219
|
+
minLevel: options.logging?.level
|
|
1220
|
+
})
|
|
1221
|
+
) : void 0;
|
|
1222
|
+
const logger = composeLoggers(options.logger, consoleLogger, fileLogger);
|
|
1223
|
+
return logger === void 0 ? options : { ...options, logger };
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/service/checkouts.ts
|
|
1227
|
+
import { OPENRECEIVE_NWC_METADATA_MAX_BYTES as OPENRECEIVE_NWC_METADATA_MAX_BYTES2 } from "@openreceive/core";
|
|
1228
|
+
|
|
1229
|
+
// src/service/requests.ts
|
|
1230
|
+
import { isBitcoinAmountCurrency, isRecord as isRecord2, nonEmptyString as nonEmptyString2 } from "@openreceive/core";
|
|
1231
|
+
|
|
1232
|
+
// src/service/core-utils.ts
|
|
1233
|
+
import { isRecord } from "@openreceive/core";
|
|
1234
|
+
var ServiceError = class extends Error {
|
|
1235
|
+
status;
|
|
1236
|
+
code;
|
|
1237
|
+
body;
|
|
1238
|
+
constructor(status, body) {
|
|
1239
|
+
super(body.message);
|
|
1240
|
+
this.name = "ServiceError";
|
|
1241
|
+
this.status = status;
|
|
1242
|
+
this.code = body.code;
|
|
1243
|
+
this.body = body;
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
function asRecord(value) {
|
|
1247
|
+
if (value === void 0) return {};
|
|
1248
|
+
if (!isRecord(value)) {
|
|
1249
|
+
throw serviceError(400, "INVALID_REQUEST", "Input must be an object.");
|
|
1250
|
+
}
|
|
1251
|
+
return value;
|
|
1252
|
+
}
|
|
1253
|
+
function parseOptionalRecord(value, field) {
|
|
1254
|
+
if (value === void 0) return void 0;
|
|
1255
|
+
if (!isRecord(value)) {
|
|
1256
|
+
throw serviceError(400, "INVALID_REQUEST", `${field} must be a JSON object.`);
|
|
1257
|
+
}
|
|
1258
|
+
return value;
|
|
1259
|
+
}
|
|
1260
|
+
function serviceError(status, code, message, options = {}) {
|
|
1261
|
+
return new ServiceError(status, {
|
|
1262
|
+
code,
|
|
1263
|
+
message,
|
|
1264
|
+
...options.retryable === void 0 ? {} : { retryable: options.retryable }
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/service/requests.ts
|
|
1269
|
+
function createAmountRequest(amount) {
|
|
1270
|
+
if ("sats" in amount) {
|
|
1271
|
+
return { amount: { currency: "SATS", value: normalizeSatsValue(amount.sats) } };
|
|
1272
|
+
}
|
|
1273
|
+
if (isBitcoinAmountCurrency(amount.currency)) {
|
|
1274
|
+
return { amount: { currency: amount.currency, value: amount.value } };
|
|
1275
|
+
}
|
|
1276
|
+
return { fiat: { currency: amount.currency, value: amount.value } };
|
|
1277
|
+
}
|
|
1278
|
+
var CREATE_CHECKOUT_REQUEST_FIELDS = [
|
|
1279
|
+
"reference",
|
|
1280
|
+
"amount",
|
|
1281
|
+
"memo",
|
|
1282
|
+
"descriptionHash",
|
|
1283
|
+
"metadata",
|
|
1284
|
+
"expirySeconds"
|
|
1285
|
+
];
|
|
1286
|
+
function normalizeCreateCheckoutRequest(input) {
|
|
1287
|
+
const body = asRecord(input);
|
|
1288
|
+
for (const key of Object.keys(body)) {
|
|
1289
|
+
if (!CREATE_CHECKOUT_REQUEST_FIELDS.includes(key)) {
|
|
1290
|
+
throw serviceError(400, "INVALID_REQUEST", `Unexpected create checkout field: ${key}.`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const reference = nonEmptyString2(body.reference);
|
|
1294
|
+
if (reference === void 0) throw serviceError(400, "INVALID_REQUEST", "reference is required.");
|
|
1295
|
+
if (reference.length > 200) {
|
|
1296
|
+
throw serviceError(400, "INVALID_REQUEST", "reference must be 200 characters or fewer.");
|
|
1297
|
+
}
|
|
1298
|
+
const amount = normalizeCreateCheckoutAmount(body.amount);
|
|
1299
|
+
const memo = nonEmptyString2(body.memo);
|
|
1300
|
+
const descriptionHash = nonEmptyString2(body.descriptionHash);
|
|
1301
|
+
getCreateDescriptionFields({ memo, descriptionHash });
|
|
1302
|
+
const metadata = parseOptionalRecord(body.metadata, "metadata");
|
|
1303
|
+
const expirySeconds = body.expirySeconds;
|
|
1304
|
+
if (expirySeconds !== void 0 && (!Number.isSafeInteger(expirySeconds) || expirySeconds <= 0)) {
|
|
1305
|
+
throw serviceError(400, "INVALID_REQUEST", "expirySeconds must be a positive safe integer.");
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
reference,
|
|
1309
|
+
amount,
|
|
1310
|
+
...memo === void 0 ? {} : { memo },
|
|
1311
|
+
...descriptionHash === void 0 ? {} : { descriptionHash },
|
|
1312
|
+
...metadata === void 0 ? {} : { metadata },
|
|
1313
|
+
...expirySeconds === void 0 ? {} : { expirySeconds }
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function normalizeCreateCheckoutAmount(value) {
|
|
1317
|
+
if (!isRecord2(value)) {
|
|
1318
|
+
throw serviceError(400, "INVALID_REQUEST", "amount must be { sats } or { currency, value }.");
|
|
1319
|
+
}
|
|
1320
|
+
const keys = Object.keys(value);
|
|
1321
|
+
if ("sats" in value && value.sats !== void 0 && keys.every((key) => key === "sats")) {
|
|
1322
|
+
return { sats: normalizeSatsValue(value.sats) };
|
|
1323
|
+
}
|
|
1324
|
+
if (keys.some((key) => key !== "currency" && key !== "value")) {
|
|
1325
|
+
throw serviceError(400, "INVALID_REQUEST", "amount contains unsupported fields.");
|
|
1326
|
+
}
|
|
1327
|
+
const currency = nonEmptyString2(value.currency);
|
|
1328
|
+
const amountValue = nonEmptyString2(value.value);
|
|
1329
|
+
if (currency === void 0 || amountValue === void 0) {
|
|
1330
|
+
throw serviceError(400, "INVALID_REQUEST", "amount must be { sats } or { currency, value }.");
|
|
1331
|
+
}
|
|
1332
|
+
if (!isBitcoinAmountCurrency(currency) && !/^[A-Z]{3}$/.test(currency)) {
|
|
1333
|
+
throw serviceError(
|
|
1334
|
+
400,
|
|
1335
|
+
"INVALID_REQUEST",
|
|
1336
|
+
"amount.currency must be uppercase ISO 4217, BTC, SAT, or SATS."
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
if (!/^[0-9]+(?:\.[0-9]+)?$/.test(amountValue) || /^0+(?:\.0+)?$/.test(amountValue)) {
|
|
1340
|
+
throw serviceError(400, "INVALID_REQUEST", "amount.value must be a positive decimal string.");
|
|
1341
|
+
}
|
|
1342
|
+
return { currency, value: amountValue };
|
|
1343
|
+
}
|
|
1344
|
+
function normalizeSatsValue(value) {
|
|
1345
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return String(value);
|
|
1346
|
+
if (typeof value === "string" && /^[0-9]+$/.test(value) && BigInt(value) > 0n) return value;
|
|
1347
|
+
throw serviceError(400, "INVALID_REQUEST", "sats must be a positive integer.");
|
|
1348
|
+
}
|
|
1349
|
+
function getCreateDescriptionFields(input) {
|
|
1350
|
+
const memo = nonEmptyString2(input.memo);
|
|
1351
|
+
const descriptionHash = nonEmptyString2(input.descriptionHash);
|
|
1352
|
+
if (memo !== void 0 && memo.length > 500) {
|
|
1353
|
+
throw serviceError(400, "INVALID_REQUEST", "memo must be 500 characters or fewer.");
|
|
1354
|
+
}
|
|
1355
|
+
if (memo !== void 0 && descriptionHash !== void 0) {
|
|
1356
|
+
throw serviceError(400, "INVALID_REQUEST", "Use only one of memo or descriptionHash.");
|
|
1357
|
+
}
|
|
1358
|
+
if (descriptionHash !== void 0 && !HEX_64.test(descriptionHash)) {
|
|
1359
|
+
throw serviceError(400, "INVALID_REQUEST", "descriptionHash must be 64 hex characters.");
|
|
1360
|
+
}
|
|
1361
|
+
return {
|
|
1362
|
+
...memo === void 0 ? {} : { description: memo },
|
|
1363
|
+
...descriptionHash === void 0 ? {} : { description_hash: descriptionHash }
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/service/pricing.ts
|
|
1368
|
+
import {
|
|
1369
|
+
createCachedLivePriceFeed,
|
|
1370
|
+
getBtcFiatRatesWithFallback,
|
|
1371
|
+
isResolvedPriceProvider,
|
|
1372
|
+
OPENRECEIVE_PRICE_FEED_FALLBACK_URL_ENV,
|
|
1373
|
+
OPENRECEIVE_PRICE_FEED_PRIMARY_URL_ENV,
|
|
1374
|
+
nonEmptyString as nonEmptyString3,
|
|
1375
|
+
quoteBitcoinAmountToMsats,
|
|
1376
|
+
quoteFiatToMsatsWithPrice
|
|
1377
|
+
} from "@openreceive/core";
|
|
1378
|
+
async function listRates(context, input = {}) {
|
|
1379
|
+
const currencies = input.currencies === void 0 ? context.priceCurrencies : payerPriceCurrencies(input.currencies);
|
|
1380
|
+
try {
|
|
1381
|
+
for (const currency of currencies) {
|
|
1382
|
+
assertAllowedFiatCurrency(currency, context.priceCurrencies);
|
|
1383
|
+
}
|
|
1384
|
+
const rates = await fetchRatesOrUnavailable({
|
|
1385
|
+
currencies,
|
|
1386
|
+
priceProviders: context.priceProviders
|
|
1387
|
+
});
|
|
1388
|
+
return rates.rates;
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
throw mapPriceError(error);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
async function quoteRates(context, input) {
|
|
1394
|
+
const body = asRecord(input);
|
|
1395
|
+
try {
|
|
1396
|
+
const fiat = parseFiatAmount(body.fiat);
|
|
1397
|
+
assertAllowedFiatCurrency(fiat.currency, context.priceCurrencies);
|
|
1398
|
+
return await quoteFiatAmount({
|
|
1399
|
+
fiat,
|
|
1400
|
+
asOf: context.clock(),
|
|
1401
|
+
priceProviders: context.priceProviders
|
|
1402
|
+
});
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
throw mapPriceError(error);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
function payerPriceCurrencies(rawCurrencies) {
|
|
1408
|
+
try {
|
|
1409
|
+
return normalizePriceCurrencies(rawCurrencies, "currencies");
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
if (error instanceof ConfigError) {
|
|
1412
|
+
throw serviceError(400, "INVALID_REQUEST", error.message);
|
|
1413
|
+
}
|
|
1414
|
+
throw error;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
function readPriceCurrencies(configured) {
|
|
1418
|
+
const rawCurrencies = configured ?? ["USD"];
|
|
1419
|
+
return normalizePriceCurrencies(rawCurrencies, "OpenReceive price currencies");
|
|
1420
|
+
}
|
|
1421
|
+
function normalizePriceCurrencies(rawCurrencies, label) {
|
|
1422
|
+
const currencies = [
|
|
1423
|
+
...new Set(rawCurrencies.map((currency) => currency.trim().toUpperCase()).filter(Boolean))
|
|
1424
|
+
];
|
|
1425
|
+
if (currencies.length === 0) {
|
|
1426
|
+
throw new ConfigError({
|
|
1427
|
+
code: "INVALID_PRICE_CURRENCIES",
|
|
1428
|
+
message: `${label} must include at least one currency.`,
|
|
1429
|
+
hint: "Set priceCurrencies to fiat codes like USD and EUR, or omit it to use USD."
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
for (const currency of currencies) {
|
|
1433
|
+
if (!/^[A-Z]{3}$/.test(currency)) {
|
|
1434
|
+
throw new ConfigError({
|
|
1435
|
+
code: "INVALID_PRICE_CURRENCIES",
|
|
1436
|
+
message: `Invalid ${label} entry: ${currency}.`,
|
|
1437
|
+
hint: "Use three-letter fiat currency codes such as USD or EUR."
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return currencies;
|
|
1442
|
+
}
|
|
1443
|
+
async function resolveCreateAmount(input) {
|
|
1444
|
+
const { body } = input;
|
|
1445
|
+
const hasAmount = body.amount !== void 0;
|
|
1446
|
+
const hasFiat = body.fiat !== void 0;
|
|
1447
|
+
const sourceCount = [hasAmount, hasFiat].filter(Boolean).length;
|
|
1448
|
+
if (sourceCount !== 1) {
|
|
1449
|
+
throw serviceError(
|
|
1450
|
+
400,
|
|
1451
|
+
"INVALID_REQUEST",
|
|
1452
|
+
"Create checkout request requires exactly one of amount or fiat."
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
if (hasAmount) {
|
|
1456
|
+
try {
|
|
1457
|
+
const quote = quoteBitcoinAmountToMsats(parseBitcoinAmount(body.amount));
|
|
1458
|
+
return {
|
|
1459
|
+
amountMsats: quote.amountMsats,
|
|
1460
|
+
amountSource: "amount",
|
|
1461
|
+
fiatQuote: null
|
|
1462
|
+
};
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
if (error instanceof ServiceError) throw error;
|
|
1465
|
+
throw mapPriceError(error);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
try {
|
|
1469
|
+
const fiat = parseFiatAmount(body.fiat);
|
|
1470
|
+
assertAllowedFiatCurrency(fiat.currency, input.priceCurrencies);
|
|
1471
|
+
const quote = await quoteFiatAmount({
|
|
1472
|
+
fiat,
|
|
1473
|
+
asOf: input.now,
|
|
1474
|
+
priceProviders: input.priceProviders
|
|
1475
|
+
});
|
|
1476
|
+
return {
|
|
1477
|
+
amountMsats: quote.amountMsats,
|
|
1478
|
+
amountSource: "fiat",
|
|
1479
|
+
fiatQuote: quote
|
|
1480
|
+
};
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
if (error instanceof ServiceError) throw error;
|
|
1483
|
+
throw mapPriceError(error);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
async function quoteFiatAmount(input) {
|
|
1487
|
+
const rates = await fetchRatesOrUnavailable({
|
|
1488
|
+
currencies: [input.fiat.currency],
|
|
1489
|
+
priceProviders: input.priceProviders
|
|
1490
|
+
});
|
|
1491
|
+
const btcFiatPrice = rates.rates.bitcoin[input.fiat.currency.toLowerCase()];
|
|
1492
|
+
if (btcFiatPrice === void 0) {
|
|
1493
|
+
throw ratesUnavailableError(
|
|
1494
|
+
`price provider ${rates.source} did not return ${input.fiat.currency}`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
return quoteFiatToMsatsWithPrice({
|
|
1498
|
+
fiat: input.fiat,
|
|
1499
|
+
btcFiatPrice,
|
|
1500
|
+
source: rates.source,
|
|
1501
|
+
asOf: input.asOf
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
function assertAllowedFiatCurrency(currency, allowedCurrencies) {
|
|
1505
|
+
if (!allowedCurrencies.includes(currency)) {
|
|
1506
|
+
throw serviceError(
|
|
1507
|
+
400,
|
|
1508
|
+
"INVALID_REQUEST",
|
|
1509
|
+
`fiat.currency must be one of the configured priceCurrencies: ${allowedCurrencies.join(", ")}.`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
async function fetchRatesOrUnavailable(input) {
|
|
1514
|
+
try {
|
|
1515
|
+
return await getBtcFiatRatesForProviders(input);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
if (error instanceof ServiceError) throw error;
|
|
1518
|
+
throw ratesUnavailableError(error instanceof Error ? error.message : void 0);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async function getBtcFiatRatesForProviders(input) {
|
|
1522
|
+
if (input.priceProviders.length === 1) {
|
|
1523
|
+
const [provider] = input.priceProviders;
|
|
1524
|
+
if (isResolvedPriceProvider(provider)) {
|
|
1525
|
+
return await provider.getBtcFiatRatesWithSource(input.currencies);
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
source: provider.source,
|
|
1529
|
+
rates: await provider.getBtcFiatRates(input.currencies)
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
return getBtcFiatRatesWithFallback({
|
|
1533
|
+
currencies: input.currencies,
|
|
1534
|
+
providers: input.priceProviders
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
function createPriceFeed(options) {
|
|
1538
|
+
const overrides = readPriceFeedUrlOverrides(options.env);
|
|
1539
|
+
return createCachedLivePriceFeed({
|
|
1540
|
+
currencies: options.currencies,
|
|
1541
|
+
fetch: options.fetch,
|
|
1542
|
+
clock: options.clock,
|
|
1543
|
+
cacheSeconds: options.cacheSeconds,
|
|
1544
|
+
primaryUrl: overrides.primaryUrl,
|
|
1545
|
+
fallbackUrl: overrides.fallbackUrl
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
function readPriceFeedUrlOverrides(env) {
|
|
1549
|
+
return {
|
|
1550
|
+
primaryUrl: readPriceFeedUrlEnv(OPENRECEIVE_PRICE_FEED_PRIMARY_URL_ENV, env),
|
|
1551
|
+
fallbackUrl: readPriceFeedUrlEnv(OPENRECEIVE_PRICE_FEED_FALLBACK_URL_ENV, env)
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
function readPriceFeedUrlEnv(name, env = globalThis.process?.env) {
|
|
1555
|
+
const value = env?.[name];
|
|
1556
|
+
if (value === void 0 || value.trim().length === 0) return void 0;
|
|
1557
|
+
return value.trim();
|
|
1558
|
+
}
|
|
1559
|
+
function ratesUnavailableError(internalDetail) {
|
|
1560
|
+
const error = serviceError(
|
|
1561
|
+
503,
|
|
1562
|
+
"INTERNAL",
|
|
1563
|
+
"Exchange rates are temporarily unavailable \u2014 please try again in a moment.",
|
|
1564
|
+
{ retryable: true }
|
|
1565
|
+
);
|
|
1566
|
+
if (internalDetail !== void 0) error.cause = new Error(internalDetail);
|
|
1567
|
+
return error;
|
|
1568
|
+
}
|
|
1569
|
+
function mapPriceError(error) {
|
|
1570
|
+
if (error instanceof ServiceError) return error;
|
|
1571
|
+
if (error instanceof RangeError) {
|
|
1572
|
+
return serviceError(400, "INVALID_REQUEST", error.message);
|
|
1573
|
+
}
|
|
1574
|
+
return ratesUnavailableError(error instanceof Error ? error.message : void 0);
|
|
1575
|
+
}
|
|
1576
|
+
function parseFiatAmount(value) {
|
|
1577
|
+
const record = parseOptionalRecord(value, "fiat");
|
|
1578
|
+
if (record === void 0) {
|
|
1579
|
+
throw serviceError(400, "INVALID_REQUEST", "fiat must be a JSON object.");
|
|
1580
|
+
}
|
|
1581
|
+
const currency = nonEmptyString3(record.currency);
|
|
1582
|
+
const amountValue = nonEmptyString3(record.value);
|
|
1583
|
+
if (currency === void 0 || !/^[A-Z]{3}$/.test(currency)) {
|
|
1584
|
+
throw serviceError(400, "INVALID_REQUEST", "fiat.currency must be an ISO 4217 uppercase code");
|
|
1585
|
+
}
|
|
1586
|
+
if (amountValue === void 0) {
|
|
1587
|
+
throw serviceError(400, "INVALID_REQUEST", "fiat.value must be a decimal string");
|
|
1588
|
+
}
|
|
1589
|
+
return {
|
|
1590
|
+
currency,
|
|
1591
|
+
value: amountValue
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
function parseBitcoinAmount(value) {
|
|
1595
|
+
const record = parseOptionalRecord(value, "amount");
|
|
1596
|
+
if (record === void 0) {
|
|
1597
|
+
throw serviceError(400, "INVALID_REQUEST", "amount must be a JSON object.");
|
|
1598
|
+
}
|
|
1599
|
+
const currency = nonEmptyString3(record.currency);
|
|
1600
|
+
const amountValue = nonEmptyString3(record.value);
|
|
1601
|
+
if (currency === void 0 || !["BTC", "SAT", "SATS"].includes(currency)) {
|
|
1602
|
+
throw serviceError(
|
|
1603
|
+
400,
|
|
1604
|
+
"INVALID_REQUEST",
|
|
1605
|
+
"amount.currency must be BTC, SAT, or SATS. Use fiat for price-feed currencies."
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
if (amountValue === void 0) {
|
|
1609
|
+
throw serviceError(400, "INVALID_REQUEST", "amount.value must be a decimal string");
|
|
1610
|
+
}
|
|
1611
|
+
return {
|
|
1612
|
+
currency,
|
|
1613
|
+
value: amountValue
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/service/checkouts.ts
|
|
1618
|
+
var OPENRECEIVE_INVOICE_EXPIRY_SECONDS = 10 * 60;
|
|
1619
|
+
var OPENRECEIVE_INVOICE_EXPIRY_TOLERANCE_SECONDS = 60;
|
|
1620
|
+
async function prepareCheckout(context, input) {
|
|
1621
|
+
const resolved = await resolveCreateAmount({
|
|
1622
|
+
body: createAmountRequest(normalizeCreateCheckoutAmount(input.amount)),
|
|
1623
|
+
now: context.clock(),
|
|
1624
|
+
priceProviders: context.priceProviders,
|
|
1625
|
+
priceCurrencies: context.priceCurrencies
|
|
1626
|
+
});
|
|
1627
|
+
return {
|
|
1628
|
+
amountMsats: resolved.amountMsats,
|
|
1629
|
+
fiatQuote: resolved.fiatQuote
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
async function createCheckout(context, request) {
|
|
1633
|
+
const input = normalizeCreateCheckoutRequest(request);
|
|
1634
|
+
const now = context.clock();
|
|
1635
|
+
const resolved = await resolveCreateAmount({
|
|
1636
|
+
body: createAmountRequest(input.amount),
|
|
1637
|
+
now,
|
|
1638
|
+
priceProviders: context.priceProviders,
|
|
1639
|
+
priceCurrencies: context.priceCurrencies
|
|
1640
|
+
});
|
|
1641
|
+
const expiry = input.expirySeconds ?? OPENRECEIVE_INVOICE_EXPIRY_SECONDS;
|
|
1642
|
+
const metadata = {
|
|
1643
|
+
...input.metadata ?? {},
|
|
1644
|
+
reference: input.reference
|
|
1645
|
+
};
|
|
1646
|
+
if (Buffer.byteLength(JSON.stringify(metadata), "utf8") > OPENRECEIVE_NWC_METADATA_MAX_BYTES2) {
|
|
1647
|
+
throw serviceError(400, "INVALID_REQUEST", "metadata is too large for NIP-47.");
|
|
1648
|
+
}
|
|
1649
|
+
const walletInvoice = await context.options.client.makeInvoice({
|
|
1650
|
+
amount_msats: BigInt(resolved.amountMsats),
|
|
1651
|
+
...getCreateDescriptionFields({
|
|
1652
|
+
memo: input.memo,
|
|
1653
|
+
descriptionHash: input.descriptionHash
|
|
1654
|
+
}),
|
|
1655
|
+
expiry,
|
|
1656
|
+
metadata
|
|
1657
|
+
});
|
|
1658
|
+
const createdAt = walletInvoice.created_at ?? now;
|
|
1659
|
+
const requestedExpiresAt = createdAt + expiry;
|
|
1660
|
+
const expiresAt = walletInvoice.expires_at ?? requestedExpiresAt;
|
|
1661
|
+
const shortfall = requestedExpiresAt - expiresAt;
|
|
1662
|
+
if (Math.abs(expiresAt - requestedExpiresAt) > OPENRECEIVE_INVOICE_EXPIRY_TOLERANCE_SECONDS) {
|
|
1663
|
+
const requiredFloor = input.expirySeconds !== void 0 && shortfall > OPENRECEIVE_INVOICE_EXPIRY_TOLERANCE_SECONDS;
|
|
1664
|
+
emitLog(
|
|
1665
|
+
context.options,
|
|
1666
|
+
requiredFloor ? "error" : "warn",
|
|
1667
|
+
requiredFloor ? "checkout.invoice_expiry.rejected" : "checkout.invoice_expiry.adjusted",
|
|
1668
|
+
requiredFloor ? `The wallet did not honor the required invoice expiry (required ${expiry}s, got ${expiresAt - createdAt}s). Use a wallet whose make_invoice honors expiry.` : `The wallet clamped the requested invoice expiry (requested ${expiry}s, got ${expiresAt - createdAt}s); the wallet's own expiry is recorded on the attempt.`,
|
|
1669
|
+
{
|
|
1670
|
+
reference: input.reference,
|
|
1671
|
+
payment_hash: walletInvoice.payment_hash.toLowerCase(),
|
|
1672
|
+
requested_expiry_seconds: expiry,
|
|
1673
|
+
actual_expiry_seconds: expiresAt - createdAt,
|
|
1674
|
+
tolerance_seconds: OPENRECEIVE_INVOICE_EXPIRY_TOLERANCE_SECONDS
|
|
1675
|
+
}
|
|
1676
|
+
);
|
|
1677
|
+
if (requiredFloor) {
|
|
1678
|
+
throw serviceError(
|
|
1679
|
+
502,
|
|
1680
|
+
"UNSUPPORTED_METHOD",
|
|
1681
|
+
"Error with the backing NWC wallet: it did not honor the requested invoice expiry."
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
reference: input.reference,
|
|
1687
|
+
paymentHash: walletInvoice.payment_hash.toLowerCase(),
|
|
1688
|
+
bolt11: walletInvoice.invoice,
|
|
1689
|
+
amountMsats: toSafeInteger(walletInvoice.amount_msats, "amount_msats"),
|
|
1690
|
+
createdAt,
|
|
1691
|
+
expiresAt,
|
|
1692
|
+
fiatQuote: resolved.fiatQuote
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
function toSafeInteger(value, field) {
|
|
1696
|
+
if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
1697
|
+
throw serviceError(502, "INTERNAL", `${field} is outside the JSON safe integer range.`);
|
|
1698
|
+
}
|
|
1699
|
+
return Number(value);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/service/swaps.ts
|
|
1703
|
+
import { isValidSwapAddressForPayInAsset } from "@openreceive/core";
|
|
1704
|
+
|
|
1705
|
+
// src/swap/state.ts
|
|
1706
|
+
var OPENRECEIVE_SWAP_STATES = {
|
|
1707
|
+
creating_provider_order: {
|
|
1708
|
+
state: "creating_provider_order",
|
|
1709
|
+
label: "Preparing payment address",
|
|
1710
|
+
detail: "Creating a payment address.",
|
|
1711
|
+
phase: "preparing",
|
|
1712
|
+
terminal: false
|
|
1713
|
+
},
|
|
1714
|
+
awaiting_deposit: {
|
|
1715
|
+
state: "awaiting_deposit",
|
|
1716
|
+
label: "Waiting for your payment",
|
|
1717
|
+
detail: "Send exactly the amount shown below.",
|
|
1718
|
+
phase: "awaiting_deposit",
|
|
1719
|
+
terminal: false
|
|
1720
|
+
},
|
|
1721
|
+
confirming: {
|
|
1722
|
+
state: "confirming",
|
|
1723
|
+
label: "Confirming payment",
|
|
1724
|
+
detail: "Your payment was detected and is confirming on-chain.",
|
|
1725
|
+
phase: "processing",
|
|
1726
|
+
terminal: false
|
|
1727
|
+
},
|
|
1728
|
+
exchanging: {
|
|
1729
|
+
state: "exchanging",
|
|
1730
|
+
label: "Converting payment",
|
|
1731
|
+
detail: "Your payment is confirmed and being converted. This usually finishes within a minute.",
|
|
1732
|
+
phase: "processing",
|
|
1733
|
+
terminal: false
|
|
1734
|
+
},
|
|
1735
|
+
paying_invoice: {
|
|
1736
|
+
state: "paying_invoice",
|
|
1737
|
+
label: "Finalizing checkout",
|
|
1738
|
+
detail: "The provider is sending the Lightning payment. This usually takes a few seconds.",
|
|
1739
|
+
phase: "processing",
|
|
1740
|
+
terminal: false
|
|
1741
|
+
},
|
|
1742
|
+
completed: {
|
|
1743
|
+
state: "completed",
|
|
1744
|
+
label: "Finalizing checkout",
|
|
1745
|
+
detail: "The provider is sending the Lightning payment. This usually takes a few seconds.",
|
|
1746
|
+
phase: "settling",
|
|
1747
|
+
terminal: false
|
|
1748
|
+
},
|
|
1749
|
+
expired: {
|
|
1750
|
+
state: "expired",
|
|
1751
|
+
label: "Expired",
|
|
1752
|
+
detail: "No payment was received before the payment window closed.",
|
|
1753
|
+
phase: "terminal",
|
|
1754
|
+
terminal: true
|
|
1755
|
+
},
|
|
1756
|
+
refund_required: {
|
|
1757
|
+
state: "refund_required",
|
|
1758
|
+
label: "Refund needed",
|
|
1759
|
+
detail: "Enter an address you control to request a refund.",
|
|
1760
|
+
phase: "refund",
|
|
1761
|
+
terminal: false
|
|
1762
|
+
},
|
|
1763
|
+
refund_pending: {
|
|
1764
|
+
state: "refund_pending",
|
|
1765
|
+
label: "Refund pending",
|
|
1766
|
+
detail: "Your refund request has been sent.",
|
|
1767
|
+
phase: "refund",
|
|
1768
|
+
terminal: false
|
|
1769
|
+
},
|
|
1770
|
+
refunded: {
|
|
1771
|
+
state: "refunded",
|
|
1772
|
+
label: "Refunded",
|
|
1773
|
+
detail: "The provider reports the refund was sent.",
|
|
1774
|
+
phase: "terminal",
|
|
1775
|
+
terminal: true
|
|
1776
|
+
},
|
|
1777
|
+
attention: {
|
|
1778
|
+
state: "attention",
|
|
1779
|
+
label: "Needs attention",
|
|
1780
|
+
detail: "This payment needs support review.",
|
|
1781
|
+
phase: "attention",
|
|
1782
|
+
terminal: true
|
|
1783
|
+
},
|
|
1784
|
+
failed: {
|
|
1785
|
+
state: "failed",
|
|
1786
|
+
label: "Failed",
|
|
1787
|
+
detail: "This payment address can no longer be used.",
|
|
1788
|
+
phase: "terminal",
|
|
1789
|
+
terminal: true
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
// src/swap/weight-budget.ts
|
|
1794
|
+
import { compact as compact4 } from "@openreceive/core";
|
|
1795
|
+
var SWAP_PROVIDER_WEIGHT_WINDOW_SECONDS = 60;
|
|
1796
|
+
var SWAP_PROVIDER_WEIGHT_SOFT_CAP = 200;
|
|
1797
|
+
var SWAP_PROVIDER_CREATE_WEIGHT_GATE = 150;
|
|
1798
|
+
var SWAP_PROVIDER_CREATE_WEIGHT = 50;
|
|
1799
|
+
var SWAP_PROVIDER_DEFAULT_WEIGHT = 1;
|
|
1800
|
+
var SWAP_PROVIDER_WEIGHT_BACKOFF_SECONDS = 60;
|
|
1801
|
+
var SwapProviderWeightBudget = class {
|
|
1802
|
+
constructor(providerId, now) {
|
|
1803
|
+
this.providerId = providerId;
|
|
1804
|
+
this.now = now;
|
|
1805
|
+
this.#windowStart = now();
|
|
1806
|
+
}
|
|
1807
|
+
providerId;
|
|
1808
|
+
now;
|
|
1809
|
+
#windowStart;
|
|
1810
|
+
#used = 0;
|
|
1811
|
+
#backoffUntil;
|
|
1812
|
+
weightForPath(path2) {
|
|
1813
|
+
return path2 === "create" ? SWAP_PROVIDER_CREATE_WEIGHT : SWAP_PROVIDER_DEFAULT_WEIGHT;
|
|
1814
|
+
}
|
|
1815
|
+
async reserve(path2) {
|
|
1816
|
+
this.#rollWindow();
|
|
1817
|
+
const now = this.now();
|
|
1818
|
+
const cost = this.weightForPath(path2);
|
|
1819
|
+
const gate = this.#gate(path2);
|
|
1820
|
+
if (this.#backoffUntil !== void 0 && this.#backoffUntil > now) {
|
|
1821
|
+
this.#deny(
|
|
1822
|
+
path2,
|
|
1823
|
+
"backoff",
|
|
1824
|
+
cost,
|
|
1825
|
+
gate,
|
|
1826
|
+
`Swap provider API is in backoff until ${this.#backoffUntil}.`
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
if (this.#used + cost > gate) {
|
|
1830
|
+
this.#deny(
|
|
1831
|
+
path2,
|
|
1832
|
+
"exhausted",
|
|
1833
|
+
cost,
|
|
1834
|
+
gate,
|
|
1835
|
+
`Swap provider API weight budget exhausted (${this.#used}+${cost} > ${gate}).`
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
this.#used += cost;
|
|
1839
|
+
}
|
|
1840
|
+
async markRateLimited() {
|
|
1841
|
+
const now = this.now();
|
|
1842
|
+
this.#used = Math.max(this.#used, SWAP_PROVIDER_WEIGHT_SOFT_CAP);
|
|
1843
|
+
this.#backoffUntil = now + SWAP_PROVIDER_WEIGHT_BACKOFF_SECONDS;
|
|
1844
|
+
}
|
|
1845
|
+
// The weight window rolls; the 429 backoff does NOT ride along with it.
|
|
1846
|
+
// Clearing it here cut a backoff arbitrarily short — markRateLimited() at
|
|
1847
|
+
// second 59 of the window was forgiven one second later — so the backoff
|
|
1848
|
+
// expires on its own clock, checked in reserve().
|
|
1849
|
+
#rollWindow() {
|
|
1850
|
+
const now = this.now();
|
|
1851
|
+
if (now - this.#windowStart < SWAP_PROVIDER_WEIGHT_WINDOW_SECONDS) return;
|
|
1852
|
+
this.#windowStart = now;
|
|
1853
|
+
this.#used = 0;
|
|
1854
|
+
}
|
|
1855
|
+
#gate(path2) {
|
|
1856
|
+
return path2 === "create" ? SWAP_PROVIDER_CREATE_WEIGHT_GATE : SWAP_PROVIDER_WEIGHT_SOFT_CAP;
|
|
1857
|
+
}
|
|
1858
|
+
// The denial carries its own diagnostics on the thrown error: there is no
|
|
1859
|
+
// observer hook, because there was never a caller for one — the service maps
|
|
1860
|
+
// a weight-budget throw to a retryable 503 and logs it there.
|
|
1861
|
+
#deny(path2, reason, cost, gate, message) {
|
|
1862
|
+
throw weightBudgetError(
|
|
1863
|
+
message,
|
|
1864
|
+
compact4({
|
|
1865
|
+
provider: this.providerId,
|
|
1866
|
+
path: path2,
|
|
1867
|
+
reason,
|
|
1868
|
+
message,
|
|
1869
|
+
used: this.#used,
|
|
1870
|
+
cost,
|
|
1871
|
+
gate,
|
|
1872
|
+
window_start: this.#windowStart,
|
|
1873
|
+
backoff_until: this.#backoffUntil
|
|
1874
|
+
})
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
function weightBudgetError(message, denial) {
|
|
1879
|
+
const error = new Error(message);
|
|
1880
|
+
Object.defineProperty(error, "weightBudget", { value: true });
|
|
1881
|
+
Object.defineProperty(error, "denial", { value: denial, enumerable: true });
|
|
1882
|
+
return error;
|
|
1883
|
+
}
|
|
1884
|
+
function isSwapProviderWeightBudgetError(error) {
|
|
1885
|
+
return error instanceof Error && "weightBudget" in error && error.weightBudget === true;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// src/swap/transport-error.ts
|
|
1889
|
+
function isProviderApiError(error) {
|
|
1890
|
+
if (typeof error !== "object" || error === null) return false;
|
|
1891
|
+
const candidate = error;
|
|
1892
|
+
return typeof candidate.kind === "string" && candidate.name === "FixedFloatApiError";
|
|
1893
|
+
}
|
|
1894
|
+
function classifySwapTransportFailure(error) {
|
|
1895
|
+
if (isSwapProviderWeightBudgetError(error)) return "rate_limited";
|
|
1896
|
+
if (!isProviderApiError(error)) return void 0;
|
|
1897
|
+
if (error.kind === "rate_limited" || error.status === 429) return "rate_limited";
|
|
1898
|
+
if (error.kind === "timeout" || error.kind === "network" || error.kind === "invalid_json" || error.status !== void 0 && error.status >= 500) {
|
|
1899
|
+
return "unreachable";
|
|
1900
|
+
}
|
|
1901
|
+
return "refused";
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// src/service/swaps.ts
|
|
1905
|
+
async function listSwapOptions(context, input) {
|
|
1906
|
+
const providers = context.swapProviders;
|
|
1907
|
+
if (providers.length === 0) {
|
|
1908
|
+
emitLog(
|
|
1909
|
+
context.options,
|
|
1910
|
+
"debug",
|
|
1911
|
+
"swap.options.resolved",
|
|
1912
|
+
"No swap providers configured; payment methods are Lightning-only.",
|
|
1913
|
+
{ option_count: 0, available_count: 0 }
|
|
1914
|
+
);
|
|
1915
|
+
return { enabled: false, options: [] };
|
|
1916
|
+
}
|
|
1917
|
+
const amountMsats = parseAmountMsats(input.amountMsats);
|
|
1918
|
+
const providerCatalog = await resolveSwapProviderCatalog(context, providers);
|
|
1919
|
+
const catalogUnreachable = providerCatalog.size === 0;
|
|
1920
|
+
const options = listSwapAssetInfo().map(
|
|
1921
|
+
(asset) => swapCatalogOption({
|
|
1922
|
+
asset,
|
|
1923
|
+
amountMsats,
|
|
1924
|
+
catalogUnreachable,
|
|
1925
|
+
providerAsset: providerCatalog.get(asset.pay_in_asset)
|
|
1926
|
+
})
|
|
1927
|
+
);
|
|
1928
|
+
const unavailableOptions = options.filter((option) => !option.available);
|
|
1929
|
+
emitLog(
|
|
1930
|
+
context.options,
|
|
1931
|
+
"debug",
|
|
1932
|
+
"swap.options.resolved",
|
|
1933
|
+
"Resolved swap pay options with availability.",
|
|
1934
|
+
{
|
|
1935
|
+
amount_msats: amountMsats,
|
|
1936
|
+
option_count: options.length,
|
|
1937
|
+
available_count: options.length - unavailableOptions.length,
|
|
1938
|
+
unavailable_count: unavailableOptions.length,
|
|
1939
|
+
pay_in_assets: options.map((option) => option.payInAsset),
|
|
1940
|
+
unavailable: unavailableOptions.map((option) => ({
|
|
1941
|
+
pay_in_asset: option.payInAsset,
|
|
1942
|
+
reason: option.unavailableReason
|
|
1943
|
+
}))
|
|
1944
|
+
}
|
|
1945
|
+
);
|
|
1946
|
+
return { enabled: true, options };
|
|
1947
|
+
}
|
|
1948
|
+
async function quoteSwap(context, input) {
|
|
1949
|
+
const amount = normalizeCreateCheckoutAmount(input.amount);
|
|
1950
|
+
const payInAsset = parsePayInAsset(input.payInAsset);
|
|
1951
|
+
const resolved = await resolveCreateAmount({
|
|
1952
|
+
body: createAmountRequest(amount),
|
|
1953
|
+
now: context.clock(),
|
|
1954
|
+
priceProviders: context.priceProviders,
|
|
1955
|
+
priceCurrencies: context.priceCurrencies
|
|
1956
|
+
});
|
|
1957
|
+
const provider = await selectProvider(context, payInAsset);
|
|
1958
|
+
const quote = await provider.quote({ payInAsset, invoiceAmountMsats: resolved.amountMsats });
|
|
1959
|
+
return {
|
|
1960
|
+
provider: quote.provider,
|
|
1961
|
+
payAsset: quote.pay_asset,
|
|
1962
|
+
available: quote.available,
|
|
1963
|
+
...quote.pay_amount === void 0 ? {} : { payAmount: quote.pay_amount },
|
|
1964
|
+
...quote.minimum_pay_amount === void 0 ? {} : { minimumPayAmount: quote.minimum_pay_amount },
|
|
1965
|
+
...quote.maximum_pay_amount === void 0 ? {} : { maximumPayAmount: quote.maximum_pay_amount },
|
|
1966
|
+
...quote.minimum_invoice_amount_msats === void 0 ? {} : { minimumInvoiceAmountMsats: quote.minimum_invoice_amount_msats },
|
|
1967
|
+
...quote.maximum_invoice_amount_msats === void 0 ? {} : { maximumInvoiceAmountMsats: quote.maximum_invoice_amount_msats },
|
|
1968
|
+
...quote.unavailable_reason === void 0 ? {} : { unavailableReason: quote.unavailable_reason },
|
|
1969
|
+
...quote.unavailable_message === void 0 ? {} : { unavailableMessage: quote.unavailable_message }
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
async function throughProvider(operation) {
|
|
1973
|
+
try {
|
|
1974
|
+
return await operation();
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
const failure = classifySwapTransportFailure(error);
|
|
1977
|
+
if (failure === void 0) throw error;
|
|
1978
|
+
if (failure === "refused") {
|
|
1979
|
+
throw serviceError(502, "INTERNAL", "The swap provider refused this request.");
|
|
1980
|
+
}
|
|
1981
|
+
throw serviceError(
|
|
1982
|
+
503,
|
|
1983
|
+
"INTERNAL",
|
|
1984
|
+
failure === "rate_limited" ? "The swap provider is rate limited. Please try again shortly." : "The swap provider is temporarily unreachable. Please try again shortly.",
|
|
1985
|
+
{ retryable: true }
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
async function createSwap(context, input) {
|
|
1990
|
+
const payInAsset = parsePayInAsset(input.payInAsset);
|
|
1991
|
+
const provider = await selectProvider(context, payInAsset);
|
|
1992
|
+
const expirySeconds = provider.invoiceExpirySeconds?.({ payInAsset });
|
|
1993
|
+
const { payInAsset: _payInAsset, ...checkoutRequest } = input;
|
|
1994
|
+
const checkout = await createCheckout(context, { ...checkoutRequest, expirySeconds });
|
|
1995
|
+
const order = await throughProvider(
|
|
1996
|
+
() => provider.createSwap({
|
|
1997
|
+
payInAsset,
|
|
1998
|
+
bolt11: checkout.bolt11,
|
|
1999
|
+
invoiceAmountMsats: checkout.amountMsats
|
|
2000
|
+
})
|
|
2001
|
+
);
|
|
2002
|
+
const swapData = {
|
|
2003
|
+
version: 1,
|
|
2004
|
+
providerOrder: recoveryOrder(order)
|
|
2005
|
+
};
|
|
2006
|
+
return {
|
|
2007
|
+
checkout,
|
|
2008
|
+
swapData,
|
|
2009
|
+
...publicSwap(order, checkout.paymentHash, checkout.reference)
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
async function getSwap(context, input) {
|
|
2013
|
+
const recovery = parseSwapData(input.swapData);
|
|
2014
|
+
const paymentHash = parsePaymentHash(input.paymentHash);
|
|
2015
|
+
const reference = parseReference(input.reference);
|
|
2016
|
+
const provider = requireProvider(context, recovery.providerOrder.provider);
|
|
2017
|
+
const current = await throughProvider(() => provider.getStatus(recovery.providerOrder));
|
|
2018
|
+
return publicSwap(current, paymentHash, reference);
|
|
2019
|
+
}
|
|
2020
|
+
async function refundSwap(context, input) {
|
|
2021
|
+
const recovery = parseSwapData(input.swapData);
|
|
2022
|
+
const paymentHash = parsePaymentHash(input.paymentHash);
|
|
2023
|
+
const reference = parseReference(input.reference);
|
|
2024
|
+
const refundAddress = parseRefundAddress(
|
|
2025
|
+
input.refundAddress,
|
|
2026
|
+
recovery.providerOrder.pay_in_asset
|
|
2027
|
+
);
|
|
2028
|
+
const provider = requireProvider(context, recovery.providerOrder.provider);
|
|
2029
|
+
const current = await throughProvider(() => provider.getStatus(recovery.providerOrder));
|
|
2030
|
+
if (current.state !== "refund_required") {
|
|
2031
|
+
throw serviceError(
|
|
2032
|
+
409,
|
|
2033
|
+
"CONFLICT",
|
|
2034
|
+
`Swap cannot be refunded from provider state ${current.state}.`
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
await throughProvider(() => provider.requestRefund(current, refundAddress));
|
|
2038
|
+
const refreshed = await throughProvider(() => provider.getStatus(current));
|
|
2039
|
+
return publicSwap(refreshed, paymentHash, reference);
|
|
2040
|
+
}
|
|
2041
|
+
async function selectProvider(context, payInAsset) {
|
|
2042
|
+
const providers = context.swapProviders;
|
|
2043
|
+
if (providers.length === 0) {
|
|
2044
|
+
throw serviceError(503, "INTERNAL", `No configured swap provider supports ${payInAsset}.`);
|
|
2045
|
+
}
|
|
2046
|
+
for (let index = 0; index < providers.length; index += 1) {
|
|
2047
|
+
const provider = providers[index];
|
|
2048
|
+
if (provider === void 0) continue;
|
|
2049
|
+
try {
|
|
2050
|
+
const supported = await provider.supportedPayInAssets();
|
|
2051
|
+
if (supported.has(payInAsset)) {
|
|
2052
|
+
if (index > 0) {
|
|
2053
|
+
emitLog(
|
|
2054
|
+
context.options,
|
|
2055
|
+
"warn",
|
|
2056
|
+
"swap.provider.failover",
|
|
2057
|
+
"Primary swap provider unavailable; using backup.",
|
|
2058
|
+
{
|
|
2059
|
+
provider: provider.name,
|
|
2060
|
+
pay_in_asset: payInAsset,
|
|
2061
|
+
failed_providers: providers.slice(0, index).map((entry) => entry.name)
|
|
2062
|
+
}
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
return provider;
|
|
2066
|
+
}
|
|
2067
|
+
throw serviceError(503, "INTERNAL", `No configured swap provider supports ${payInAsset}.`);
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
if (error instanceof ServiceError) throw error;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
throw serviceError(503, "INTERNAL", `No configured swap provider supports ${payInAsset}.`);
|
|
2073
|
+
}
|
|
2074
|
+
function requireProvider(context, name) {
|
|
2075
|
+
const provider = context.swapProviders.find((candidate) => candidate.name === name);
|
|
2076
|
+
if (provider === void 0) {
|
|
2077
|
+
throw serviceError(503, "INTERNAL", `Swap provider ${name} is not configured.`);
|
|
2078
|
+
}
|
|
2079
|
+
return provider;
|
|
2080
|
+
}
|
|
2081
|
+
function parseSwapData(value) {
|
|
2082
|
+
const payload = value;
|
|
2083
|
+
if (payload?.version !== 1 || typeof payload.providerOrder !== "object" || payload.providerOrder === null || typeof payload.providerOrder.provider !== "string" || payload.providerOrder.provider.length === 0 || typeof payload.providerOrder.provider_order_id !== "string" || payload.providerOrder.provider_order_id.length === 0) {
|
|
2084
|
+
throw serviceError(400, "INVALID_REQUEST", "swapData is invalid.");
|
|
2085
|
+
}
|
|
2086
|
+
return payload;
|
|
2087
|
+
}
|
|
2088
|
+
function recoveryOrder(order) {
|
|
2089
|
+
const { raw: _raw, ...safe } = order;
|
|
2090
|
+
return structuredClone(safe);
|
|
2091
|
+
}
|
|
2092
|
+
function publicSwap(order, paymentHash, reference) {
|
|
2093
|
+
return {
|
|
2094
|
+
paymentHash,
|
|
2095
|
+
reference,
|
|
2096
|
+
provider: order.provider,
|
|
2097
|
+
payInAsset: order.pay_in_asset,
|
|
2098
|
+
depositAddress: order.deposit_address,
|
|
2099
|
+
...order.deposit_memo === void 0 ? {} : { depositMemo: order.deposit_memo },
|
|
2100
|
+
depositAmount: order.deposit_amount,
|
|
2101
|
+
providerState: order.state,
|
|
2102
|
+
providerExpiresAt: order.expires_at,
|
|
2103
|
+
...order.deposit_tx_id === void 0 ? {} : { depositTxId: order.deposit_tx_id },
|
|
2104
|
+
...order.payout_tx_id === void 0 ? {} : { payoutTxId: order.payout_tx_id },
|
|
2105
|
+
...order.refund_tx_id === void 0 ? {} : { refundTxId: order.refund_tx_id },
|
|
2106
|
+
...order.refund_reason === void 0 ? {} : { refundReason: order.refund_reason },
|
|
2107
|
+
...order.refund_amount === void 0 ? {} : { refundAmount: order.refund_amount },
|
|
2108
|
+
...order.attention === void 0 ? {} : { attention: order.attention },
|
|
2109
|
+
...order.attention_reason === void 0 ? {} : { attentionReason: order.attention_reason },
|
|
2110
|
+
...order.deposit_received_amount === void 0 ? {} : { depositReceivedAmount: order.deposit_received_amount },
|
|
2111
|
+
...order.emergency_repeat === void 0 ? {} : { emergencyRepeat: order.emergency_repeat },
|
|
2112
|
+
providerOrderId: order.provider_order_id,
|
|
2113
|
+
...order.fee === void 0 ? {} : { fee: order.fee }
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
function parsePaymentHash(value) {
|
|
2117
|
+
const normalized = value.trim().toLowerCase();
|
|
2118
|
+
if (!/^[0-9a-f]{64}$/.test(normalized)) {
|
|
2119
|
+
throw serviceError(400, "INVALID_REQUEST", "paymentHash is invalid.");
|
|
2120
|
+
}
|
|
2121
|
+
return normalized;
|
|
2122
|
+
}
|
|
2123
|
+
function parseReference(value) {
|
|
2124
|
+
const normalized = value.trim();
|
|
2125
|
+
if (normalized.length === 0 || normalized.length > 200) {
|
|
2126
|
+
throw serviceError(400, "INVALID_REQUEST", "reference is invalid.");
|
|
2127
|
+
}
|
|
2128
|
+
return normalized;
|
|
2129
|
+
}
|
|
2130
|
+
function parsePayInAsset(value) {
|
|
2131
|
+
if (!isSwapPayInAsset(value)) {
|
|
2132
|
+
throw serviceError(400, "INVALID_REQUEST", "payInAsset is not supported.");
|
|
2133
|
+
}
|
|
2134
|
+
return value;
|
|
2135
|
+
}
|
|
2136
|
+
function parseRefundAddress(value, payInAsset) {
|
|
2137
|
+
const normalized = value.trim();
|
|
2138
|
+
if (normalized.length === 0 || normalized.length > 300) {
|
|
2139
|
+
throw serviceError(400, "INVALID_REQUEST", "refundAddress is invalid.");
|
|
2140
|
+
}
|
|
2141
|
+
if (typeof payInAsset === "string" && !isValidSwapAddressForPayInAsset(payInAsset, normalized)) {
|
|
2142
|
+
throw serviceError(
|
|
2143
|
+
400,
|
|
2144
|
+
"INVALID_REQUEST",
|
|
2145
|
+
`refundAddress is not a valid ${payInAsset} address.`
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
return normalized;
|
|
2149
|
+
}
|
|
2150
|
+
function parseAmountMsats(value) {
|
|
2151
|
+
if (!Number.isSafeInteger(value) || value < 1e3) {
|
|
2152
|
+
throw serviceError(400, "INVALID_REQUEST", "amountMsats must be an integer >= 1000.");
|
|
2153
|
+
}
|
|
2154
|
+
return Math.ceil(value / 1e3) * 1e3;
|
|
2155
|
+
}
|
|
2156
|
+
async function resolveSwapProviderCatalog(context, providers) {
|
|
2157
|
+
for (let index = 0; index < providers.length; index += 1) {
|
|
2158
|
+
const provider = providers[index];
|
|
2159
|
+
if (provider === void 0) continue;
|
|
2160
|
+
let catalog;
|
|
2161
|
+
try {
|
|
2162
|
+
catalog = provider.payInAssetCatalog === void 0 ? Array.from(await provider.supportedPayInAssets(), (payInAsset) => ({
|
|
2163
|
+
pay_asset: payInAsset
|
|
2164
|
+
})) : await provider.payInAssetCatalog();
|
|
2165
|
+
} catch {
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
if (index > 0) {
|
|
2169
|
+
emitLog(
|
|
2170
|
+
context.options,
|
|
2171
|
+
"warn",
|
|
2172
|
+
"swap.provider.failover",
|
|
2173
|
+
"Primary swap provider unavailable; using backup.",
|
|
2174
|
+
{
|
|
2175
|
+
provider: provider.name,
|
|
2176
|
+
failed_providers: providers.slice(0, index).map((entry) => entry.name)
|
|
2177
|
+
}
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
const byAsset = /* @__PURE__ */ new Map();
|
|
2181
|
+
for (const item of catalog) {
|
|
2182
|
+
byAsset.set(item.pay_asset, {
|
|
2183
|
+
...item,
|
|
2184
|
+
provider: provider.name
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
return byAsset;
|
|
2188
|
+
}
|
|
2189
|
+
return /* @__PURE__ */ new Map();
|
|
2190
|
+
}
|
|
2191
|
+
function swapCatalogOption(input) {
|
|
2192
|
+
const { asset, amountMsats, providerAsset } = input;
|
|
2193
|
+
if (providerAsset === void 0) {
|
|
2194
|
+
return {
|
|
2195
|
+
payInAsset: asset.pay_in_asset,
|
|
2196
|
+
label: asset.label,
|
|
2197
|
+
networkLabel: asset.network_label,
|
|
2198
|
+
provider: "",
|
|
2199
|
+
available: false,
|
|
2200
|
+
...input.catalogUnreachable ? {
|
|
2201
|
+
unavailableReason: "provider_unreachable",
|
|
2202
|
+
unavailableMessage: "The swap provider is temporarily unreachable."
|
|
2203
|
+
} : {
|
|
2204
|
+
unavailableReason: "provider_unconfigured",
|
|
2205
|
+
unavailableMessage: "Automated swaps are not configured for this asset."
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
const limitReason = amountMsats > 0 && providerAsset.minimum_invoice_amount_msats !== void 0 && amountMsats < providerAsset.minimum_invoice_amount_msats ? "amount_too_small" : amountMsats > 0 && providerAsset.maximum_invoice_amount_msats !== void 0 && amountMsats > providerAsset.maximum_invoice_amount_msats ? "amount_too_large" : void 0;
|
|
2210
|
+
const unavailableReason = limitReason ?? (providerAsset.available === false ? providerAsset.unavailable_reason : void 0);
|
|
2211
|
+
const unavailableMessage = limitReason === "amount_too_small" ? "This invoice is below the provider minimum." : limitReason === "amount_too_large" ? "This invoice is above the provider maximum." : providerAsset.available === false ? providerAsset.unavailable_message : void 0;
|
|
2212
|
+
return {
|
|
2213
|
+
payInAsset: asset.pay_in_asset,
|
|
2214
|
+
label: asset.label,
|
|
2215
|
+
networkLabel: asset.network_label,
|
|
2216
|
+
provider: providerAsset.provider,
|
|
2217
|
+
available: unavailableReason === void 0 && providerAsset.available !== false,
|
|
2218
|
+
...unavailableReason === void 0 ? {} : { unavailableReason },
|
|
2219
|
+
...unavailableMessage === void 0 ? {} : { unavailableMessage },
|
|
2220
|
+
...providerAsset.minimum_pay_amount === void 0 ? {} : { minimumPayAmount: providerAsset.minimum_pay_amount },
|
|
2221
|
+
...providerAsset.maximum_pay_amount === void 0 ? {} : { maximumPayAmount: providerAsset.maximum_pay_amount },
|
|
2222
|
+
...providerAsset.minimum_invoice_amount_msats === void 0 ? {} : { minimumInvoiceAmountMsats: providerAsset.minimum_invoice_amount_msats },
|
|
2223
|
+
...providerAsset.maximum_invoice_amount_msats === void 0 ? {} : { maximumInvoiceAmountMsats: providerAsset.maximum_invoice_amount_msats }
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
// src/service.ts
|
|
2228
|
+
async function createOpenReceive(supplied = {}) {
|
|
2229
|
+
const environment = supplied.env ?? process.env;
|
|
2230
|
+
const options = attachLogging(supplied);
|
|
2231
|
+
const clock = options.clock ?? unixSeconds;
|
|
2232
|
+
const nwcLogger = createNwcEndpointLogger(options);
|
|
2233
|
+
const allowSpendCapableWallet = options.allowSpendCapableWallet ?? isEnvironmentFlagEnabled(environment.OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC);
|
|
2234
|
+
const client = options.client ?? createNwcReceiveClient({
|
|
2235
|
+
connectionString: requireNwc(options.nwc ?? environment.NWC_URI, {
|
|
2236
|
+
// Name the source the value actually came from, so the error does not
|
|
2237
|
+
// talk about `nwc` when the host only set the env var (or vice versa).
|
|
2238
|
+
subject: options.nwc === void 0 ? "NWC_URI" : "nwc"
|
|
2239
|
+
}),
|
|
2240
|
+
allowSpendCapableWallet,
|
|
2241
|
+
logger: nwcLogger
|
|
2242
|
+
});
|
|
2243
|
+
await preflight(client);
|
|
2244
|
+
const priceCurrencies = readPriceCurrencies(options.priceCurrencies);
|
|
2245
|
+
const priceProviders = options.priceProviders ?? [
|
|
2246
|
+
createPriceFeed({
|
|
2247
|
+
currencies: priceCurrencies,
|
|
2248
|
+
fetch: options.priceFetch,
|
|
2249
|
+
clock,
|
|
2250
|
+
env: environment
|
|
2251
|
+
})
|
|
2252
|
+
];
|
|
2253
|
+
const swapProviders = resolveSwapProviders(
|
|
2254
|
+
options.swap,
|
|
2255
|
+
() => createLscSwapProvidersFromEnvironment(environment, { now: clock })
|
|
2256
|
+
);
|
|
2257
|
+
const nodeOptions = { ...options, client };
|
|
2258
|
+
emitLog(
|
|
2259
|
+
nodeOptions,
|
|
2260
|
+
"debug",
|
|
2261
|
+
"swap.providers.resolved",
|
|
2262
|
+
swapProviders.length === 0 ? "No swap providers configured; automated swaps are disabled." : "Resolved automated swap providers.",
|
|
2263
|
+
{
|
|
2264
|
+
provider_count: swapProviders.length,
|
|
2265
|
+
providers: swapProviders.map((provider) => provider.name)
|
|
2266
|
+
}
|
|
2267
|
+
);
|
|
2268
|
+
const swapCache = new TransientSwapCache(clock);
|
|
2269
|
+
for (const provider of swapProviders) {
|
|
2270
|
+
provider.attachSwapCache?.(swapCache);
|
|
2271
|
+
provider.attachWeightBudget?.(new SwapProviderWeightBudget(provider.name, clock));
|
|
2272
|
+
provider.attachApiRequestLogger?.(
|
|
2273
|
+
(entry) => emitLog(
|
|
2274
|
+
nodeOptions,
|
|
2275
|
+
swapProviderApiLogLevel(entry.path),
|
|
2276
|
+
"swap.provider.request",
|
|
2277
|
+
"Swap provider API request.",
|
|
2278
|
+
summarizeSwapProviderApiRequest(entry)
|
|
2279
|
+
)
|
|
2280
|
+
);
|
|
2281
|
+
provider.attachApiResponseLogger?.(
|
|
2282
|
+
(entry) => emitLog(
|
|
2283
|
+
nodeOptions,
|
|
2284
|
+
swapProviderApiLogLevel(entry.path),
|
|
2285
|
+
"swap.provider.response",
|
|
2286
|
+
"Swap provider API response.",
|
|
2287
|
+
summarizeSwapProviderApiResponse(entry)
|
|
2288
|
+
)
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
const context = {
|
|
2292
|
+
options: nodeOptions,
|
|
2293
|
+
clock,
|
|
2294
|
+
priceProviders,
|
|
2295
|
+
priceCurrencies,
|
|
2296
|
+
swapProviders
|
|
2297
|
+
};
|
|
2298
|
+
const service = {
|
|
2299
|
+
priceCurrencies,
|
|
2300
|
+
prepareCheckout: (input) => prepareCheckout(context, input),
|
|
2301
|
+
createCheckout: (input) => createCheckout(context, input),
|
|
2302
|
+
reconcilePayments: async (input) => {
|
|
2303
|
+
emitLog(
|
|
2304
|
+
nodeOptions,
|
|
2305
|
+
"debug",
|
|
2306
|
+
"payment.reconcile.requested",
|
|
2307
|
+
"Polling NWC wallet to reconcile payment attempts.",
|
|
2308
|
+
{ attempt_count: input.attempts.length }
|
|
2309
|
+
);
|
|
2310
|
+
try {
|
|
2311
|
+
const walks = [];
|
|
2312
|
+
const results = await reconcilePaymentAttempts({
|
|
2313
|
+
client,
|
|
2314
|
+
clock,
|
|
2315
|
+
attempts: input.attempts,
|
|
2316
|
+
until: input.until,
|
|
2317
|
+
overlapSeconds: input.overlapSeconds,
|
|
2318
|
+
maxPages: input.maxPages,
|
|
2319
|
+
onWalk: (walk) => walks.push(walk)
|
|
2320
|
+
});
|
|
2321
|
+
emitLog(
|
|
2322
|
+
nodeOptions,
|
|
2323
|
+
"info",
|
|
2324
|
+
"payment.reconcile.completed",
|
|
2325
|
+
"NWC payment reconciliation completed.",
|
|
2326
|
+
{
|
|
2327
|
+
// Attempts scanned vs hashes decided: results collapse duplicate
|
|
2328
|
+
// hashes and omit any the wallet walk could not reach, so a gap
|
|
2329
|
+
// between these two is how a truncated scan shows up in the log.
|
|
2330
|
+
attempt_count: input.attempts.length,
|
|
2331
|
+
result_count: results.length,
|
|
2332
|
+
settled_count: results.filter((result) => result.status === "settled").length,
|
|
2333
|
+
pending_count: results.filter((result) => result.status === "pending").length,
|
|
2334
|
+
not_found_count: results.filter((result) => result.status === "not_found").length,
|
|
2335
|
+
// All pending attempts share these walks: one creation-time window,
|
|
2336
|
+
// never one wallet call per invoice.
|
|
2337
|
+
list_transactions_walks: walks.length,
|
|
2338
|
+
...walks[0] === void 0 ? {} : { window_from: walks[0].from, window_until: walks[0].until }
|
|
2339
|
+
}
|
|
2340
|
+
);
|
|
2341
|
+
return results;
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
emitLog(
|
|
2344
|
+
nodeOptions,
|
|
2345
|
+
"error",
|
|
2346
|
+
"payment.reconcile.failed",
|
|
2347
|
+
"NWC payment reconciliation failed.",
|
|
2348
|
+
{
|
|
2349
|
+
attempt_count: input.attempts.length,
|
|
2350
|
+
error_message: error instanceof Error ? error.message : String(error)
|
|
2351
|
+
}
|
|
2352
|
+
);
|
|
2353
|
+
throw error;
|
|
2354
|
+
}
|
|
2355
|
+
},
|
|
2356
|
+
subscribeWalletNotifications: async (handler) => {
|
|
2357
|
+
const subscribe = client.subscribeNotifications;
|
|
2358
|
+
if (typeof subscribe !== "function") {
|
|
2359
|
+
throw new OpenReceiveError3({
|
|
2360
|
+
code: "UNSUPPORTED_METHOD",
|
|
2361
|
+
message: "NWC wallet client does not support NWC notifications (subscribeNotifications). Keep polling reconciliation.",
|
|
2362
|
+
retryable: false
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
emitLog(
|
|
2366
|
+
nodeOptions,
|
|
2367
|
+
"debug",
|
|
2368
|
+
"wallet.notifications.subscribe.requested",
|
|
2369
|
+
"Subscribing to NWC-02 payment_received wallet notifications."
|
|
2370
|
+
);
|
|
2371
|
+
const unsubscribe = await subscribe.call(client, (notification) => {
|
|
2372
|
+
emitLog(
|
|
2373
|
+
nodeOptions,
|
|
2374
|
+
"debug",
|
|
2375
|
+
"wallet.notifications.received",
|
|
2376
|
+
"NWC wallet notification received.",
|
|
2377
|
+
{
|
|
2378
|
+
notification_type: notification.type,
|
|
2379
|
+
...notification.payment_hash === void 0 ? {} : { payment_hash: notification.payment_hash }
|
|
2380
|
+
}
|
|
2381
|
+
);
|
|
2382
|
+
handler(notification);
|
|
2383
|
+
});
|
|
2384
|
+
emitLog(
|
|
2385
|
+
nodeOptions,
|
|
2386
|
+
"info",
|
|
2387
|
+
"wallet.notifications.subscribed",
|
|
2388
|
+
"Subscribed to NWC-02 payment_received wallet notifications; polling reconciliation remains the safety net."
|
|
2389
|
+
);
|
|
2390
|
+
return async () => {
|
|
2391
|
+
await unsubscribe();
|
|
2392
|
+
emitLog(
|
|
2393
|
+
nodeOptions,
|
|
2394
|
+
"info",
|
|
2395
|
+
"wallet.notifications.unsubscribed",
|
|
2396
|
+
"Unsubscribed from NWC-02 wallet notifications."
|
|
2397
|
+
);
|
|
2398
|
+
};
|
|
2399
|
+
},
|
|
2400
|
+
quoteSwap: (input) => quoteSwap(context, input),
|
|
2401
|
+
listSwapOptions: (input) => listSwapOptions(context, input),
|
|
2402
|
+
createSwap: (input) => createSwap(context, input),
|
|
2403
|
+
getSwap: (input) => getSwap(context, input),
|
|
2404
|
+
refundSwap: (input) => refundSwap(context, input),
|
|
2405
|
+
listRates: (input) => listRates(context, input),
|
|
2406
|
+
quoteRates: (input) => quoteRates(context, input),
|
|
2407
|
+
async close() {
|
|
2408
|
+
await client.close?.();
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
return service;
|
|
2412
|
+
}
|
|
2413
|
+
function swapProviderApiLogLevel(path2) {
|
|
2414
|
+
const normalized = path2.toLowerCase();
|
|
2415
|
+
if (normalized.includes("rates") || normalized.includes("ccies") || normalized.includes("price")) {
|
|
2416
|
+
return "debug";
|
|
2417
|
+
}
|
|
2418
|
+
return "info";
|
|
2419
|
+
}
|
|
2420
|
+
function requireNwc(value, { subject }) {
|
|
2421
|
+
if (value === void 0 || value.trim().length === 0) {
|
|
2422
|
+
throw new ConfigError({
|
|
2423
|
+
code: "MISSING_NWC",
|
|
2424
|
+
message: formatMissingNwcMessage(),
|
|
2425
|
+
hint: "Set the receive-only connection in NWC_URI or pass nwc explicitly."
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
try {
|
|
2429
|
+
parseNwcUri2(value.trim());
|
|
2430
|
+
} catch (error) {
|
|
2431
|
+
const reason = error instanceof NwcUriParseError ? error.description : "Invalid NWC URI.";
|
|
2432
|
+
throw new ConfigError({
|
|
2433
|
+
code: "INVALID_NWC",
|
|
2434
|
+
message: formatInvalidNwcMessage({ reason, subject }),
|
|
2435
|
+
hint: "Use a receive-only nostr+walletconnect URI from a trusted wallet.",
|
|
2436
|
+
cause: error
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
return value.trim();
|
|
2440
|
+
}
|
|
2441
|
+
function resolveSwapProviders(swap, fromEnvironment) {
|
|
2442
|
+
if (swap?.provider !== void 0) return [swap.provider, ...swap.failoverProviders ?? []];
|
|
2443
|
+
if (swap?.failoverProviders !== void 0 && swap.failoverProviders.length > 0) {
|
|
2444
|
+
throw new TypeError("swap.failoverProviders requires swap.provider.");
|
|
2445
|
+
}
|
|
2446
|
+
return fromEnvironment();
|
|
2447
|
+
}
|
|
2448
|
+
function isEnvironmentFlagEnabled(value) {
|
|
2449
|
+
if (value === void 0) return false;
|
|
2450
|
+
const normalized = value.trim().toLowerCase();
|
|
2451
|
+
if (["1", "true", "yes"].includes(normalized)) return true;
|
|
2452
|
+
if (normalized.length > 0) {
|
|
2453
|
+
console.warn(
|
|
2454
|
+
`[openreceive] Unrecognized OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC value ${JSON.stringify(value)}; treating it as disabled. Use 1/true/yes to enable.`
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
return false;
|
|
2458
|
+
}
|
|
2459
|
+
async function preflight(client) {
|
|
2460
|
+
try {
|
|
2461
|
+
await client.preflight();
|
|
2462
|
+
} catch (cause) {
|
|
2463
|
+
throw new ConfigError({
|
|
2464
|
+
code: "WALLET_PREFLIGHT_FAILED",
|
|
2465
|
+
message: "OpenReceive wallet preflight failed.",
|
|
2466
|
+
hint: `Use a receive-only NWC connection advertising make_invoice and list_transactions. Get one at ${OPENRECEIVE_NWC_CODE_HELP_URL}`,
|
|
2467
|
+
cause
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/require-nwc.ts
|
|
2473
|
+
import {
|
|
2474
|
+
formatInvalidNwcMessage as formatInvalidNwcMessage2,
|
|
2475
|
+
formatMissingNwcMessage as formatMissingNwcMessage2,
|
|
2476
|
+
NwcUriParseError as NwcUriParseError2,
|
|
2477
|
+
parseNwcUri as parseNwcUri3
|
|
2478
|
+
} from "@openreceive/core";
|
|
2479
|
+
function readNwcFromEnvironment(options = {}) {
|
|
2480
|
+
const subject = options.subject ?? "OpenReceive";
|
|
2481
|
+
const value = (options.env ?? process.env).NWC_URI?.trim();
|
|
2482
|
+
if (value === void 0 || value.length === 0) {
|
|
2483
|
+
throw new Error(formatMissingNwcMessage2({ subject }));
|
|
2484
|
+
}
|
|
2485
|
+
try {
|
|
2486
|
+
parseNwcUri3(value);
|
|
2487
|
+
} catch (error) {
|
|
2488
|
+
const reason = error instanceof NwcUriParseError2 ? error.description : "Invalid NWC URI.";
|
|
2489
|
+
throw new Error(formatInvalidNwcMessage2({ reason }));
|
|
2490
|
+
}
|
|
2491
|
+
return value;
|
|
2492
|
+
}
|
|
2493
|
+
export {
|
|
2494
|
+
ConfigError,
|
|
2495
|
+
OPENRECEIVE_SWAP_PAY_IN_ASSETS,
|
|
2496
|
+
OPENRECEIVE_SWAP_STATES,
|
|
2497
|
+
OpenReceiveError4 as OpenReceiveError,
|
|
2498
|
+
ReceiveCheckoutValidationError,
|
|
2499
|
+
ServiceError,
|
|
2500
|
+
WalletPreflightError,
|
|
2501
|
+
createAppConsoleLogger,
|
|
2502
|
+
createNwcReceiveClient,
|
|
2503
|
+
createOpenReceive,
|
|
2504
|
+
createPriceFeed,
|
|
2505
|
+
fixedFloatCompatibleSwapProvider,
|
|
2506
|
+
fixedFloatProvider,
|
|
2507
|
+
formatLscUri,
|
|
2508
|
+
readNwcFromEnvironment,
|
|
2509
|
+
sanitizeEvent
|
|
2510
|
+
};
|