@unicitylabs/sphere-sdk 0.9.1-dev.12 → 0.9.1-dev.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -11802,6 +11802,7 @@ function computeHistoryDedupKey(type, tokenId, transferId) {
11802
11802
  }
11803
11803
  var MAX_SYNCED_HISTORY_ENTRIES = 5e3;
11804
11804
  var MAX_HISTORY_HYDRATION_PAGES = 100;
11805
+ var MAX_PR_HYDRATION_PAGES = 100;
11805
11806
  var SEND_ENGINE_OP_TIMEOUT_MS = 6e4;
11806
11807
  var DELIVERY_POLL_INTERVAL_MS = 3e4;
11807
11808
  function enrichWithRegistry(info) {
@@ -12129,10 +12130,13 @@ var PaymentsModule = class _PaymentsModule {
12129
12130
  /** Coalesces concurrent payment-request pump runs. */
12130
12131
  prPumpInFlight = null;
12131
12132
  /**
12132
- * Set after the once-per-session `status=open` bootstrap scan: the surfaced
12133
- * incoming list is in-memory only, so still-open requests BELOW the
12134
- * persisted cursor must be recovered at session start (bounded by the §5.5
12135
- * per-payer open cap).
12133
+ * Set after the once-per-session full incoming hydration (#556): the surfaced
12134
+ * incoming list is in-memory only, so on a fresh engine the CURRENT state of
12135
+ * ALL incoming requests — open AND resolved (paid/declined/expired) — must be
12136
+ * rebuilt from a `role=incoming&since=0` pull, not just the still-open ones.
12137
+ * A status-filtered bootstrap (the pre-#556 `status=open` scan) dropped
12138
+ * requests resolved in a PRIOR session, so the payer reopened and the
12139
+ * 'Paid Successfully' request was gone (twin of #521/#549).
12136
12140
  */
12137
12141
  prBootstrapped = false;
12138
12142
  // Guard: ensure load() completes before processing incoming bundles
@@ -14981,22 +14985,28 @@ var PaymentsModule = class _PaymentsModule {
14981
14985
  * mailbox-cursor pattern: the persisted `{cursor, syncEpoch}` is the resume
14982
14986
  * point; a `syncEpoch` change (server restore — §5.4) voids cursor
14983
14987
  * continuity, so the tail re-pulls from 0 and the id-dedup in
14984
- * {@link surfaceIncomingPaymentRequest} absorbs the replays. Because the
14985
- * surfaced list is in-memory only, each session ALSO starts with one
14986
- * `status=open` bootstrap scan from 0 — still-open requests below the
14987
- * cursor (bounded by the §5.5 per-payer open cap) are recovered; resolved
14988
- * ones are not re-surfaced.
14988
+ * {@link surfaceIncomingPaymentRequest} absorbs the replays.
14989
+ *
14990
+ * Because the surfaced list is in-memory only, each session FIRST runs one
14991
+ * full incoming hydration from `since=0` with NO status filter (#556 —
14992
+ * mirrors {@link hydrateHistoryFromServer}): a fresh engine rebuilds the
14993
+ * CURRENT state of ALL incoming requests — open AND resolved — so a request
14994
+ * paid/declined/expired in a PRIOR session is still present (with its
14995
+ * resolved status) instead of vanishing once the cursor advanced past it.
14996
+ * Hydration NEVER fires the new-incoming handlers/events for resolved
14997
+ * requests — only `open` ones notify (the status-aware
14998
+ * {@link surfaceIncomingPaymentRequest}) — so reopening can't spam stale
14999
+ * 'new request' notifications. After hydration the `since`-cursor delta poll
15000
+ * picks up live updates from the resume point.
14989
15001
  */
14990
15002
  async pumpIncomingPaymentRequests(api) {
14991
15003
  const persisted = await this.readPrCursorState();
14992
15004
  if (!this.prBootstrapped) {
14993
- if (persisted !== null && persisted.cursor > 0n) {
14994
- let scan = await api.listPaymentRequests({ role: "incoming", status: "open", since: 0n });
14995
- for (; ; ) {
14996
- for (const wire of scan.requests) this.surfaceIncomingPaymentRequest(wire);
14997
- if (!scan.more) break;
14998
- scan = await api.listPaymentRequests({ role: "incoming", status: "open", since: scan.cursor });
14999
- }
15005
+ let scan = await api.listPaymentRequests({ role: "incoming", since: 0n });
15006
+ for (let pageCount = 0; pageCount < MAX_PR_HYDRATION_PAGES; pageCount++) {
15007
+ for (const wire of scan.requests) this.surfaceIncomingPaymentRequest(wire);
15008
+ if (!scan.more) break;
15009
+ scan = await api.listPaymentRequests({ role: "incoming", since: scan.cursor });
15000
15010
  }
15001
15011
  this.prBootstrapped = true;
15002
15012
  }
@@ -15030,17 +15040,23 @@ var PaymentsModule = class _PaymentsModule {
15030
15040
  return {};
15031
15041
  }
15032
15042
  }
15043
+ /** §16 wire status → the public {@link PaymentRequestStatus} display status. */
15044
+ static PR_WIRE_STATUS = { open: "pending", paid: "paid", declined: "rejected", expired: "expired" };
15033
15045
  /**
15034
15046
  * Map a §16 wire request onto the public {@link IncomingPaymentRequest}
15035
- * surface and notify (event + handlers). Only `open` requests are
15036
- * actionable; the in-memory id-dedup doubles as the replay guard for
15037
- * cursor resets. Multi-asset requests surface their first asset (the
15038
- * module's request surface is single-asset; module-created requests
15039
- * always are).
15047
+ * surface, deduped by id (the in-memory id-dedup doubles as the replay guard
15048
+ * for cursor resets). Requests of EVERY status are surfaced so a reloaded
15049
+ * thin wallet rebuilds the CURRENT state of its incoming view (#556) — open
15050
+ * ones land as actionable `pending`, resolved ones carry their paid/declined
15051
+ * (→ `rejected`)/expired status. Only `open` requests fire the new-incoming
15052
+ * event + handlers; resolved requests are folded into the list silently, so a
15053
+ * reload (or a `syncEpoch` re-pull) never re-notifies for already-resolved
15054
+ * requests. Multi-asset requests surface their first asset (the module's
15055
+ * request surface is single-asset; module-created requests always are).
15040
15056
  */
15041
15057
  surfaceIncomingPaymentRequest(wire) {
15042
- if (wire.status !== "open") return;
15043
15058
  if (this.paymentRequests.some((r) => r.id === wire.id)) return;
15059
+ const status = _PaymentsModule.PR_WIRE_STATUS[wire.status];
15044
15060
  const coinId = wire.assets[0]?.coinId ?? "";
15045
15061
  const coinDef = TokenRegistry.getInstance().getDefinition(coinId);
15046
15062
  const { memo: message, senderNametag } = this.decryptPaymentRequestMemo(wire);
@@ -15054,18 +15070,20 @@ var PaymentsModule = class _PaymentsModule {
15054
15070
  ...message !== void 0 ? { message } : {},
15055
15071
  requestId: wire.id,
15056
15072
  timestamp: wire.createdAt,
15057
- status: "pending"
15073
+ status
15058
15074
  };
15059
15075
  this.paymentRequests.unshift(request);
15060
- this.deps?.emitEvent("payment_request:incoming", request);
15061
- for (const handler of this.paymentRequestHandlers) {
15062
- try {
15063
- handler(request);
15064
- } catch (error) {
15065
- logger.debug("Payments", "Payment request handler error:", error);
15076
+ if (status === "pending") {
15077
+ this.deps?.emitEvent("payment_request:incoming", request);
15078
+ for (const handler of this.paymentRequestHandlers) {
15079
+ try {
15080
+ handler(request);
15081
+ } catch (error) {
15082
+ logger.debug("Payments", "Payment request handler error:", error);
15083
+ }
15066
15084
  }
15067
15085
  }
15068
- logger.debug("Payments", `Incoming payment request: ${request.id} for ${request.amount} ${request.symbol}`);
15086
+ logger.debug("Payments", `Incoming payment request: ${request.id} (${status}) for ${request.amount} ${request.symbol}`);
15069
15087
  }
15070
15088
  /**
15071
15089
  * Outgoing requests are a `?before=` backfill view (§16 — newest-first, no