@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.
@@ -0,0 +1,774 @@
1
+ import { WalletCapabilitySummary, ReceiveNwcClient, NwcTransaction, ParsedNwcConnection, RedactedNwcConnection, MakeInvoiceRequest, MakeInvoiceResult, ListTransactionsRequest, ListTransactionsResult, ErrorCode, ErrorBody, RateQuote, SourcedPriceProvider, SimplePriceFetch, PaidPayment, PaymentCheck, BtcFiatRateMapWithSource, MoneyAmount, CachedPriceFeed } from '@openreceive/core';
2
+ export { ErrorBody, ErrorCode, NwcTransaction, OpenReceiveError, PaidPayment, PaymentCheck, RateQuote, ReceiveNwcClient } from '@openreceive/core';
3
+
4
+ /**
5
+ * Everything that reaches a wallet: the compatible-client shape OpenReceive
6
+ * accepts, method dispatch across naming conventions, the lazily loaded
7
+ * @getalby/sdk client, and subscription teardown.
8
+ */
9
+ interface AlbyNwcCompatibleClient {
10
+ getInfo?: () => Promise<unknown>;
11
+ get_info?: () => Promise<unknown>;
12
+ getWalletServiceInfo?: () => Promise<unknown>;
13
+ makeInvoice?: (request: Record<string, unknown>) => Promise<unknown>;
14
+ make_invoice?: (request: Record<string, unknown>) => Promise<unknown>;
15
+ listTransactions?: (request: Record<string, unknown>) => Promise<unknown>;
16
+ list_transactions?: (request: Record<string, unknown>) => Promise<unknown>;
17
+ subscribeNotifications?: (callback: (notification: unknown) => void, notificationTypes?: string[]) => unknown;
18
+ close?: () => Promise<void> | void;
19
+ }
20
+
21
+ /**
22
+ * NWC failure normalization: the wallet-error alias table and the walk that
23
+ * turns any client's thrown shape into a canonical `OpenReceiveError`.
24
+ * Pure functions over already-thrown values — no transport, no wallet calls.
25
+ */
26
+
27
+ type WalletPreflightErrorCode = "missing_required_method" | "spend_capability_advertised" | "unsupported_encryption" | "wallet_unavailable";
28
+ declare class WalletPreflightError extends Error {
29
+ readonly code: WalletPreflightErrorCode;
30
+ readonly summary?: WalletCapabilitySummary;
31
+ constructor(code: WalletPreflightErrorCode, message: string, summary?: WalletCapabilitySummary);
32
+ }
33
+ declare class ReceiveCheckoutValidationError extends Error {
34
+ constructor(message: string);
35
+ }
36
+
37
+ /**
38
+ * NIP-47 request building, request validation, and reply normalization.
39
+ *
40
+ * Everything here is a pure function over a value the wallet already returned,
41
+ * so every accept/degrade/skip rule is testable without a relay. Reply handling
42
+ * is deliberately tolerant per field: the wallet is trusted, and one quirky row
43
+ * must never fail a reconciliation scan.
44
+ */
45
+
46
+ /**
47
+ * Normalized NWC-02 wallet notification. `transaction` is the notification
48
+ * payload normalized exactly like a `list_transactions` row, so a settled
49
+ * `payment_received` can settle its matching pending attempt directly —
50
+ * notifications are authenticated wallet data. Logging still surfaces only
51
+ * the type and the payment hash, never the payload.
52
+ */
53
+ interface NwcWalletNotification {
54
+ readonly type: string;
55
+ readonly payment_hash?: string;
56
+ readonly transaction?: NwcTransaction;
57
+ }
58
+ /**
59
+ * A wallet client that also supports NWC-02 notifications. Declared rather than
60
+ * duck-typed: both concrete clients (Alby, testkit) implement it, and core's
61
+ * ReceiveNwcClient stays the minimum receive contract for custom clients that
62
+ * only poll.
63
+ */
64
+ interface NotifyingReceiveNwcClient extends ReceiveNwcClient {
65
+ subscribeNotifications?(handler: (notification: NwcWalletNotification) => void): Promise<() => Promise<void> | void>;
66
+ }
67
+
68
+ /**
69
+ * The receive-only NWC client OpenReceive drives a wallet with.
70
+ *
71
+ * Only the client itself lives here — the transport, the reply normalization,
72
+ * and the error table are separate modules so each is testable without a relay:
73
+ * - `./nwc/transport.ts` — wallet dispatch and the lazily loaded @getalby/sdk client.
74
+ * - `./nwc/normalize.ts` — request building, validation, reply normalization.
75
+ * - `./nwc/errors.ts` — preflight/validation errors and the wallet-error table.
76
+ */
77
+
78
+ type NwcWalletNotificationHandler = (notification: NwcWalletNotification) => void;
79
+ type NwcNotificationUnsubscribe = () => Promise<void> | void;
80
+ type AlbyNwcClientFactory = (connection: ParsedNwcConnection) => Promise<AlbyNwcCompatibleClient> | AlbyNwcCompatibleClient;
81
+ type NwcEndpointLogLevel = "debug" | "info" | "warn" | "error";
82
+ interface NwcEndpointLogEntry {
83
+ readonly level: NwcEndpointLogLevel;
84
+ readonly event: string;
85
+ readonly message: string;
86
+ readonly [key: string]: unknown;
87
+ }
88
+ /**
89
+ * Server-side hook invoked every time the receive client hits an NWC wallet
90
+ * endpoint (get_info / make_invoice / list_transactions). Structurally
91
+ * compatible with the node service's Logger so the same sink can be
92
+ * reused. The hook must never throw — failures are swallowed so diagnostics can
93
+ * never change receive-checkout behavior.
94
+ */
95
+ type NwcEndpointLogger = (entry: NwcEndpointLogEntry) => void;
96
+ interface AlbyNwcReceiveClientOptions {
97
+ connectionString: string;
98
+ client?: AlbyNwcCompatibleClient;
99
+ clientFactory?: AlbyNwcClientFactory;
100
+ requirePreflight?: boolean;
101
+ logger?: NwcEndpointLogger;
102
+ /**
103
+ * Explicit override: boot even when the connection advertises spend methods
104
+ * such as `pay_invoice` — in `get_info.methods`, or in the kind-13194 info
105
+ * event when the client exposes no `get_info`. Defaults to `false`, which
106
+ * makes preflight fail closed on a spend-capable connection.
107
+ */
108
+ allowSpendCapableWallet?: boolean;
109
+ /**
110
+ * Pause after warning that the info event advertises spend methods (override
111
+ * path only). Defaults to `0`: a library must not stall a host's boot. A CLI
112
+ * with a human at the terminal passes {@link SPEND_CAPABILITY_WARNING_DELAY_MS}
113
+ * so the warning can be read before output scrolls on.
114
+ */
115
+ spendCapabilityWarningDelayMs?: number;
116
+ /** Sink for the spend-capability warning (override path only). Defaults to `console.error`. */
117
+ spendCapabilityWarning?: (message: string) => void;
118
+ }
119
+ declare class AlbyNwcReceiveClient implements ReceiveNwcClient {
120
+ #private;
121
+ /**
122
+ * Safe, loggable view of the connection (wallet pubkey, relays, lud16, and
123
+ * the secret-redacted URI). The parsed connection with the client secret is
124
+ * deliberately private: `console.log(client)` or host error serialization
125
+ * must never leak the secret.
126
+ */
127
+ readonly connection: RedactedNwcConnection;
128
+ constructor(options: AlbyNwcReceiveClientOptions);
129
+ preflight(): Promise<WalletCapabilitySummary>;
130
+ makeInvoice(request: MakeInvoiceRequest): Promise<MakeInvoiceResult>;
131
+ listTransactions(request: ListTransactionsRequest): Promise<ListTransactionsResult>;
132
+ /**
133
+ * Opt-in NWC-02 notification subscription, limited to `payment_received`.
134
+ * Notifications are authenticated wallet data: the handler receives the
135
+ * payload normalized like a `list_transactions` row so a payload satisfying
136
+ * the settlement rule can settle its pending attempt directly, while
137
+ * anything less only wakes reconciliation. Direct settlement assumes the
138
+ * client binds notification decryption to the connection's wallet pubkey
139
+ * (the bundled @getalby/sdk does). Resolves to an unsubscribe function.
140
+ * Throws an OpenReceiveError with code `UNSUPPORTED_METHOD` when the
141
+ * underlying wallet client does not support NWC notifications.
142
+ */
143
+ subscribeNotifications(handler: NwcWalletNotificationHandler): Promise<NwcNotificationUnsubscribe>;
144
+ close(): Promise<void>;
145
+ private ensurePreflight;
146
+ private getClient;
147
+ }
148
+ declare function createNwcReceiveClient(options: AlbyNwcReceiveClientOptions): AlbyNwcReceiveClient;
149
+
150
+ declare const OPENRECEIVE_SWAP_PAY_IN_ASSETS: readonly ["SOL_SOL", "USDT_TRON", "USDT_SOL", "USDC_SOL", "ETH_ETH", "USDT_ETH", "USDC_ETH"];
151
+ type SwapPayInAsset = (typeof OPENRECEIVE_SWAP_PAY_IN_ASSETS)[number];
152
+
153
+ interface SwapCacheResolveOptions<T> {
154
+ readonly refreshSeconds: number;
155
+ readonly maxStaleSeconds: number;
156
+ readonly claimSeconds?: number;
157
+ readonly serveStaleOnFailure?: boolean;
158
+ readonly fetch: () => Promise<T>;
159
+ readonly serialize: (value: T) => string;
160
+ readonly deserialize: (value: string) => T;
161
+ }
162
+ interface TransientSwapCacheOptions {
163
+ readonly warn?: (message: string, fields?: Record<string, unknown>) => void;
164
+ }
165
+ /** Disposable, process-local provider catalog/rate cache. It has no storage adapter. */
166
+ declare class TransientSwapCache {
167
+ #private;
168
+ constructor(clock: () => number, options?: TransientSwapCacheOptions);
169
+ resolve<T>(key: string, options: SwapCacheResolveOptions<T>): Promise<T>;
170
+ }
171
+
172
+ type SwapProviderState = "creating_provider_order" | "awaiting_deposit" | "confirming" | "exchanging" | "paying_invoice" | "completed" | "expired" | "refund_required" | "refund_pending" | "refunded" | "attention" | "failed";
173
+ type SwapAvailabilityReason = "provider_unconfigured" | "amount_too_small" | "amount_too_large" | "pair_temporarily_unavailable" | "region_unsupported" | "provider_rate_limited" | "provider_unreachable";
174
+ /**
175
+ * Why a swap attempt entered the `attention` state and needs human/support review.
176
+ * Every code path that sets `attention: true` records one of these so a dashboard or
177
+ * runbook can branch on the cause instead of a bare boolean. See the "Attention"
178
+ * section of docs/internal/swap-operations.md for the per-reason operator runbook.
179
+ */
180
+ type SwapAttentionReason = "provider_completed_without_wallet_settlement" | "provider_order_creation_stale" | "provider_order_creation_failed" | "provider_order_creation_needs_reconcile" | "provider_reported_emergency" | "provider_status_unrecognized" | "provider_order_expires_after_shadow_invoice";
181
+ /**
182
+ * Why a swap attempt entered the refund path (`refund_required` → `refunded`).
183
+ * Mapped from FixedFloat `emergency.status` (LESS / EXPIRED). Overpay (MORE/OVER)
184
+ * goes to `attention`, not here.
185
+ */
186
+ type SwapRefundReason = "underpaid" | "late_deposit" | "underpaid_and_late";
187
+ interface SwapQuote {
188
+ readonly pay_amount?: string;
189
+ readonly minimum_pay_amount?: string;
190
+ readonly maximum_pay_amount?: string;
191
+ /** Invoice-side (Lightning receive) limits in msats, when the provider reports them. */
192
+ readonly minimum_invoice_amount_msats?: number;
193
+ readonly maximum_invoice_amount_msats?: number;
194
+ readonly pay_asset: SwapPayInAsset;
195
+ readonly available: boolean;
196
+ readonly unavailable_reason?: SwapAvailabilityReason;
197
+ readonly unavailable_message?: string;
198
+ readonly provider: string;
199
+ }
200
+ interface SwapProviderAsset {
201
+ readonly pay_asset: SwapPayInAsset;
202
+ readonly available?: boolean;
203
+ readonly unavailable_reason?: SwapAvailabilityReason;
204
+ readonly unavailable_message?: string;
205
+ readonly minimum_pay_amount?: string;
206
+ readonly maximum_pay_amount?: string;
207
+ readonly minimum_invoice_amount_msats?: number;
208
+ readonly maximum_invoice_amount_msats?: number;
209
+ }
210
+ /**
211
+ * Fiat equivalents that explain why the payer sends more crypto than the cart total.
212
+ * Sourced from the provider's own quote (e.g. FixedFloat `from.usd` / `to.usd`). The
213
+ * swap fee the payer absorbs is `pay_in_fiat` − `payout_fiat` (exchange spread plus
214
+ * network fees, which the provider bakes into the deposit amount). All values are
215
+ * decimal strings so hosts can round-trip them exactly when retaining an audit snapshot.
216
+ */
217
+ interface SwapFee {
218
+ /** Fiat currency the equivalents are expressed in, e.g. "USD". */
219
+ readonly currency: string;
220
+ /** Fiat value of the crypto the payer must send (provider `from.usd`). */
221
+ readonly pay_in_fiat: string;
222
+ /** Fiat value delivered to the merchant — the cart total (provider `to.usd`). */
223
+ readonly payout_fiat: string;
224
+ }
225
+ interface SwapOrder {
226
+ readonly provider: string;
227
+ readonly provider_order_id: string;
228
+ readonly provider_token: string;
229
+ readonly pay_in_asset: SwapPayInAsset;
230
+ readonly deposit_address: string;
231
+ readonly deposit_memo?: string;
232
+ readonly deposit_amount: string;
233
+ readonly expires_at: number;
234
+ readonly state: SwapProviderState;
235
+ readonly deposit_tx_id?: string;
236
+ readonly payout_tx_id?: string;
237
+ readonly refund_tx_id?: string;
238
+ readonly attention?: boolean;
239
+ readonly attention_reason?: SwapAttentionReason;
240
+ /**
241
+ * Why a refund is needed, when the attempt is on the refund path. Mapped from
242
+ * FixedFloat `emergency.status` (LESS / EXPIRED).
243
+ */
244
+ readonly refund_reason?: SwapRefundReason;
245
+ /**
246
+ * Amount actually received on the deposit tx (`from.tx.amount`), when known.
247
+ * Compared with `deposit_amount` to explain underpayment.
248
+ */
249
+ readonly deposit_received_amount?: string;
250
+ /**
251
+ * Provider-reported refund amount excluding the network fee (`back.amount`).
252
+ */
253
+ readonly refund_amount?: string;
254
+ /**
255
+ * FixedFloat `emergency.repeat`: a second deposit hit the same provider order.
256
+ * Extra funds may sit at the provider while the attempt looks like a normal
257
+ * refund/attention path — surface this so operators can reconcile.
258
+ */
259
+ readonly emergency_repeat?: boolean;
260
+ readonly fee?: SwapFee;
261
+ readonly raw?: unknown;
262
+ }
263
+ /**
264
+ * A single raw provider API response, surfaced for server-side observability.
265
+ * Carries the HTTP status and the parsed `{code, msg, data}` envelope. Emitted
266
+ * through the service's sanitizing log sink, so any nested secret (e.g. a
267
+ * FixedFloat order token) is redacted before it reaches a log line.
268
+ */
269
+ interface SwapProviderApiResponseLog {
270
+ readonly provider: string;
271
+ readonly path: string;
272
+ readonly status: number;
273
+ readonly ok: boolean;
274
+ readonly code: unknown;
275
+ readonly msg: unknown;
276
+ readonly data: unknown;
277
+ }
278
+ /**
279
+ * A single outbound provider API request, surfaced for server-side observability
280
+ * alongside {@link SwapProviderApiResponseLog}. Carries the request path and body.
281
+ * Emitted through the service's sanitizing log sink, so any secret in the body
282
+ * (e.g. a FixedFloat order token on status/refund calls) is redacted; provider
283
+ * auth headers are never included here.
284
+ */
285
+ interface SwapProviderApiRequestLog {
286
+ readonly provider: string;
287
+ readonly path: string;
288
+ readonly body: unknown;
289
+ }
290
+ interface SwapProvider {
291
+ readonly name: string;
292
+ /**
293
+ * Attach a disposable process-local cache for provider catalogs and quotes.
294
+ */
295
+ attachSwapCache?(cache: TransientSwapCache): void;
296
+ /**
297
+ * Attach a sink for outbound provider API requests, mirroring
298
+ * {@link attachApiResponseLogger}. The
299
+ * service routes entries through its sanitizing log sink, so secrets in the body
300
+ * are redacted. Providers that make no remote calls may omit this.
301
+ */
302
+ attachApiRequestLogger?(log: (entry: SwapProviderApiRequestLog) => void): void;
303
+ /**
304
+ * Attach a sink for raw provider API responses. The service routes entries through
305
+ * its sanitizing log sink, so nested secrets are redacted. Providers that make no
306
+ * remote calls may omit this.
307
+ */
308
+ attachApiResponseLogger?(log: (entry: SwapProviderApiResponseLog) => void): void;
309
+ /**
310
+ * Attach a process-local request weight guard for this provider.
311
+ * Providers that do not hit a weight-budgeted API may omit this.
312
+ */
313
+ attachWeightBudget?(budget: {
314
+ reserve(path: string): Promise<void>;
315
+ markRateLimited(): Promise<void>;
316
+ }): void;
317
+ supportedPayInAssets(): Promise<Set<SwapPayInAsset>>;
318
+ payInAssetCatalog?(): Promise<readonly SwapProviderAsset[]>;
319
+ invoiceExpirySeconds?(input: {
320
+ readonly payInAsset: SwapPayInAsset;
321
+ }): number;
322
+ quote(input: {
323
+ readonly payInAsset: SwapPayInAsset;
324
+ readonly invoiceAmountMsats: number;
325
+ }): Promise<SwapQuote>;
326
+ createSwap(input: {
327
+ readonly payInAsset: SwapPayInAsset;
328
+ readonly bolt11: string;
329
+ readonly invoiceAmountMsats: number;
330
+ }): Promise<SwapOrder>;
331
+ getStatus(order: SwapOrder): Promise<SwapOrder>;
332
+ requestRefund(order: SwapOrder, refundAddress: string): Promise<void>;
333
+ }
334
+
335
+ declare const LSC_URI_PROTOCOL: "lightning+swapconnect:";
336
+ interface LscConnection {
337
+ readonly uriProtocol: typeof LSC_URI_PROTOCOL;
338
+ readonly baseUrl: string;
339
+ readonly providerId: string;
340
+ readonly key: string;
341
+ readonly secret: string;
342
+ }
343
+ interface FormatLscUriInput {
344
+ readonly baseUrl: string;
345
+ readonly key: string;
346
+ readonly secret: string;
347
+ }
348
+ interface CreateLscSwapProvidersOptions {
349
+ readonly fetch?: typeof globalThis.fetch;
350
+ readonly now?: () => number;
351
+ }
352
+ declare function formatLscUri(input: FormatLscUriInput): string;
353
+
354
+ declare class ServiceError extends Error {
355
+ readonly status: number;
356
+ readonly code: ErrorCode;
357
+ readonly body: ErrorBody;
358
+ constructor(status: number, body: ErrorBody);
359
+ }
360
+
361
+ /**
362
+ * The FixedFloat-compatible SwapProvider: option defaults and validation, the
363
+ * provider id, and the assembly that routes every SwapProvider call through the
364
+ * signed transport, the `/ccies` resolution and XML rates index held in the
365
+ * transient cache, and the order / quote normalizers beside this file.
366
+ */
367
+
368
+ interface FixedFloatProviderOptions {
369
+ readonly key: string;
370
+ readonly secret: string;
371
+ readonly baseUrl?: string;
372
+ readonly lightningCcy?: string;
373
+ readonly fetch?: typeof globalThis.fetch;
374
+ readonly now?: () => number;
375
+ /** TTL for the disposable `/ccies` currency catalog cache. */
376
+ readonly cacheSeconds?: number;
377
+ /**
378
+ * TTL for the process-local public XML rates cache (`/rates/fixed.xml`). Defaults to
379
+ * {@link SWAP_RATES_REFRESH_SECONDS}. Shared only within the current process.
380
+ */
381
+ readonly ratesCacheSeconds?: number;
382
+ readonly requestTimeoutMs?: number;
383
+ readonly invoiceExpirySeconds?: number;
384
+ readonly depositWindowSeconds?: number;
385
+ readonly settlementSlaSeconds?: number;
386
+ readonly invoiceExpiryMarginSeconds?: number;
387
+ }
388
+ interface FixedFloatCompatibleSwapProviderOptions extends FixedFloatProviderOptions {
389
+ readonly id: string;
390
+ }
391
+ declare function fixedFloatProvider(options: FixedFloatProviderOptions): SwapProvider;
392
+ declare function fixedFloatCompatibleSwapProvider(options: FixedFloatCompatibleSwapProviderOptions): SwapProvider;
393
+
394
+ /**
395
+ * Coarse lifecycle bucket for a swap attempt's `provider_state`. Twelve provider
396
+ * states collapse into these seven phases so a UI can branch on "what should the
397
+ * payer see / do now" without hardcoding every state:
398
+ *
399
+ * - `preparing` — the deposit address is still being created.
400
+ * - `awaiting_deposit` — show the deposit address/amount; the payer must send funds.
401
+ * - `processing` — funds seen; the provider is confirming/converting/paying.
402
+ * - `settling` — provider reports done, but OpenReceive has NOT settled the
403
+ * order yet. Never render this as "Paid": the wallet sweep is
404
+ * the settlement authority (see automated-swaps.md).
405
+ * - `refund` — a refund is required, staged, or in flight.
406
+ * - `attention` — needs operator/support review (funds may be stuck).
407
+ * - `terminal` — the attempt is over and will not change (expired/refunded/failed).
408
+ */
409
+ type SwapPhase = "preparing" | "awaiting_deposit" | "processing" | "settling" | "refund" | "attention" | "terminal";
410
+ interface SwapStateInfo {
411
+ /** The provider state this describes. */
412
+ readonly state: SwapProviderState;
413
+ /** Short, payer-facing status label, e.g. "Waiting for your payment". */
414
+ readonly label: string;
415
+ /** One-sentence payer-facing explanation of what is happening. */
416
+ readonly detail: string;
417
+ /** Coarse lifecycle bucket for UI branching. */
418
+ readonly phase: SwapPhase;
419
+ /**
420
+ * Whether the attempt is over and will not transition again. Terminal states stop
421
+ * being polled by the backend; a UI should stop refreshing this attempt.
422
+ */
423
+ readonly terminal: boolean;
424
+ }
425
+ /**
426
+ * The canonical catalog of every swap `provider_state`, its payer-facing copy, its
427
+ * coarse {@link SwapPhase}, and whether it is terminal. This is the single
428
+ * source of truth the built-in checkout element and custom UIs should both read from.
429
+ *
430
+ * Settlement-safety invariant: `completed` is deliberately NON-terminal and lives in
431
+ * the `settling` phase. Provider completion is not payment — OpenReceive only marks an
432
+ * order paid when the wallet sweep sees a settled transaction.
433
+ */
434
+ declare const OPENRECEIVE_SWAP_STATES: Readonly<Record<SwapProviderState, SwapStateInfo>>;
435
+
436
+ type LogLevel = "debug" | "info" | "warn" | "error";
437
+ interface LogEvent {
438
+ readonly level: LogLevel;
439
+ readonly event: string;
440
+ readonly message: string;
441
+ readonly [key: string]: unknown;
442
+ }
443
+ type EventHandler = (event: LogEvent) => void;
444
+ /** Same signature as {@link EventHandler}; kept as the `logger` option's name. */
445
+ type Logger = EventHandler;
446
+ interface LoggingOptions {
447
+ /**
448
+ * Opt into the rotating file logger. Default `false`: a library must not
449
+ * create files in the host's working directory unasked. Console logging is
450
+ * unaffected.
451
+ */
452
+ readonly enabled?: boolean;
453
+ readonly directory?: string;
454
+ readonly filename?: string;
455
+ readonly maxFileSizeMb?: number;
456
+ readonly maxFiles?: number;
457
+ /**
458
+ * Minimum level for the built-in console and file loggers.
459
+ * When omitted, both read `LOG_LEVEL` (`DEBUG`|`INFO`|`WARN`|`ERROR`, default `INFO`).
460
+ */
461
+ readonly level?: LogLevel;
462
+ /**
463
+ * Attach the built-in console logger.
464
+ * Default: `true` when no custom `logger` is supplied; `false` when a custom
465
+ * `logger` is supplied (set `true` to keep both).
466
+ */
467
+ readonly console?: boolean;
468
+ /** Console prefix, e.g. `openreceive:my-app`. Default `openreceive`. */
469
+ readonly prefix?: string;
470
+ }
471
+ interface NodeOptions {
472
+ readonly client: NotifyingReceiveNwcClient;
473
+ readonly priceProviders?: readonly SourcedPriceProvider[];
474
+ readonly priceCurrencies?: readonly string[];
475
+ readonly swap?: SwapOptions;
476
+ readonly onEvent?: EventHandler;
477
+ readonly logger?: Logger;
478
+ readonly logging?: LoggingOptions;
479
+ readonly clock?: () => number;
480
+ }
481
+ interface CreateOpenReceiveOptions extends Omit<NodeOptions, "client"> {
482
+ readonly client?: NotifyingReceiveNwcClient;
483
+ /** Explicit override. Normal applications read the receive-only URI from NWC_URI. */
484
+ readonly nwc?: string;
485
+ /** Environment source for NWC_URI, LSC_URI_PRIMARY, and LSC_URI_BACKUP. Defaults to process.env. */
486
+ readonly env?: Readonly<Record<string, string | undefined>>;
487
+ readonly priceFetch?: SimplePriceFetch;
488
+ /**
489
+ * Explicit override: boot even when the wallet advertises spend methods such
490
+ * as `pay_invoice`. Defaults to `false` (preflight fails closed). Also
491
+ * settable via `OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC=true`.
492
+ */
493
+ readonly allowSpendCapableWallet?: boolean;
494
+ }
495
+ type CreateCheckoutAmount = {
496
+ readonly sats: number | string;
497
+ readonly currency?: never;
498
+ readonly value?: never;
499
+ } | {
500
+ readonly currency: string;
501
+ readonly value: string;
502
+ readonly sats?: never;
503
+ };
504
+ interface CreateCheckoutRequest {
505
+ readonly reference: string;
506
+ readonly amount: CreateCheckoutAmount;
507
+ readonly memo?: string;
508
+ readonly descriptionHash?: string;
509
+ readonly metadata?: Record<string, unknown>;
510
+ /**
511
+ * Requested invoice expiry in seconds (default 600). Ignored on the swap
512
+ * path, where the provider-mandated shadow-invoice expiry always wins.
513
+ */
514
+ readonly expirySeconds?: number;
515
+ }
516
+ /**
517
+ * The minted invoice — the service-level view of the OpenAPI `Checkout` object
518
+ * (the HTTP handler serializes it to the snake_case wire shape,
519
+ * `WireCheckout`).
520
+ */
521
+ interface Checkout {
522
+ readonly reference: string;
523
+ readonly paymentHash: string;
524
+ readonly bolt11: string;
525
+ readonly amountMsats: number;
526
+ readonly createdAt: number;
527
+ readonly expiresAt: number;
528
+ readonly fiatQuote: RateQuote | null;
529
+ }
530
+ interface ReconcilePaymentsRequest {
531
+ readonly attempts: readonly {
532
+ readonly paymentHash: string;
533
+ readonly createdAt: number;
534
+ }[];
535
+ readonly until?: number;
536
+ readonly overlapSeconds?: number;
537
+ /** Page cap per wallet-history walk; bounds request-path (opportunistic) passes. */
538
+ readonly maxPages?: number;
539
+ }
540
+ type NodeSettlementActionInput = PaidPayment;
541
+ type NodeSettlementActionHook = (input: NodeSettlementActionInput) => Promise<void> | void;
542
+ /**
543
+ * One NWC-02 wallet notification: its type, the payment hash when present, and
544
+ * the payload normalized exactly like a `list_transactions` row. Notifications
545
+ * are authenticated wallet data — a `transaction` that satisfies the
546
+ * settlement rule (`settled_at` or a settled state; never a preimage alone)
547
+ * may settle its matching pending attempt directly. Logging only ever surfaces
548
+ * the type and the payment hash.
549
+ */
550
+ interface WalletNotification {
551
+ readonly type: string;
552
+ readonly payment_hash?: string;
553
+ readonly transaction?: NwcTransaction;
554
+ }
555
+ type WalletNotificationHandler = (notification: WalletNotification) => void;
556
+ interface SwapOptions {
557
+ /**
558
+ * The provider every catalog, quote, and create goes to while it answers.
559
+ * Omitted (the normal case), providers come from `LSC_URI_PRIMARY` and
560
+ * `LSC_URI_BACKUP` in `env`.
561
+ */
562
+ readonly provider?: SwapProvider;
563
+ /**
564
+ * Consulted in order only when `provider` (or an earlier failover) throws —
565
+ * a network or API failure. Never used to fill in an asset a healthy
566
+ * provider simply does not list. Requires `provider`.
567
+ */
568
+ readonly failoverProviders?: readonly SwapProvider[];
569
+ }
570
+ interface SwapQuoteRequest {
571
+ readonly amount: CreateCheckoutAmount;
572
+ readonly payInAsset: SwapPayInAsset | string;
573
+ }
574
+ /**
575
+ * Service-level swap quote, camelCase like every other service result (the
576
+ * HTTP handler converts to the snake_case wire shape).
577
+ */
578
+ interface SwapQuoteResult {
579
+ readonly provider: string;
580
+ readonly payAsset: SwapPayInAsset;
581
+ readonly available: boolean;
582
+ readonly payAmount?: string;
583
+ readonly minimumPayAmount?: string;
584
+ readonly maximumPayAmount?: string;
585
+ /** Invoice-side (Lightning receive) limits in msats, when reported. */
586
+ readonly minimumInvoiceAmountMsats?: number;
587
+ readonly maximumInvoiceAmountMsats?: number;
588
+ readonly unavailableReason?: string;
589
+ readonly unavailableMessage?: string;
590
+ }
591
+ interface ListSwapOptionsRequest {
592
+ /** Host-owned invoice amount in millisatoshis. */
593
+ readonly amountMsats: number;
594
+ }
595
+ interface SwapPaymentMethod {
596
+ readonly payInAsset: SwapPayInAsset;
597
+ readonly label: string;
598
+ readonly networkLabel: string;
599
+ readonly provider: string;
600
+ readonly available: boolean;
601
+ readonly unavailableReason?: string;
602
+ readonly unavailableMessage?: string;
603
+ readonly payAmount?: string;
604
+ readonly minimumPayAmount?: string;
605
+ readonly maximumPayAmount?: string;
606
+ readonly minimumInvoiceAmountMsats?: number;
607
+ readonly maximumInvoiceAmountMsats?: number;
608
+ }
609
+ interface ListSwapOptionsResult {
610
+ readonly enabled: boolean;
611
+ readonly options: readonly SwapPaymentMethod[];
612
+ }
613
+ interface CreateSwapRequest extends CreateCheckoutRequest {
614
+ readonly payInAsset: SwapPayInAsset | string;
615
+ }
616
+ interface GetSwapRequest {
617
+ readonly reference: string;
618
+ readonly paymentHash: string;
619
+ readonly swapData: SwapData;
620
+ }
621
+ interface SwapRefundRequest extends GetSwapRequest {
622
+ readonly refundAddress: string;
623
+ }
624
+ /** Server-only provider recovery state persisted by the host application. */
625
+ interface SwapData {
626
+ readonly version: 1;
627
+ readonly providerOrder: SwapOrder;
628
+ }
629
+ interface PublicSwap {
630
+ readonly paymentHash: string;
631
+ readonly reference: string;
632
+ readonly provider: string;
633
+ readonly payInAsset: SwapPayInAsset;
634
+ readonly depositAddress: string;
635
+ readonly depositMemo?: string;
636
+ readonly depositAmount: string;
637
+ readonly providerState: SwapProviderState;
638
+ readonly providerExpiresAt: number;
639
+ readonly depositTxId?: string;
640
+ readonly payoutTxId?: string;
641
+ readonly refundTxId?: string;
642
+ readonly refundReason?: string;
643
+ readonly refundAmount?: string;
644
+ readonly attention?: boolean;
645
+ /** Why the attempt needs an operator, when `attention` is set. */
646
+ readonly attentionReason?: string;
647
+ /**
648
+ * Amount actually received on the deposit transaction, when the provider
649
+ * reports it. The payer UI compares it with `depositAmount` to explain an
650
+ * underpayment ("you sent X but Y was required").
651
+ */
652
+ readonly depositReceivedAmount?: string;
653
+ /**
654
+ * A second deposit hit the same provider order. Extra funds may sit at the
655
+ * provider while the attempt looks like an ordinary refund path.
656
+ */
657
+ readonly emergencyRepeat?: boolean;
658
+ /** Provider-side order reference, shown to the payer for support. */
659
+ readonly providerOrderId?: string;
660
+ /**
661
+ * Fiat equivalents explaining why the payer sends more than the cart total.
662
+ * Never a price authority — the invoice amount is.
663
+ */
664
+ readonly fee?: SwapFee;
665
+ }
666
+ interface SwapCheckout extends PublicSwap {
667
+ readonly checkout: Checkout;
668
+ /** Sensitive host-only state. Never serialize this into a browser response. */
669
+ readonly swapData: SwapData;
670
+ }
671
+ interface ListRatesRequest {
672
+ readonly currencies?: readonly string[];
673
+ }
674
+ interface OpenReceive {
675
+ readonly priceCurrencies: readonly string[];
676
+ /** Resolve amount_msats without minting a Lightning invoice or committing an attempt. */
677
+ prepareCheckout(input: {
678
+ readonly amount: CreateCheckoutAmount;
679
+ }): Promise<{
680
+ readonly amountMsats: number;
681
+ readonly fiatQuote: RateQuote | null;
682
+ }>;
683
+ createCheckout(input: CreateCheckoutRequest): Promise<Checkout>;
684
+ reconcilePayments(input: ReconcilePaymentsRequest): Promise<readonly PaymentCheck[]>;
685
+ /**
686
+ * Opt-in NWC-02 notifications: subscribe to wallet `payment_received`
687
+ * notifications. Notifications are authenticated wallet data — a payload
688
+ * that satisfies the settlement rule settles its matching pending attempt
689
+ * directly; anything less only wakes a batched wallet scan
690
+ * (`reconcilePayments`). Polling remains the safety net for notifications
691
+ * missed while offline. Direct settlement assumes the NWC client binds
692
+ * notification decryption to the connection's wallet pubkey (the bundled
693
+ * SDK does). Resolves to an unsubscribe function; rejects with an
694
+ * OpenReceiveError of code `UNSUPPORTED_METHOD` when the wallet client does
695
+ * not support notifications.
696
+ */
697
+ subscribeWalletNotifications?(handler: WalletNotificationHandler): Promise<() => Promise<void> | void>;
698
+ quoteSwap(input: SwapQuoteRequest): Promise<SwapQuoteResult>;
699
+ listSwapOptions(input: ListSwapOptionsRequest): Promise<ListSwapOptionsResult>;
700
+ createSwap(input: CreateSwapRequest): Promise<SwapCheckout>;
701
+ getSwap(input: GetSwapRequest): Promise<PublicSwap>;
702
+ refundSwap(input: SwapRefundRequest): Promise<PublicSwap>;
703
+ listRates(input?: ListRatesRequest): Promise<BtcFiatRateMapWithSource["rates"]>;
704
+ quoteRates(input: {
705
+ readonly fiat: MoneyAmount;
706
+ }): Promise<RateQuote>;
707
+ close(): Promise<void>;
708
+ }
709
+
710
+ declare function createPriceFeed(options: {
711
+ currencies: readonly string[];
712
+ fetch?: SimplePriceFetch;
713
+ clock?: () => number;
714
+ cacheSeconds?: number;
715
+ /** Environment for the URL override vars. Defaults to process.env. */
716
+ env?: Readonly<Record<string, string | undefined>>;
717
+ }): CachedPriceFeed;
718
+
719
+ type ConfigErrorCode = "MISSING_NWC" | "INVALID_NWC" | "WALLET_PREFLIGHT_FAILED" | "INVALID_PRICE_CURRENCIES";
720
+ declare class ConfigError extends Error {
721
+ readonly code: ConfigErrorCode;
722
+ readonly hint: string;
723
+ readonly cause?: unknown;
724
+ constructor(input: {
725
+ readonly code: ConfigErrorCode;
726
+ readonly message: string;
727
+ readonly hint: string;
728
+ readonly cause?: unknown;
729
+ });
730
+ }
731
+
732
+ declare function createOpenReceive(supplied?: CreateOpenReceiveOptions): Promise<OpenReceive>;
733
+
734
+ declare function sanitizeEvent(entry: LogEvent): LogEvent;
735
+
736
+ interface RequireNwcFromEnvironmentOptions {
737
+ /** Environment object to read. Defaults to `process.env`. */
738
+ readonly env?: Readonly<Record<string, string | undefined>>;
739
+ /** Subject phrase for the missing-NWC message. Default `"OpenReceive"`. */
740
+ readonly subject?: string;
741
+ }
742
+ /**
743
+ * Read and validate the receive-only connection string in `NWC_URI`.
744
+ * Throws an `Error` with a host-facing message when missing or invalid.
745
+ */
746
+ declare function readNwcFromEnvironment(options?: RequireNwcFromEnvironmentOptions): string;
747
+
748
+ interface CreateConsoleLoggerOptions {
749
+ /** Prefix before the event, e.g. `openreceive:my-app`. Default `openreceive`. */
750
+ readonly prefix?: string;
751
+ /**
752
+ * Minimum level to emit. Default: `LOG_LEVEL` from the environment, or `info`.
753
+ * Accepts the same values as `LOG_LEVEL` (`DEBUG` | `INFO` | `WARN` | `ERROR`).
754
+ */
755
+ readonly minLevel?: LogLevel | string;
756
+ readonly console?: Pick<Console, "debug" | "info" | "warn" | "error" | "log">;
757
+ /** Clock for the leading ISO timestamp. Default `() => new Date()`. */
758
+ readonly now?: () => Date;
759
+ }
760
+ type AppConsoleLogger = (event: string, message: string, fields?: Record<string, unknown>, level?: LogLevel) => void;
761
+ interface CreateAppConsoleLoggerOptions {
762
+ /** Prefix before the event, e.g. `hello-fruit:node-express:server`. */
763
+ readonly prefix: string;
764
+ /**
765
+ * Minimum level to emit. Default: `LOG_LEVEL` from the environment, or `info`.
766
+ */
767
+ readonly minLevel?: LogLevel | string;
768
+ readonly console?: Pick<Console, "debug" | "info" | "warn" | "error" | "log">;
769
+ readonly now?: () => Date;
770
+ }
771
+ /** Ad-hoc `(event, message, fields?, level?)` console logger for host app routes. */
772
+ declare function createAppConsoleLogger(options: CreateAppConsoleLoggerOptions): AppConsoleLogger;
773
+
774
+ export { type AlbyNwcReceiveClientOptions, type AppConsoleLogger, type Checkout, ConfigError, type CreateAppConsoleLoggerOptions, type CreateCheckoutAmount, type CreateCheckoutRequest, type CreateConsoleLoggerOptions, type CreateLscSwapProvidersOptions, type CreateOpenReceiveOptions, type CreateSwapRequest, type EventHandler, type FixedFloatCompatibleSwapProviderOptions, type FixedFloatProviderOptions, type FormatLscUriInput, type GetSwapRequest, type ListRatesRequest, type ListSwapOptionsRequest, type ListSwapOptionsResult, type LogEvent, type LogLevel, type Logger, type LoggingOptions, type LscConnection, type NodeSettlementActionHook, type NodeSettlementActionInput, type NwcEndpointLogEntry, type NwcEndpointLogLevel, type NwcEndpointLogger, type NwcNotificationUnsubscribe, OPENRECEIVE_SWAP_PAY_IN_ASSETS, OPENRECEIVE_SWAP_STATES, type OpenReceive, type PublicSwap, ReceiveCheckoutValidationError, type ReconcilePaymentsRequest, type RequireNwcFromEnvironmentOptions, ServiceError, type SwapAttentionReason, type SwapCheckout, type SwapData, type SwapOptions, type SwapOrder, type SwapPayInAsset, type SwapPaymentMethod, type SwapProvider, type SwapProviderAsset, type SwapProviderState, type SwapQuote, type SwapQuoteRequest, type SwapQuoteResult, type SwapRefundRequest, type WalletNotification, type WalletNotificationHandler, WalletPreflightError, type WalletPreflightErrorCode, createAppConsoleLogger, createNwcReceiveClient, createOpenReceive, createPriceFeed, fixedFloatCompatibleSwapProvider, fixedFloatProvider, formatLscUri, readNwcFromEnvironment, sanitizeEvent };