@openreceive/node 0.4.9 → 0.4.11

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.
@@ -2,7 +2,8 @@
2
2
  import {
3
3
  isErrorCode,
4
4
  isRetryableErrorCode,
5
- OpenReceiveError
5
+ OpenReceiveError,
6
+ publicErrorBody
6
7
  } from "@openreceive/core";
7
8
  var WalletPreflightError = class extends Error {
8
9
  code;
@@ -69,7 +70,8 @@ var OPENRECEIVE_ERROR_MESSAGES = {
69
70
  CONFLICT: "NWC wallet service reported a conflicting request."
70
71
  };
71
72
  function normalizeNwcWalletError(error) {
72
- if (error instanceof OpenReceiveError) return error;
73
+ if (error instanceof OpenReceiveError)
74
+ return new OpenReceiveError(publicErrorBody(error.toJSON()));
73
75
  const records = collectErrorRecords(error);
74
76
  const code = knownErrorCode(error) ?? errorCodeFromRecords(records) ?? (typeof error === "string" ? normalizeNwcErrorCode(error) : void 0) ?? "OTHER";
75
77
  const message = errorMessageFromRecords(records, error, code);
@@ -83,7 +85,7 @@ function normalizeNwcWalletError(error) {
83
85
  ...requestId === void 0 ? {} : { request_id: requestId },
84
86
  ...details === void 0 ? {} : { details }
85
87
  };
86
- return new OpenReceiveError(body, { cause: error });
88
+ return new OpenReceiveError(publicErrorBody(body));
87
89
  }
88
90
  function knownErrorCode(error) {
89
91
  if (error instanceof ReceiveCheckoutValidationError) {
@@ -309,7 +311,7 @@ function normalizeMakeInvoiceResult(rawResult) {
309
311
  const expiresAt = parseOptionalInteger(result.expires_at ?? result.expiresAt, "expires_at");
310
312
  return {
311
313
  invoice,
312
- payment_hash: paymentHash,
314
+ payment_hash: paymentHash.toLowerCase(),
313
315
  amount_msats: amountMsats,
314
316
  ...createdAt === void 0 ? {} : { created_at: createdAt },
315
317
  ...expiresAt === void 0 ? {} : { expires_at: expiresAt }
@@ -331,6 +333,10 @@ function normalizeListTransactionsResult(rawResult) {
331
333
  const transactions = [];
332
334
  let skippedRows = 0;
333
335
  for (const rawTransaction of rawTransactions) {
336
+ if (typeof rawTransaction !== "object" || rawTransaction === null || Array.isArray(rawTransaction)) {
337
+ skippedRows += 1;
338
+ continue;
339
+ }
334
340
  try {
335
341
  transactions.push(normalizeNwcTransaction(rawTransaction));
336
342
  } catch {
@@ -354,7 +360,7 @@ function normalizeNwcTransaction(rawTransaction) {
354
360
  if (!/^[0-9a-fA-F]{64}$/.test(paymentHash)) {
355
361
  throw new TypeError("payment_hash must be 64 hexadecimal characters");
356
362
  }
357
- normalized.payment_hash = paymentHash;
363
+ normalized.payment_hash = paymentHash.toLowerCase();
358
364
  }
359
365
  if (result.amount_msats !== void 0 || result.amount !== void 0) {
360
366
  normalized.amount_msats = toBigInt(result.amount_msats ?? result.amount, "amount_msats");
@@ -394,7 +400,7 @@ function normalizeNwcNotification(rawNotification) {
394
400
  }
395
401
  }
396
402
  const rawHash = payload.payment_hash ?? payload.paymentHash ?? record.payment_hash ?? record.paymentHash;
397
- const paymentHash = typeof rawHash === "string" && rawHash.length > 0 ? rawHash : transaction?.payment_hash;
403
+ const paymentHash = typeof rawHash === "string" && rawHash.length > 0 ? rawHash.toLowerCase() : transaction?.payment_hash;
398
404
  return {
399
405
  type,
400
406
  ...paymentHash === void 0 ? {} : { payment_hash: paymentHash },
@@ -485,15 +491,85 @@ function unwrapNwcResult(value) {
485
491
  import { createRequire } from "module";
486
492
  import { pathToFileURL } from "url";
487
493
  import { recordOrEmpty as recordOrEmpty2 } from "@openreceive/core";
494
+
495
+ // src/nwc/history-request.ts
496
+ async function historyRequest(client, params, signal) {
497
+ signal.throwIfAborted();
498
+ const content = await client.encrypt(
499
+ client.walletPubkey,
500
+ JSON.stringify({ method: "list_transactions", params })
501
+ );
502
+ signal.throwIfAborted();
503
+ const event = await client.signEvent({
504
+ kind: 23194,
505
+ created_at: Math.floor(Date.now() / 1e3),
506
+ tags: [
507
+ ["p", client.walletPubkey],
508
+ ["v", client.encryptionType === "nip44_v2" ? "1.0" : "0.0"],
509
+ ["encryption", client.encryptionType]
510
+ ],
511
+ content
512
+ });
513
+ signal.throwIfAborted();
514
+ return new Promise((resolve, reject) => {
515
+ const controller = new AbortController();
516
+ let done = false;
517
+ let subscription;
518
+ const finish = (error, result) => {
519
+ if (done) return;
520
+ done = true;
521
+ signal.removeEventListener("abort", abort);
522
+ controller.abort();
523
+ subscription?.close();
524
+ if (error === void 0) resolve(result);
525
+ else reject(error);
526
+ };
527
+ const abort = () => finish(signal.reason ?? new Error("Wallet scan cancelled."));
528
+ signal.addEventListener("abort", abort, { once: true });
529
+ if (signal.aborted) {
530
+ abort();
531
+ return;
532
+ }
533
+ try {
534
+ subscription = client.pool.subscribe(
535
+ client.relayUrls,
536
+ { kinds: [23195], authors: [client.walletPubkey], "#e": [event.id] },
537
+ {
538
+ abort: controller.signal,
539
+ onevent: async (reply) => {
540
+ if (done || reply.pubkey !== client.walletPubkey || !reply.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))
541
+ return;
542
+ try {
543
+ const body = JSON.parse(await client.decrypt(client.walletPubkey, reply.content));
544
+ if (done || signal.aborted) return;
545
+ if (body.error !== void 0 && body.error !== null) finish(body.error);
546
+ else finish(void 0, body.result);
547
+ } catch (error) {
548
+ finish(error);
549
+ }
550
+ }
551
+ }
552
+ );
553
+ if (done) {
554
+ subscription.close();
555
+ return;
556
+ }
557
+ void Promise.any(
558
+ client.pool.publish(client.relayUrls, event, { abort: controller.signal })
559
+ ).catch((error) => finish(error));
560
+ } catch (error) {
561
+ finish(error);
562
+ }
563
+ });
564
+ }
565
+
566
+ // src/nwc/transport.ts
488
567
  var require2 = createRequire(import.meta.url);
489
- async function callRequiredMethod(client, names, request) {
568
+ async function callRequiredMethod(client, names, request, options) {
490
569
  for (const name of names) {
491
570
  const method = client[name];
492
571
  if (typeof method === "function") {
493
- return await method.call(
494
- client,
495
- request
496
- );
572
+ return await method.call(client, request, options);
497
573
  }
498
574
  }
499
575
  throw new WalletPreflightError(
@@ -515,9 +591,10 @@ async function createDefaultAlbyNwcClient(connectionString) {
515
591
  );
516
592
  }
517
593
  const NWCClientConstructor = Constructor;
518
- return new NWCClientConstructor({
519
- nostrWalletConnectUrl: connectionString
520
- });
594
+ const client = new NWCClientConstructor({ nostrWalletConnectUrl: connectionString });
595
+ const receiveClient = client;
596
+ receiveClient.listTransactions = (request, options) => historyRequest(client, request, options?.signal ?? AbortSignal.timeout(1e4));
597
+ return receiveClient;
521
598
  }
522
599
  function ensureNodeWebSocket() {
523
600
  if (globalThis.WebSocket !== void 0) return;
@@ -555,7 +632,6 @@ var AlbyNwcReceiveClient = class {
555
632
  #clientFactory;
556
633
  #preflightSummary;
557
634
  #preflightPromise;
558
- #requirePreflight;
559
635
  #logger;
560
636
  #allowSpendCapableWallet;
561
637
  #spendCapabilityWarningDelayMs;
@@ -572,7 +648,6 @@ var AlbyNwcReceiveClient = class {
572
648
  };
573
649
  this.#client = options.client;
574
650
  this.#clientFactory = options.clientFactory;
575
- this.#requirePreflight = options.requirePreflight ?? true;
576
651
  this.#logger = options.logger;
577
652
  this.#allowSpendCapableWallet = options.allowSpendCapableWallet ?? false;
578
653
  this.#spendCapabilityWarningDelayMs = options.spendCapabilityWarningDelayMs ?? 0;
@@ -729,7 +804,7 @@ var AlbyNwcReceiveClient = class {
729
804
  });
730
805
  return result;
731
806
  }
732
- async listTransactions(request) {
807
+ async listTransactions(request, options) {
733
808
  await this.ensurePreflight();
734
809
  validateListTransactionsRequest(request);
735
810
  this.#log(
@@ -752,7 +827,8 @@ var AlbyNwcReceiveClient = class {
752
827
  rawResult = await callRequiredMethod(
753
828
  await this.getClient(),
754
829
  ["listTransactions", "list_transactions"],
755
- toNip47ListTransactionsParams(request)
830
+ toNip47ListTransactionsParams(request),
831
+ options
756
832
  );
757
833
  } catch (error) {
758
834
  const normalized = normalizeNwcWalletError(error);
@@ -909,7 +985,7 @@ var AlbyNwcReceiveClient = class {
909
985
  await client?.close?.();
910
986
  }
911
987
  async ensurePreflight() {
912
- if (!this.#requirePreflight || this.#preflightSummary !== void 0) return;
988
+ if (this.#preflightSummary !== void 0) return;
913
989
  this.#preflightPromise ??= this.preflight();
914
990
  try {
915
991
  await this.#preflightPromise;
@@ -963,21 +1039,24 @@ var OPENRECEIVE_SWAP_ASSET_INFO = {
963
1039
  label: "USDT",
964
1040
  network_label: "Tron",
965
1041
  coin: "USDT",
966
- network: "TRX"
1042
+ network: "TRX",
1043
+ pegged_to: "USD"
967
1044
  },
968
1045
  USDT_SOL: {
969
1046
  pay_in_asset: "USDT_SOL",
970
1047
  label: "USDT",
971
1048
  network_label: "Solana",
972
1049
  coin: "USDT",
973
- network: "SOL"
1050
+ network: "SOL",
1051
+ pegged_to: "USD"
974
1052
  },
975
1053
  USDC_SOL: {
976
1054
  pay_in_asset: "USDC_SOL",
977
1055
  label: "USDC",
978
1056
  network_label: "Solana",
979
1057
  coin: "USDC",
980
- network: "SOL"
1058
+ network: "SOL",
1059
+ pegged_to: "USD"
981
1060
  },
982
1061
  ETH_ETH: {
983
1062
  pay_in_asset: "ETH_ETH",
@@ -991,14 +1070,16 @@ var OPENRECEIVE_SWAP_ASSET_INFO = {
991
1070
  label: "USDT",
992
1071
  network_label: "Ethereum",
993
1072
  coin: "USDT",
994
- network: "ETH"
1073
+ network: "ETH",
1074
+ pegged_to: "USD"
995
1075
  },
996
1076
  USDC_ETH: {
997
1077
  pay_in_asset: "USDC_ETH",
998
1078
  label: "USDC",
999
1079
  network_label: "Ethereum",
1000
1080
  coin: "USDC",
1001
- network: "ETH"
1081
+ network: "ETH",
1082
+ pegged_to: "USD"
1002
1083
  }
1003
1084
  };
1004
1085
  var OPENRECEIVE_SWAP_PROVIDER_STATES = [
@@ -1735,6 +1816,7 @@ function fixedFloatAvailabilityMessage(reason) {
1735
1816
 
1736
1817
  // src/swap/fixedfloat-transport.ts
1737
1818
  import { createHmac } from "crypto";
1819
+ import { isRecord } from "@openreceive/core";
1738
1820
  var FixedFloatApiError = class _FixedFloatApiError extends Error {
1739
1821
  path;
1740
1822
  kind;
@@ -1871,22 +1953,31 @@ var FixedFloatTransport = class {
1871
1953
  return parsed.data;
1872
1954
  }
1873
1955
  logApiRequest(path, body = {}) {
1874
- this.apiRequestLogger?.({
1875
- provider: this.provider,
1876
- path,
1877
- body
1878
- });
1956
+ try {
1957
+ this.apiRequestLogger?.({
1958
+ provider: this.provider,
1959
+ path,
1960
+ has_body: Object.keys(body).length > 0,
1961
+ has_token: body.token != null
1962
+ });
1963
+ } catch {
1964
+ }
1879
1965
  }
1880
1966
  logApiResponse(input) {
1881
- this.apiResponseLogger?.({
1882
- provider: this.provider,
1883
- path: input.path,
1884
- status: input.status,
1885
- ok: input.ok,
1886
- code: input.code,
1887
- msg: input.msg,
1888
- data: input.data
1889
- });
1967
+ const pairCount = isRecord(input.data) ? input.data.pair_count : void 0;
1968
+ try {
1969
+ this.apiResponseLogger?.({
1970
+ provider: this.provider,
1971
+ path: input.path,
1972
+ status: input.status,
1973
+ ok: input.ok,
1974
+ code: typeof input.code === "number" ? input.code : void 0,
1975
+ has_data: input.data != null,
1976
+ items: Array.isArray(input.data) ? input.data.length : void 0,
1977
+ pair_count: typeof pairCount === "number" ? pairCount : void 0
1978
+ });
1979
+ } catch {
1980
+ }
1890
1981
  }
1891
1982
  };
1892
1983
  function formatFixedFloatApiErrorMessage(path, status, msg) {
@@ -2375,7 +2466,8 @@ function providerIdFromUri(uri) {
2375
2466
  }
2376
2467
 
2377
2468
  // src/service/logging.ts
2378
- import { compact as compact3, isRecord } from "@openreceive/core";
2469
+ import { isSensitiveLogKey, sanitizeLogValue } from "@openreceive/core";
2470
+ import { isSensitiveLogKey as isSensitiveLogKey2, redactSecrets } from "@openreceive/core";
2379
2471
  function emitLog(options, level, event, message, fields = {}) {
2380
2472
  emitEvent(options, {
2381
2473
  level,
@@ -2421,113 +2513,25 @@ function sanitizeEvent(entry) {
2421
2513
  }
2422
2514
  return clean;
2423
2515
  }
2424
- function sanitizeLogValue(value) {
2425
- if (typeof value === "string") return redactSecrets(value);
2426
- if (Array.isArray(value)) return value.map(sanitizeLogValue);
2427
- if (typeof value !== "object" || value === null) return value;
2428
- const clean = {};
2429
- for (const [key, nested] of Object.entries(value)) {
2430
- if (isSensitiveLogKey(key)) {
2431
- clean[key] = "[REDACTED]";
2432
- } else {
2433
- clean[key] = sanitizeLogValue(nested);
2434
- }
2435
- }
2436
- return clean;
2437
- }
2438
- function isSensitiveLogKey(key) {
2439
- if (/_present$/i.test(key)) return false;
2440
- return /secret|token|authorization|cookie|nwc|dsn|preimage|invoice|bolt11|swap_?data|(?:private|api)[_-]?key|^key$|api[_-]?sign/i.test(
2441
- key
2442
- );
2443
- }
2444
- function redactSecrets(value) {
2445
- return value.replace(/nostr\+walletconnect:[^\s"'`<>]+/g, "[REDACTED_NWC]").replace(/lightning\+swapconnect:[^\s"'`<>]+/g, "[REDACTED_LSC]").replace(/([?&](?:token|secret|key)=)[^&\s"'`<>]+/gi, "$1[REDACTED]");
2446
- }
2447
2516
  function summarizeSwapProviderApiRequest(entry) {
2448
- const body = isRecord(entry.body) ? entry.body : void 0;
2449
- return compact3({
2517
+ return {
2450
2518
  provider: entry.provider,
2451
2519
  path: entry.path,
2452
- reference: optionalLogString(body?.id),
2453
- choice: optionalLogString(body?.choice),
2454
- from_ccy: optionalLogString(body?.fromCcy),
2455
- to_ccy: optionalLogString(body?.toCcy),
2456
- amount: optionalLogString(body?.amount) ?? optionalLogNumber(body?.amount)
2457
- });
2520
+ has_body: entry.has_body,
2521
+ has_token: entry.has_token
2522
+ };
2458
2523
  }
2459
2524
  function summarizeSwapProviderApiResponse(entry) {
2460
- const summary = {
2525
+ return {
2461
2526
  provider: entry.provider,
2462
2527
  path: entry.path,
2463
2528
  status: entry.status,
2464
- ok: entry.ok
2529
+ ok: entry.ok,
2530
+ code: entry.code,
2531
+ has_data: entry.has_data,
2532
+ items: entry.items,
2533
+ pair_count: entry.pair_count
2465
2534
  };
2466
- if (entry.code !== void 0 && entry.code !== null) summary.code = entry.code;
2467
- const msg = optionalLogString(entry.msg);
2468
- if (msg !== void 0 && msg !== "OK") summary.msg = msg;
2469
- const data = entry.data;
2470
- if (Array.isArray(data)) {
2471
- summary.items = data.length;
2472
- return summary;
2473
- }
2474
- if (!isRecord(data)) return summary;
2475
- const pairCount = optionalLogNumber(data.pair_count);
2476
- if (pairCount !== void 0) {
2477
- summary.pair_count = pairCount;
2478
- return summary;
2479
- }
2480
- const reference = optionalLogString(data.id);
2481
- const orderStatus = optionalLogString(data.status);
2482
- if (reference !== void 0) summary.reference = reference;
2483
- if (orderStatus !== void 0) summary.order_status = orderStatus;
2484
- const from = summarizeSwapProviderSide(data.from);
2485
- const to = summarizeSwapProviderSide(data.to);
2486
- if (from !== void 0) summary.from = from;
2487
- if (to !== void 0) summary.to = to;
2488
- if (isRecord(data.time)) {
2489
- const left = optionalLogNumber(data.time.left);
2490
- if (left !== void 0) summary.left = left;
2491
- }
2492
- if (isRecord(data.emergency)) {
2493
- const choice = optionalLogString(data.emergency.choice);
2494
- if (choice !== void 0 && choice !== "NONE") summary.emergency = choice;
2495
- const statuses = Array.isArray(data.emergency.status) ? data.emergency.status.filter((item) => typeof item === "string" && item.length > 0).map((item) => item.toUpperCase()) : [];
2496
- if (statuses.length > 0) summary.emergency_status = statuses.join(",");
2497
- const repeat = data.emergency.repeat;
2498
- if (repeat === true || repeat === "1" || repeat === 1) summary.emergency_repeat = true;
2499
- }
2500
- if (isRecord(data.from) && isRecord(data.from.tx)) {
2501
- const received = optionalLogString(data.from.tx.amount);
2502
- if (received !== void 0) summary.deposit_received = received;
2503
- }
2504
- if (isRecord(data.back)) {
2505
- const refundAmount = optionalLogString(data.back.amount);
2506
- if (refundAmount !== void 0) summary.refund_amount = refundAmount;
2507
- }
2508
- if (reference === void 0) {
2509
- const fromRecord = isRecord(data.from) ? data.from : void 0;
2510
- const toRecord = isRecord(data.to) ? data.to : void 0;
2511
- const fromAmount = optionalLogString(fromRecord?.amount) ?? optionalLogString(data.fromAmount);
2512
- const toAmount = optionalLogString(toRecord?.amount) ?? optionalLogString(data.toAmount);
2513
- if (fromAmount !== void 0) summary.from_amount = fromAmount;
2514
- if (toAmount !== void 0) summary.to_amount = toAmount;
2515
- }
2516
- return summary;
2517
- }
2518
- function summarizeSwapProviderSide(side) {
2519
- if (!isRecord(side)) return void 0;
2520
- const code = optionalLogString(side.code) ?? optionalLogString(side.coin);
2521
- const amount = optionalLogString(side.amount);
2522
- if (code === void 0 && amount === void 0) return void 0;
2523
- if (code !== void 0 && amount !== void 0) return `${code} ${amount}`;
2524
- return code ?? amount;
2525
- }
2526
- function optionalLogString(value) {
2527
- return typeof value === "string" && value.length > 0 ? value : void 0;
2528
- }
2529
- function optionalLogNumber(value) {
2530
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2531
2535
  }
2532
2536
 
2533
2537
  export {
@@ -2550,7 +2554,7 @@ export {
2550
2554
  createNwcEndpointLogger,
2551
2555
  summarizeReconcilePass,
2552
2556
  sanitizeEvent,
2553
- redactSecrets,
2554
2557
  summarizeSwapProviderApiRequest,
2555
- summarizeSwapProviderApiResponse
2558
+ summarizeSwapProviderApiResponse,
2559
+ redactSecrets
2556
2560
  };
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  createNwcReceiveClient,
3
3
  readLscConnectionsFromEnvironment,
4
4
  redactSecrets
5
- } from "./chunk-65UPROSE.js";
5
+ } from "./chunk-7WTXLEDH.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { existsSync } from "fs";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { WalletCapabilitySummary, ReceiveNwcClient, NwcTransaction, ParsedNwcConnection, RedactedNwcConnection, MakeInvoiceRequest, MakeInvoiceResult, ListTransactionsRequest, ListTransactionsResult, ErrorCode, ErrorBody, RateQuote, SourcedPriceProvider, SimplePriceFetch, PaidPayment, PaymentCheck, BtcFiatRateMapWithSource, MoneyAmount, CachedPriceFeed } from '@openreceive/core';
1
+ import { WalletCapabilitySummary, ReceiveNwcClient, NwcTransaction, ParsedNwcConnection, RedactedNwcConnection, MakeInvoiceRequest, MakeInvoiceResult, ListTransactionsRequest, ListTransactionsResult, ErrorCode, ErrorBody, RateQuote, SourcedPriceProvider, SimplePriceFetch, PaidPayment, PaymentCheck, PaymentScanWindow, PaymentScanSlice, BtcFiatRateMapWithSource, MoneyAmount, CachedPriceFeed } from '@openreceive/core';
2
2
  export { ErrorBody, ErrorCode, NwcTransaction, OpenReceiveError, PaidPayment, PaymentCheck, RateQuote, ReceiveNwcClient } from '@openreceive/core';
3
3
 
4
4
  /**
@@ -12,8 +12,12 @@ interface AlbyNwcCompatibleClient {
12
12
  getWalletServiceInfo?: () => Promise<unknown>;
13
13
  makeInvoice?: (request: Record<string, unknown>) => Promise<unknown>;
14
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>;
15
+ listTransactions?: (request: Record<string, unknown>, options?: {
16
+ readonly signal?: AbortSignal;
17
+ }) => Promise<unknown>;
18
+ list_transactions?: (request: Record<string, unknown>, options?: {
19
+ readonly signal?: AbortSignal;
20
+ }) => Promise<unknown>;
17
21
  subscribeNotifications?: (callback: (notification: unknown) => void, notificationTypes?: string[]) => unknown;
18
22
  close?: () => Promise<void> | void;
19
23
  }
@@ -97,7 +101,6 @@ interface AlbyNwcReceiveClientOptions {
97
101
  connectionString: string;
98
102
  client?: AlbyNwcCompatibleClient;
99
103
  clientFactory?: AlbyNwcClientFactory;
100
- requirePreflight?: boolean;
101
104
  logger?: NwcEndpointLogger;
102
105
  /**
103
106
  * Explicit override: boot even when the connection advertises spend methods
@@ -128,7 +131,9 @@ declare class AlbyNwcReceiveClient implements ReceiveNwcClient {
128
131
  constructor(options: AlbyNwcReceiveClientOptions);
129
132
  preflight(): Promise<WalletCapabilitySummary>;
130
133
  makeInvoice(request: MakeInvoiceRequest): Promise<MakeInvoiceResult>;
131
- listTransactions(request: ListTransactionsRequest): Promise<ListTransactionsResult>;
134
+ listTransactions(request: ListTransactionsRequest, options?: {
135
+ readonly signal?: AbortSignal;
136
+ }): Promise<ListTransactionsResult>;
132
137
  /**
133
138
  * Opt-in NWC-02 notification subscription, limited to `payment_received`.
134
139
  * Notifications are authenticated wallet data: the handler receives the
@@ -227,6 +232,11 @@ interface SwapProviderAsset {
227
232
  * swap fee the payer absorbs is `pay_in_fiat` − `payout_fiat` (exchange spread plus
228
233
  * network fees, which the provider bakes into the deposit amount). All values are
229
234
  * decimal strings so hosts can round-trip them exactly when retaining an audit snapshot.
235
+ *
236
+ * These are VALUATIONS that explain the spread, never amounts a payer is told to
237
+ * send — `deposit_amount` is the only such amount. For a stablecoin pegged to
238
+ * `currency` the checkout renders the breakdown in the token and never shows
239
+ * `pay_in_fiat`, which would read as the deposit amount with a typo.
230
240
  */
231
241
  interface SwapFee {
232
242
  /** Fiat currency the equivalents are expressed in, e.g. "USD". */
@@ -274,32 +284,23 @@ interface SwapOrder {
274
284
  readonly fee?: SwapFee;
275
285
  readonly raw?: unknown;
276
286
  }
277
- /**
278
- * A single raw provider API response, surfaced for server-side observability.
279
- * Carries the HTTP status and the parsed `{code, msg, data}` envelope. Emitted
280
- * through the service's sanitizing log sink, so any nested secret (e.g. a
281
- * FixedFloat order token) is redacted before it reaches a log line.
282
- */
287
+ /** Allowlisted metadata, sanitized before invoking any diagnostic sink. */
283
288
  interface SwapProviderApiResponseLog {
284
289
  readonly provider: string;
285
290
  readonly path: string;
286
291
  readonly status: number;
287
292
  readonly ok: boolean;
288
- readonly code: unknown;
289
- readonly msg: unknown;
290
- readonly data: unknown;
293
+ readonly code?: number;
294
+ readonly has_data: boolean;
295
+ readonly items?: number;
296
+ readonly pair_count?: number;
291
297
  }
292
- /**
293
- * A single outbound provider API request, surfaced for server-side observability
294
- * alongside {@link SwapProviderApiResponseLog}. Carries the request path and body.
295
- * Emitted through the service's sanitizing log sink, so any secret in the body
296
- * (e.g. a FixedFloat order token on status/refund calls) is redacted; provider
297
- * auth headers are never included here.
298
- */
298
+ /** Request bodies, credentials, invoices and addresses never enter this hook. */
299
299
  interface SwapProviderApiRequestLog {
300
300
  readonly provider: string;
301
301
  readonly path: string;
302
- readonly body: unknown;
302
+ readonly has_body: boolean;
303
+ readonly has_token: boolean;
303
304
  }
304
305
  interface SwapProvider {
305
306
  readonly name: string;
@@ -309,15 +310,13 @@ interface SwapProvider {
309
310
  attachSwapCache?(cache: TransientSwapCache): void;
310
311
  /**
311
312
  * Attach a sink for outbound provider API requests, mirroring
312
- * {@link attachApiResponseLogger}. The
313
- * service routes entries through its sanitizing log sink, so secrets in the body
314
- * are redacted. Providers that make no remote calls may omit this.
313
+ * {@link attachApiResponseLogger}. Emit only the allowlisted metadata;
314
+ * diagnostics must not affect payment behavior. Providers without remote calls may omit this.
315
315
  */
316
316
  attachApiRequestLogger?(log: (entry: SwapProviderApiRequestLog) => void): void;
317
317
  /**
318
- * Attach a sink for raw provider API responses. The service routes entries through
319
- * its sanitizing log sink, so nested secrets are redacted. Providers that make no
320
- * remote calls may omit this.
318
+ * Attach a sink for allowlisted response metadata, never raw envelopes.
319
+ * Providers without remote calls may omit this.
321
320
  */
322
321
  attachApiResponseLogger?(log: (entry: SwapProviderApiResponseLog) => void): void;
323
322
  /**
@@ -542,6 +541,8 @@ interface Checkout {
542
541
  readonly bolt11: string;
543
542
  readonly amountMsats: number;
544
543
  readonly createdAt: number;
544
+ /** Missing on legacy snapshots; only wallet timestamps narrow history scans. */
545
+ readonly createdAtSource?: "wallet" | "host";
545
546
  readonly expiresAt: number;
546
547
  readonly fiatQuote: RateQuote | null;
547
548
  }
@@ -700,6 +701,14 @@ interface OpenReceive {
700
701
  }>;
701
702
  createCheckout(input: CreateCheckoutRequest): Promise<Checkout>;
702
703
  reconcilePayments(input: ReconcilePaymentsRequest): Promise<readonly PaymentCheck[]>;
704
+ /** Bounded durable history slice used by repository-backed HTTP and workers. */
705
+ scanPaymentSlice(input: {
706
+ window: PaymentScanWindow;
707
+ maxPages?: number;
708
+ deadline?: number;
709
+ signal?: AbortSignal;
710
+ onFinality?: (check: PaymentCheck) => Promise<void>;
711
+ }): Promise<PaymentScanSlice>;
703
712
  /**
704
713
  * Opt-in NWC-02 notifications: subscribe to wallet `payment_received`
705
714
  * notifications. Notifications are authenticated wallet data — a payload
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  summarizeReconcilePass,
20
20
  summarizeSwapProviderApiRequest,
21
21
  summarizeSwapProviderApiResponse
22
- } from "./chunk-65UPROSE.js";
22
+ } from "./chunk-7WTXLEDH.js";
23
23
 
24
24
  // src/index.ts
25
25
  import { OpenReceiveError as OpenReceiveError2 } from "@openreceive/core";
@@ -33,6 +33,7 @@ import {
33
33
  OpenReceiveError,
34
34
  parseNwcUri,
35
35
  reconcilePaymentAttempts,
36
+ scanPaymentSlice,
36
37
  unixSeconds
37
38
  } from "@openreceive/core";
38
39
 
@@ -765,6 +766,7 @@ async function createCheckout(context, request) {
765
766
  bolt11: walletInvoice.invoice,
766
767
  amountMsats: toSafeInteger(walletInvoice.amount_msats, "amount_msats"),
767
768
  createdAt,
769
+ createdAtSource: walletInvoice.created_at === void 0 ? "host" : "wallet",
768
770
  expiresAt,
769
771
  fiatQuote: resolved.fiatQuote
770
772
  };
@@ -1184,11 +1186,14 @@ function parsePayInAsset(value) {
1184
1186
  return value;
1185
1187
  }
1186
1188
  function parseRefundAddress(value, payInAsset) {
1189
+ if (!isSwapPayInAsset(payInAsset)) {
1190
+ throw serviceError(503, "INTERNAL", "Swap recovery requires a supported pay-in asset/network.");
1191
+ }
1187
1192
  const normalized = value.trim();
1188
1193
  if (normalized.length === 0 || normalized.length > 300) {
1189
1194
  throw serviceError(400, "INVALID_REQUEST", "refundAddress is invalid.");
1190
1195
  }
1191
- if (typeof payInAsset === "string" && !isValidSwapAddressForPayInAsset(payInAsset, normalized)) {
1196
+ if (!isValidSwapAddressForPayInAsset(payInAsset, normalized)) {
1192
1197
  throw serviceError(
1193
1198
  400,
1194
1199
  "INVALID_REQUEST",
@@ -1347,6 +1352,7 @@ async function createOpenReceive(supplied = {}) {
1347
1352
  };
1348
1353
  const service = {
1349
1354
  priceCurrencies,
1355
+ scanPaymentSlice: (input) => scanPaymentSlice({ ...input, client, clock }),
1350
1356
  prepareCheckout: (input) => prepareCheckout(context, input),
1351
1357
  createCheckout: (input) => createCheckout(context, input),
1352
1358
  reconcilePayments: async (input) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openreceive/node",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Accept Bitcoin Lightning payments in Node.js with your own wallet and optional USDT, USDC, SOL and ETH swaps.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -17,7 +17,7 @@
17
17
  "types": "./dist/index.d.ts",
18
18
  "dependencies": {
19
19
  "@getalby/sdk": "^8.0.3",
20
- "@openreceive/core": "0.4.9"
20
+ "@openreceive/core": "0.4.11"
21
21
  },
22
22
  "bin": {
23
23
  "openreceive": "./bin/openreceive.mjs"
@@ -70,6 +70,14 @@ same diagnostics redacted, always exit 0 — safe to share.
70
70
  - Refunds exist only for swap deposits from `refund_required`. There is **no
71
71
  Lightning refund** — the wallet cannot spend. Do not chase one.
72
72
  https://openreceive.org/guides/swap-refunds.md
73
+ - "Payer reports two different amounts on a stablecoin checkout" (50.05 or
74
+ 50.03?): the deposit amount is a token quantity, `fee.pay_in_fiat` is its
75
+ fiat valuation. Only `swap.deposit_amount` is an instruction. From 0.4.10 the
76
+ packaged checkout renders a USD stablecoin's breakdown in the token and never
77
+ shows `pay_in_fiat`; on an older bundle, upgrade `@openreceive/*`. To verify,
78
+ read the row's `deposit_amount` and `fee` and confirm the UI shows only the
79
+ deposit amount. A custom UI must call `createSwapFeeBreakdown(fee, swap)`
80
+ with the swap, not the fee alone.
73
81
 
74
82
  ## 6. Checkout UI shows nothing
75
83
 
@@ -86,6 +86,17 @@ state; do not retry-loop it, and do not build an idempotency store around it —
86
86
  that serialization is the library's job. (A hook failure while persisting an
87
87
  attempt is a **503 retryable**, deliberately distinct.)
88
88
 
89
+ ## Amounts on the deposit panel
90
+
91
+ `swap.deposit_amount` is the ONLY amount a payer is ever told to send, in the
92
+ pay-in token. `swap.fee.pay_in_fiat` / `payout_fiat` are fiat valuations that
93
+ explain the spread (why the deposit exceeds the cart total); they are not
94
+ instructions. For a stablecoin pegged to the fee currency (USDT, USDC) the
95
+ packaged checkout expresses the breakdown in the token and never renders
96
+ `pay_in_fiat` — "$50.03" under "50.05 USDC" reads as the same number with a
97
+ typo. A custom UI gets the same rule from `createSwapFeeBreakdown(fee, swap)`;
98
+ pass the swap, not just the fee.
99
+
89
100
  ## Secrets
90
101
 
91
102
  `NWC_URI` and `LSC_URI_*` are server-only. Never put them in browser code,
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (BTCPay Server)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Connect a BTCPay Server store to a receive-only NWC wallet with the OpenReceive
6
6
  plugin, and optionally let payers pay BTCPay invoices with USDT, USDC, ETH or
@@ -32,7 +32,7 @@ refund path on the same checkout screen.
32
32
 
33
33
  ## Step 0 — check the deployment before you change anything
34
34
 
35
- 1. Confirm the BTCPay Server version is 2.4.2 or later (Server Settings →
35
+ 1. Confirm the BTCPay Server version is 2.4.4 or later (Server Settings →
36
36
  About, or `GET /api/v1/server/info`). The plugin declares that minimum and
37
37
  BTCPay refuses to load it below.
38
38
  2. Check whether the plugin is installed (Server Settings → Plugins, or the
@@ -119,6 +119,8 @@ enough; drop the `.md` for the same page a person would read.
119
119
  Questions, or a problem with the plugin itself:
120
120
  https://openreceive.org/contact
121
121
 
122
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
123
+
122
124
  ---
123
125
 
124
126
  ## The quickstart, in full
@@ -128,7 +130,7 @@ passes. The page it comes from is https://openreceive.org/guides/quickstart-btcp
128
130
 
129
131
  ## BTCPay Server quickstart
130
132
 
131
- Requires BTCPay Server ≥ 2.4.2.
133
+ Requires BTCPay Server ≥ 2.4.4.
132
134
 
133
135
  The OpenReceive plugin makes a receive-only NWC wallet the Lightning node of a
134
136
  BTCPay store. BTCPay mints every Lightning invoice in that wallet and records
@@ -143,7 +145,7 @@ invoices, checkout, webhooks and Greenfield API are the host.
143
145
 
144
146
  ### 1. Prerequisites
145
147
 
146
- - A BTCPay Server, version 2.4.2 or later, on any network (mainnet, testnet,
148
+ - A BTCPay Server, version 2.4.4 or later, on any network (mainnet, testnet,
147
149
  signet, regtest). The wallet must be on the same network.
148
150
  - A receive-only NWC code for the wallet you want to receive into
149
151
  ([get one here](https://openreceive.org/get_a_nwc_code_to_receive_payments)).
@@ -157,9 +159,9 @@ invoices, checkout, webhooks and Greenfield API are the host.
157
159
 
158
160
  In BTCPay, open **Server Settings → Plugins**, search the plugin directory
159
161
  for **OpenReceive**, click **Install**, and restart BTCPay when prompted.
160
- BTCPay creates the plugin's one table (`openreceive_swaps`, schema
161
- `BTCPayServer.Plugins.OpenReceive`) in its own Postgres at startup; nothing
162
- else is created.
162
+ BTCPay creates the plugin's two tables (`openreceive_invoices` and
163
+ `openreceive_swaps`, schema `BTCPayServer.Plugins.OpenReceive`) in its own
164
+ Postgres at startup; nothing else is created.
163
165
 
164
166
  To build the plugin from source instead, follow
165
167
  [the .NET workspace README](https://github.com/OpenReceive/openreceive/blob/master/packages/dotnet/README.md).
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Django)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Django project — the app you are already working in. You
6
6
  do not need a copy of the OpenReceive source: the Python package is on PyPI
@@ -130,7 +130,7 @@ itself, and they hold for every integration.
130
130
  a placeholder that allows everything (`manage.py check` warns
131
131
  `openreceive.W002` while it is set) — replace it with this app's real
132
132
  ownership check, same as `on_paid`.
133
- - `on_paid` must be idempotent. It runs once per `reference` — your order
133
+ - `on_paid` must be idempotent. Its database fulfillment commits once per `reference` — your order
134
134
  id, one per thing you fulfill, created before checkout, kept across retries,
135
135
  never reused. A fresh id per page load lets one order be paid twice.
136
136
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -231,6 +231,13 @@ built on `@openreceive/browser/headless`. Read that before writing components.
231
231
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
232
232
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
233
233
  the model gives it.
234
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
235
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
236
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
237
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
238
+ (`payout_fiat`); express "you send" and the fee in the token. Use
239
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
240
+ it applies this for you; SOL and ETH keep a fiat breakdown.
234
241
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
235
242
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
236
243
  `startSwap` reports through `onError`.
@@ -300,6 +307,8 @@ enough; drop the `.md` for the same page a person would read.
300
307
  Questions, or a problem with the library itself:
301
308
  https://openreceive.org/contact
302
309
 
310
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
311
+
303
312
  ---
304
313
 
305
314
  ## The quickstart, in full
@@ -473,7 +482,7 @@ The host class needs three things: authorization, the trusted price, and
473
482
  fulfillment. All three receive the `reference` — a string you choose, and the
474
483
  fulfillment identity: your order id, one per thing you fulfill, created before
475
484
  checkout, kept across retries, never reused. OpenReceive never looks inside
476
- it, but `on_paid` runs once per reference, a new checkout under a reference
485
+ it, but `on_paid` commits fulfillment once per reference, a new checkout under a reference
477
486
  that already settled is refused with 409, and a fresh id per page load lets
478
487
  one order be paid twice.
479
488
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (FastAPI)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a FastAPI application — the app you are already working in.
6
6
  You do not need a copy of the OpenReceive source: the engine is on PyPI
@@ -116,7 +116,7 @@ itself, and they hold for every integration.
116
116
  - `authorize` runs on every request, and the `resource` it receives is a CLAIM
117
117
  the payer made, not proof. Read the Starlette request's session, cookie or
118
118
  auth dependency; never trust a body field.
119
- - `on_paid` must be idempotent. It runs once per `reference` — your order id, one
119
+ - `on_paid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
120
120
  per thing you fulfill, created before checkout, kept across retries, never
121
121
  reused. A fresh id per page load lets one order be paid twice.
122
122
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -228,6 +228,13 @@ components.
228
228
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
229
229
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
230
230
  the model gives it.
231
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
232
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
233
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
234
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
235
+ (`payout_fiat`); express "you send" and the fee in the token. Use
236
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
237
+ it applies this for you; SOL and ETH keep a fiat breakdown.
231
238
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
232
239
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
233
240
  `startSwap` reports through `onError`.
@@ -294,6 +301,8 @@ enough; drop the `.md` for the same page a person would read.
294
301
  Questions, or a problem with the library itself:
295
302
  https://openreceive.org/contact
296
303
 
304
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
305
+
297
306
  ---
298
307
 
299
308
  ## The quickstart, in full
@@ -468,7 +477,7 @@ the `reference`. OpenReceive never prices from payer input.
468
477
  The `reference` is a string you choose, and it is the fulfillment identity:
469
478
  your order id — one per thing you fulfill, created before checkout, kept
470
479
  across retries, never reused. OpenReceive never looks inside it, but `on_paid`
471
- runs once per reference, a new checkout under a reference that already
480
+ commits fulfillment once per reference, a new checkout under a reference that already
472
481
  settled is refused with 409, and a fresh id per page load lets one order be
473
482
  paid twice.
474
483
 
@@ -517,7 +526,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
517
526
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
518
527
 
519
528
  That is the whole loop: your server owns the price and the order, the payer gets
520
- an invoice, and `onPaid` runs once inside the settlement transaction.
529
+ an invoice, and `onPaid` runs inside the settlement transaction. Rolled-back transactions may retry the callback; use a host outbox for external delivery.
521
530
 
522
531
  A page without a bundler renders the same checkout as a custom element:
523
532
  `<openreceive-checkout reference="…" prefix="/openreceive">` from
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Fastify)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Fastify application — the app you are already working in.
6
6
  You do not need a copy of the OpenReceive source: the packages are on npm, and
@@ -107,7 +107,7 @@ itself, and they hold for every integration.
107
107
  payer-supplied amounts.
108
108
  - `authorize` runs on every request, and the `resource` it receives is a CLAIM
109
109
  the payer made, not proof. Read a framework session; never trust a body field.
110
- - `onPaid` must be idempotent. It runs once per `reference` — your order id, one
110
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
111
111
  per thing you fulfill, created before checkout, kept across retries, never
112
112
  reused. A fresh id per page load lets one order be paid twice.
113
113
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -211,6 +211,13 @@ components.
211
211
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
212
212
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
213
213
  the model gives it.
214
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
215
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
216
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
217
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
218
+ (`payout_fiat`); express "you send" and the fee in the token. Use
219
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
220
+ it applies this for you; SOL and ETH keep a fiat breakdown.
214
221
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
215
222
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
216
223
  `startSwap` reports through `onError`.
@@ -277,6 +284,8 @@ enough; drop the `.md` for the same page a person would read.
277
284
  Questions, or a problem with the library itself:
278
285
  https://openreceive.org/contact
279
286
 
287
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
288
+
280
289
  ---
281
290
 
282
291
  ## The quickstart, in full
@@ -470,7 +479,7 @@ the `reference`. OpenReceive never prices from payer input.
470
479
  The `reference` is a string you choose, and it is the fulfillment identity:
471
480
  your order id — one per thing you fulfill, created before checkout, kept
472
481
  across retries, never reused. OpenReceive never looks inside it, but `onPaid`
473
- runs once per reference, a new checkout under a reference that already
482
+ commits fulfillment once per reference, a new checkout under a reference that already
474
483
  settled is refused with 409, and a fresh id per page load lets one order be
475
484
  paid twice.
476
485
 
@@ -519,7 +528,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
519
528
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
520
529
 
521
530
  That is the whole loop: your server owns the price and the order, the payer gets
522
- an invoice, and `onPaid` runs once inside the settlement transaction.
531
+ an invoice, and `onPaid` runs inside the settlement transaction. Rolled-back transactions may retry the callback; use a host outbox for external delivery.
523
532
 
524
533
  A runnable illustration of this boundary — not a template to copy models from —
525
534
  is Buy a Button
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Laravel)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Laravel application — the app you are already working in.
6
6
  You do not need a copy of the OpenReceive source: the package is on Packagist
@@ -121,7 +121,7 @@ itself, and they hold for every integration.
121
121
  scaffolds `use AllowAllAuthorize;`, a placeholder trait that allows
122
122
  everything (the engine warns at boot while it is there) — replace it with
123
123
  this app's real ownership check, same as `onPaid`.
124
- - `onPaid` must be idempotent. It runs once per `reference` — your order
124
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order
125
125
  id, one per thing you fulfill, created before checkout, kept across retries,
126
126
  never reused. A fresh id per page load lets one order be paid twice.
127
127
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -216,6 +216,13 @@ built on `@openreceive/browser/headless`. Read that before writing components.
216
216
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
217
217
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
218
218
  the model gives it.
219
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
220
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
221
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
222
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
223
+ (`payout_fiat`); express "you send" and the fee in the token. Use
224
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
225
+ it applies this for you; SOL and ETH keep a fiat breakdown.
219
226
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
220
227
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
221
228
  `startSwap` reports through `onError`.
@@ -284,6 +291,8 @@ enough; drop the `.md` for the same page a person would read.
284
291
  Questions, or a problem with the library itself:
285
292
  https://openreceive.org/contact
286
293
 
294
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
295
+
287
296
  ---
288
297
 
289
298
  ## The quickstart, in full
@@ -449,7 +458,7 @@ variable as set/unset only.
449
458
  price, and fulfillment. All three receive the `reference` — a string you
450
459
  choose, and the fulfillment identity: your order id, one per thing you
451
460
  fulfill, created before checkout, kept across retries, never reused.
452
- OpenReceive never looks inside it, but `onPaid` runs once per reference, a new
461
+ OpenReceive never looks inside it, but `onPaid` commits fulfillment once per reference, a new
453
462
  checkout under a reference that already settled is refused with 409, and a
454
463
  fresh id per page load lets one order be paid twice.
455
464
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Next.js)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Next.js App Router application — the app you are already
6
6
  working in. You do not need a copy of the OpenReceive source: the packages are
@@ -109,7 +109,7 @@ itself, and they hold for every integration.
109
109
  payer-supplied amounts.
110
110
  - `authorize` runs on every request, and the `resource` it receives is a CLAIM
111
111
  the payer made, not proof. Read a framework session; never trust a body field.
112
- - `onPaid` must be idempotent. It runs once per `reference` — your order id, one
112
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
113
113
  per thing you fulfill, created before checkout, kept across retries, never
114
114
  reused. A fresh id per page load lets one order be paid twice.
115
115
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -217,6 +217,13 @@ components.
217
217
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
218
218
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
219
219
  the model gives it.
220
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
221
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
222
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
223
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
224
+ (`payout_fiat`); express "you send" and the fee in the token. Use
225
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
226
+ it applies this for you; SOL and ETH keep a fiat breakdown.
220
227
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
221
228
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
222
229
  `startSwap` reports through `onError`.
@@ -283,6 +290,8 @@ enough; drop the `.md` for the same page a person would read.
283
290
  Questions, or a problem with the library itself:
284
291
  https://openreceive.org/contact
285
292
 
293
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
294
+
286
295
  ---
287
296
 
288
297
  ## The quickstart, in full
@@ -489,7 +498,7 @@ the `reference`. OpenReceive never prices from payer input.
489
498
  The `reference` is a string you choose, and it is the fulfillment identity:
490
499
  your order id — one per thing you fulfill, created before checkout, kept
491
500
  across retries, never reused. OpenReceive never looks inside it, but `onPaid`
492
- runs once per reference, a new checkout under a reference that already
501
+ commits fulfillment once per reference, a new checkout under a reference that already
493
502
  settled is refused with 409, and a fresh id per page load lets one order be
494
503
  paid twice.
495
504
 
@@ -569,7 +578,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
569
578
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
570
579
 
571
580
  That is the whole loop: your server owns the price and the order, the payer gets
572
- an invoice, and `onPaid` runs once inside the settlement transaction.
581
+ an invoice, and `onPaid` runs inside the settlement transaction. Rolled-back transactions may retry the callback; use a host outbox for external delivery.
573
582
 
574
583
  A runnable illustration of this boundary — not a template to copy models from —
575
584
  is Buy a Button
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Node.js)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Node application — the app you are already working in. You
6
6
  do not need a copy of the OpenReceive source: the packages are on npm, and the
@@ -104,7 +104,7 @@ itself, and they hold for every integration.
104
104
  payer-supplied amounts.
105
105
  - `authorize` runs on every request, and the `resource` it receives is a CLAIM
106
106
  the payer made, not proof. Read a framework session; never trust a body field.
107
- - `onPaid` must be idempotent. It runs once per `reference` — your order id, one
107
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
108
108
  per thing you fulfill, created before checkout, kept across retries, never
109
109
  reused. A fresh id per page load lets one order be paid twice.
110
110
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -203,6 +203,13 @@ components.
203
203
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
204
204
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
205
205
  the model gives it.
206
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
207
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
208
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
209
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
210
+ (`payout_fiat`); express "you send" and the fee in the token. Use
211
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
212
+ it applies this for you; SOL and ETH keep a fiat breakdown.
206
213
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
207
214
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
208
215
  `startSwap` reports through `onError`.
@@ -269,6 +276,8 @@ enough; drop the `.md` for the same page a person would read.
269
276
  Questions, or a problem with the library itself:
270
277
  https://openreceive.org/contact
271
278
 
279
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
280
+
272
281
  ---
273
282
 
274
283
  ## The quickstart, in full
@@ -447,7 +456,7 @@ the `reference`. OpenReceive never prices from payer input.
447
456
  The `reference` is a string you choose, and it is the fulfillment identity:
448
457
  your order id — one per thing you fulfill, created before checkout, kept
449
458
  across retries, never reused. OpenReceive never looks inside it, but `onPaid`
450
- runs once per reference, a new checkout under a reference that already
459
+ commits fulfillment once per reference, a new checkout under a reference that already
451
460
  settled is refused with 409, and a fresh id per page load lets one order be
452
461
  paid twice.
453
462
 
@@ -496,7 +505,7 @@ Content-Security-Policy has a strict `img-src`, allow `data:`
496
505
  ([Provider registry](https://openreceive.org/guides/provider-registry.md#assets)).
497
506
 
498
507
  That is the whole loop: your server owns the price and the order, the payer gets
499
- an invoice, and `onPaid` runs once inside the settlement transaction.
508
+ an invoice, and `onPaid` runs inside the settlement transaction. Rolled-back transactions may retry the callback; use a host outbox for external delivery.
500
509
 
501
510
  A runnable illustration of this boundary — not a template to copy models from —
502
511
  is Buy a Button
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (PHP)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a PHP application — the app you are already working in. You
6
6
  do not need a copy of the OpenReceive source: the engine is on Packagist
@@ -125,7 +125,7 @@ itself, and they hold for every integration.
125
125
  is a placeholder that allows everything (the engine warns at boot while a
126
126
  host uses it) — replace it with this app's real ownership check, same as
127
127
  `onPaid`'s `Hosts\LoggingOnPaid`.
128
- - `onPaid` must be idempotent. It runs once per `reference` — your order id, one
128
+ - `onPaid` must be idempotent. Its database fulfillment commits once per `reference` — your order id, one
129
129
  per thing you fulfill, created before checkout, kept across retries, never
130
130
  reused. A fresh id per page load lets one order be paid twice.
131
131
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -229,6 +229,13 @@ of https://openreceive.org/guides/checkout-ux.md, for a UI built on
229
229
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
230
230
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
231
231
  the model gives it.
232
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
233
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
234
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
235
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
236
+ (`payout_fiat`); express "you send" and the fee in the token. Use
237
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
238
+ it applies this for you; SOL and ETH keep a fiat breakdown.
232
239
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
233
240
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
234
241
  `startSwap` reports through `onError`.
@@ -294,6 +301,8 @@ enough; drop the `.md` for the same page a person would read.
294
301
  Questions, or a problem with the library itself:
295
302
  https://openreceive.org/contact
296
303
 
304
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
305
+
297
306
  ---
298
307
 
299
308
  ## The quickstart, in full
@@ -505,7 +514,7 @@ prices with exact decimal math, and returns the order id the page will pass as
505
514
  the `reference`. OpenReceive never prices from payer input. The `reference` is
506
515
  a string you choose, and it is the fulfillment identity: your order id — one
507
516
  per thing you fulfill, created before checkout, kept across retries, never
508
- reused. `onPaid` runs once per reference, a new checkout under a reference
517
+ reused. `onPaid` commits fulfillment once per reference, a new checkout under a reference
509
518
  that already settled is refused with 409, and a fresh id per page load lets
510
519
  one order be paid twice.
511
520
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (Rails)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Add OpenReceive to a Rails application — the app you are already working in. You
6
6
  do not need a copy of the OpenReceive source: the gem is on RubyGems, the
@@ -116,7 +116,7 @@ itself, and they hold for every integration.
116
116
  body field. The generator installs `OpenReceive::ALLOW_ALL_AUTHORIZE`, a
117
117
  placeholder that allows everything (the engine warns at boot while it is
118
118
  set) — replace it with this app's real ownership check, same as `on_paid`.
119
- - `config.on_paid` must be idempotent. It runs once per `reference` — your order
119
+ - `config.on_paid` must be idempotent. Its database fulfillment commits once per `reference` — your order
120
120
  id, one per thing you fulfill, created before checkout, kept across retries,
121
121
  never reused. A fresh id per page load lets one order be paid twice.
122
122
  - Receive-only NWC is required; a spend-capable code fails closed at boot unless
@@ -210,6 +210,13 @@ built on `@openreceive/browser/headless`. Read that before writing components.
210
210
  - `createSwapDisplayModel` → `display.copyRows` for deposits: address, memo,
211
211
  and the bare amount each get a copy row. Render `swap.networkWarning*` as
212
212
  the model gives it.
213
+ - `swap.deposit_amount` is the only amount a payer is told to send. Never put
214
+ a fiat valuation of it (`swap.fee.pay_in_fiat`) next to a stablecoin amount:
215
+ "$50.03" under "50.05 USDC" reads as a typo, and the payer asks which one to
216
+ send. The one fiat figure on a USDT/USDC deposit panel is the cart total
217
+ (`payout_fiat`); express "you send" and the fee in the token. Use
218
+ `createSwapFeeBreakdown(fee, swap)` — with the swap, not the fee alone — and
219
+ it applies this for you; SOL and ETH keep a fiat breakdown.
213
220
  - `createCheckoutSession` owns mint and swap start. To start swaps, pass its
214
221
  `swap` option (`selection`, `prefix`, `fetch`) together. Without it
215
222
  `startSwap` reports through `onError`.
@@ -278,6 +285,8 @@ enough; drop the `.md` for the same page a person would read.
278
285
  Questions, or a problem with the library itself:
279
286
  https://openreceive.org/contact
280
287
 
288
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
289
+
281
290
  ---
282
291
 
283
292
  ## The quickstart, in full
@@ -425,7 +434,7 @@ The initializer needs three things: authorization, the trusted price, and
425
434
  fulfillment. All three receive the `reference` — a string you choose, and the
426
435
  fulfillment identity: your order id, one per thing you fulfill, created before
427
436
  checkout, kept across retries, never reused. OpenReceive never looks inside
428
- it, but `on_paid` runs once per reference, a new checkout under a reference
437
+ it, but `on_paid` commits fulfillment once per reference, a new checkout under a reference
429
438
  that already settled is refused with 409, and a fresh id per page load lets
430
439
  one order be paid twice.
431
440
 
@@ -1,6 +1,6 @@
1
1
  # OpenReceive agent directions (WordPress + WooCommerce)
2
2
 
3
- These directions describe OpenReceive 0.4.9.
3
+ These directions describe OpenReceive 0.4.11.
4
4
 
5
5
  Install and configure the OpenReceive gateway in the existing WooCommerce
6
6
  store. Preserve its theme, checkout, customer accounts, order model and prices.
@@ -73,6 +73,8 @@ flows; a receive-only NWC wallet cannot send payments.
73
73
  - [Agent Directions: BTCPay Server](https://openreceive.org/guides/agent-directions-btcpay.md)
74
74
  - [WordPress + WooCommerce Quickstart](https://openreceive.org/guides/quickstart-woocommerce.md)
75
75
 
76
+ - https://openreceive.org/guides/payment-safety-upgrade.md — coordinated upgrades and reviewed repair of existing attempts
77
+
76
78
  ---
77
79
 
78
80
  ## The quickstart, in full