@unicitylabs/sphere-sdk 0.14.0-dev.5 → 0.14.0-dev.6

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.
@@ -7143,6 +7143,25 @@ interface PaymentsRequestsApi {
7143
7143
  decline(id: string): Promise<void>;
7144
7144
  dismissProcessed(): void;
7145
7145
  }
7146
+ /**
7147
+ * A pending-transfers row, derived ON READ from the §6 stores (intent backstop
7148
+ * + delivery journal + shortfalls) — never a cached mirror. kind 'shortfall' =
7149
+ * a completed partial (#690) whose `amount` is the remainder still owed;
7150
+ * legs.certified counts journaled legs (certified, delivery still owed).
7151
+ */
7152
+ interface PendingTransfer {
7153
+ transferId: string;
7154
+ kind: 'open' | 'shortfall';
7155
+ recipient: string;
7156
+ coinId: string;
7157
+ amount: string;
7158
+ legs: {
7159
+ certified: number;
7160
+ total: number;
7161
+ };
7162
+ deliveryPending: boolean;
7163
+ createdAt: number;
7164
+ }
7146
7165
  interface PaymentsV2 {
7147
7166
  assets(coinId?: string): Promise<Asset[]>;
7148
7167
  tokens(filter?: {
@@ -7157,6 +7176,8 @@ interface PaymentsV2 {
7157
7176
  receive(): Promise<{
7158
7177
  transfers: IncomingTransfer[];
7159
7178
  }>;
7179
+ pendingTransfers(): Promise<PendingTransfer[]>;
7180
+ resumeNow(): Promise<void>;
7160
7181
  readonly requests: PaymentsRequestsApi;
7161
7182
  }
7162
7183
 
@@ -7143,6 +7143,25 @@ interface PaymentsRequestsApi {
7143
7143
  decline(id: string): Promise<void>;
7144
7144
  dismissProcessed(): void;
7145
7145
  }
7146
+ /**
7147
+ * A pending-transfers row, derived ON READ from the §6 stores (intent backstop
7148
+ * + delivery journal + shortfalls) — never a cached mirror. kind 'shortfall' =
7149
+ * a completed partial (#690) whose `amount` is the remainder still owed;
7150
+ * legs.certified counts journaled legs (certified, delivery still owed).
7151
+ */
7152
+ interface PendingTransfer {
7153
+ transferId: string;
7154
+ kind: 'open' | 'shortfall';
7155
+ recipient: string;
7156
+ coinId: string;
7157
+ amount: string;
7158
+ legs: {
7159
+ certified: number;
7160
+ total: number;
7161
+ };
7162
+ deliveryPending: boolean;
7163
+ createdAt: number;
7164
+ }
7146
7165
  interface PaymentsV2 {
7147
7166
  assets(coinId?: string): Promise<Asset[]>;
7148
7167
  tokens(filter?: {
@@ -7157,6 +7176,8 @@ interface PaymentsV2 {
7157
7176
  receive(): Promise<{
7158
7177
  transfers: IncomingTransfer[];
7159
7178
  }>;
7179
+ pendingTransfers(): Promise<PendingTransfer[]>;
7180
+ resumeNow(): Promise<void>;
7160
7181
  readonly requests: PaymentsRequestsApi;
7161
7182
  }
7162
7183
 
@@ -25991,6 +25991,10 @@ function isValidationReject(err) {
25991
25991
  if (e === null) return false;
25992
25992
  return e.status === 422 || e.statusCode === 422 || e.code === "VALIDATION" || e.code === "VALIDATION_FAILED";
25993
25993
  }
25994
+ function retryAfterMsOf(err) {
25995
+ const retryAfter = err !== null && typeof err === "object" ? err.retryAfter : void 0;
25996
+ return typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : null;
25997
+ }
25994
25998
  var chainRegistry = /* @__PURE__ */ new WeakMap();
25995
25999
  var replayRegistry = /* @__PURE__ */ new WeakMap();
25996
26000
  function registryFor(registry, kv, storeKey, make) {
@@ -26628,6 +26632,217 @@ function validatePayload(raw) {
26628
26632
  return p;
26629
26633
  }
26630
26634
 
26635
+ // modules/payments-v2/convergence.ts
26636
+ var HEARTBEAT_SEED_MS = 5e3;
26637
+ var HEARTBEAT_CAP_MS = 12e4;
26638
+ var ConvergenceHeartbeat = class {
26639
+ constructor(deps) {
26640
+ this.deps = deps;
26641
+ }
26642
+ timer = null;
26643
+ delayMs = HEARTBEAT_SEED_MS;
26644
+ notBefore = 0;
26645
+ running = false;
26646
+ start() {
26647
+ this.running = true;
26648
+ this.delayMs = HEARTBEAT_SEED_MS;
26649
+ this.notBefore = 0;
26650
+ }
26651
+ /** §7 quiescence unchanged: a mid-backoff stop() cancels cleanly. */
26652
+ stop() {
26653
+ this.running = false;
26654
+ this.clearTimer();
26655
+ }
26656
+ /** Reset to seed on progress, ×2 to the cap otherwise; idle stops rescheduling. */
26657
+ settle(outcome, pending) {
26658
+ if (outcome.retryAfterMs !== null) {
26659
+ this.notBefore = Math.max(this.notBefore, this.deps.now() + outcome.retryAfterMs);
26660
+ }
26661
+ if (!pending) {
26662
+ this.delayMs = HEARTBEAT_SEED_MS;
26663
+ return;
26664
+ }
26665
+ const delay = outcome.progress ? HEARTBEAT_SEED_MS : this.delayMs;
26666
+ this.delayMs = Math.min(delay * 2, HEARTBEAT_CAP_MS);
26667
+ this.schedule(delay);
26668
+ }
26669
+ /** New pending work (keep-open park / journaled delivery / mint hold): prompt retry. */
26670
+ arm() {
26671
+ if (!this.running) return;
26672
+ this.delayMs = Math.min(HEARTBEAT_SEED_MS * 2, HEARTBEAT_CAP_MS);
26673
+ this.schedule(HEARTBEAT_SEED_MS);
26674
+ }
26675
+ /** connection:status recovery: the backoff resets; a pending tick re-fires at the seed. */
26676
+ recovered() {
26677
+ this.delayMs = HEARTBEAT_SEED_MS;
26678
+ if (this.timer !== null) this.arm();
26679
+ }
26680
+ schedule(delayMs) {
26681
+ this.clearTimer();
26682
+ if (!this.running) return;
26683
+ const floored = Math.max(delayMs, this.notBefore - this.deps.now());
26684
+ this.timer = setTimeout(() => {
26685
+ this.timer = null;
26686
+ this.deps.onTick();
26687
+ }, floored);
26688
+ }
26689
+ clearTimer() {
26690
+ if (this.timer !== null) clearTimeout(this.timer);
26691
+ this.timer = null;
26692
+ }
26693
+ };
26694
+ var Converger = class {
26695
+ constructor(deps) {
26696
+ this.deps = deps;
26697
+ }
26698
+ /** Resume open intents, then drain the delivery + mint journals (all idempotent). */
26699
+ async convergeOnce() {
26700
+ let progress = false;
26701
+ let retryAfterMs = null;
26702
+ try {
26703
+ progress = await this.resumePass();
26704
+ } catch (err) {
26705
+ retryAfterMs = retryAfterMsOf(err);
26706
+ }
26707
+ try {
26708
+ const stats = await this.deps.stores.deliveryJournal.replay(this.replayDeps());
26709
+ progress = progress || stats.delivered > 0;
26710
+ } catch (err) {
26711
+ retryAfterMs ??= retryAfterMsOf(err);
26712
+ }
26713
+ try {
26714
+ progress = await this.deps.replayMints() > 0 || progress;
26715
+ } catch (err) {
26716
+ retryAfterMs ??= retryAfterMsOf(err);
26717
+ }
26718
+ return { progress, retryAfterMs };
26719
+ }
26720
+ /** §4 heartbeat predicate — read fresh from the §6 stores, never cached. */
26721
+ async pendingWork() {
26722
+ const [backstop, journal, mints] = await Promise.all([
26723
+ this.deps.stores.backstop.list(),
26724
+ this.deps.stores.deliveryJournal.list(),
26725
+ this.deps.stores.mintJournal.list()
26726
+ ]);
26727
+ return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
26728
+ }
26729
+ async resumePass() {
26730
+ const report = await resumeAll(this.filteredMachineDeps());
26731
+ const progress = report.resumed.length > 0 || report.conflicted.length > 0;
26732
+ await this.deps.reconcile({
26733
+ resumed: report.resumed,
26734
+ conflicted: report.conflicted,
26735
+ open: report.failed
26736
+ });
26737
+ if (progress) await this.deps.refreshView().catch(() => void 0);
26738
+ return progress;
26739
+ }
26740
+ filteredMachineDeps() {
26741
+ const intents = this.deps.machineDeps.intents;
26742
+ return {
26743
+ ...this.deps.machineDeps,
26744
+ intents: {
26745
+ ...intents,
26746
+ listOpen: async () => (await intents.listOpen()).filter((intent) => !this.deps.isActiveOp(intent.transferId))
26747
+ }
26748
+ };
26749
+ }
26750
+ replayDeps() {
26751
+ return {
26752
+ delivery: this.deps.delivery,
26753
+ now: () => this.deps.now(),
26754
+ attention: (transferId, code, detail) => {
26755
+ this.deps.emit(
26756
+ "transfer:attention",
26757
+ detail === void 0 ? { transferId, code } : { transferId, code, detail }
26758
+ );
26759
+ }
26760
+ };
26761
+ }
26762
+ };
26763
+ async function derivePendingTransfers(stores, decryptPayload) {
26764
+ const [backstop, journal, shortfalls] = await Promise.all([
26765
+ stores.backstop.list(),
26766
+ stores.deliveryJournal.list(),
26767
+ stores.shortfalls.list()
26768
+ ]);
26769
+ const byTransfer = groupJournal(journal);
26770
+ const out = [];
26771
+ const seen = /* @__PURE__ */ new Set();
26772
+ for (const entry of backstop) {
26773
+ if (entry.disposition !== "open") continue;
26774
+ seen.add(entry.transferId);
26775
+ out.push(await openRow(entry, byTransfer.get(entry.transferId) ?? [], decryptPayload));
26776
+ }
26777
+ for (const [transferId, entries] of byTransfer) {
26778
+ if (seen.has(transferId)) continue;
26779
+ seen.add(transferId);
26780
+ out.push(journalOnlyRow(transferId, entries));
26781
+ }
26782
+ for (const entry of shortfalls) {
26783
+ if (!seen.has(entry.transferId)) out.push(shortfallRow(entry));
26784
+ }
26785
+ return out;
26786
+ }
26787
+ function groupJournal(journal) {
26788
+ const byTransfer = /* @__PURE__ */ new Map();
26789
+ for (const entry of journal) {
26790
+ const mine = byTransfer.get(entry.transferId);
26791
+ if (mine === void 0) byTransfer.set(entry.transferId, [entry]);
26792
+ else mine.push(entry);
26793
+ }
26794
+ return byTransfer;
26795
+ }
26796
+ async function openRow(entry, journal, decryptPayload) {
26797
+ let payload = null;
26798
+ try {
26799
+ const raw = await decryptPayload(entry.payloadEnvelope);
26800
+ if (raw !== null && typeof raw === "object") payload = raw;
26801
+ } catch {
26802
+ payload = null;
26803
+ }
26804
+ const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
26805
+ const certified = distinctOpCount(journal);
26806
+ return {
26807
+ transferId: entry.transferId,
26808
+ kind: "open",
26809
+ recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
26810
+ coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
26811
+ amount: typeof payload?.amount === "string" ? payload.amount : "",
26812
+ legs: { certified, total: Math.max(total, certified) },
26813
+ deliveryPending: journal.length > 0,
26814
+ createdAt: entry.createdAt
26815
+ };
26816
+ }
26817
+ function journalOnlyRow(transferId, entries) {
26818
+ const certified = distinctOpCount(entries);
26819
+ return {
26820
+ transferId,
26821
+ kind: "open",
26822
+ recipient: entries[0].recipientPubkey,
26823
+ coinId: "",
26824
+ amount: "",
26825
+ legs: { certified, total: certified },
26826
+ deliveryPending: true,
26827
+ createdAt: 0
26828
+ };
26829
+ }
26830
+ function shortfallRow(entry) {
26831
+ return {
26832
+ transferId: entry.transferId,
26833
+ kind: "shortfall",
26834
+ recipient: entry.recipient,
26835
+ coinId: entry.coinId,
26836
+ amount: entry.remainingAmount,
26837
+ legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
26838
+ deliveryPending: false,
26839
+ createdAt: entry.createdAt
26840
+ };
26841
+ }
26842
+ function distinctOpCount(entries) {
26843
+ return new Set(entries.map((entry) => entry.opIndex)).size;
26844
+ }
26845
+
26631
26846
  // modules/payments-v2/history/History.ts
26632
26847
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
26633
26848
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
@@ -28017,6 +28232,21 @@ var PaymentsFacade = class {
28017
28232
  this.ownPubkeyBytes = parts.ownPubkeyBytes;
28018
28233
  this.requests = parts.requests;
28019
28234
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
28235
+ this.heartbeat = new ConvergenceHeartbeat({
28236
+ now: () => this.nowMs(),
28237
+ onTick: () => this.trackTail(this.runConvergencePass())
28238
+ });
28239
+ this.converger = new Converger({
28240
+ machineDeps: this.machineDeps,
28241
+ stores: this.machineStores,
28242
+ delivery: deps.deliveryPort,
28243
+ reconcile: (report) => this.requests.reconcile(report),
28244
+ refreshView: () => this.view.delta(),
28245
+ replayMints: () => this.replayMints(),
28246
+ isActiveOp: (id) => this.activeMoneyOps.has(id),
28247
+ emit: deps.emit,
28248
+ now: () => this.nowMs()
28249
+ });
28020
28250
  }
28021
28251
  view;
28022
28252
  ledger;
@@ -28033,10 +28263,18 @@ var PaymentsFacade = class {
28033
28263
  pendingOps = /* @__PURE__ */ new Set();
28034
28264
  unsubscribers = [];
28035
28265
  started = false;
28266
+ // §7 heartbeat: in-memory timing cell + pass runner (see convergence.ts).
28267
+ heartbeat;
28268
+ converger;
28269
+ /** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
28270
+ resumeFlight = new SingleFlight();
28271
+ /** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
28272
+ activeMoneyOps = /* @__PURE__ */ new Set();
28036
28273
  // ── lifecycle ──────────────────────────────────────────────────────────────
28037
28274
  async start() {
28038
28275
  if (this.started) return;
28039
28276
  this.started = true;
28277
+ this.heartbeat.start();
28040
28278
  await this.deps.session.start();
28041
28279
  this.unsubscribers.push(
28042
28280
  this.deps.session.subscribeStream("mailbox", () => {
@@ -28049,20 +28287,11 @@ var PaymentsFacade = class {
28049
28287
  this.trackTail(this.requests.drainIncoming());
28050
28288
  })
28051
28289
  );
28052
- this.trackTail(this.runResume());
28053
- this.trackTail(
28054
- this.machineStores.deliveryJournal.replay({
28055
- delivery: this.deps.deliveryPort,
28056
- now: this.deps.now ?? Date.now,
28057
- attention: (transferId, code, detail) => {
28058
- this.deps.emit(
28059
- "transfer:attention",
28060
- detail === void 0 ? { transferId, code } : { transferId, code, detail }
28061
- );
28062
- }
28063
- })
28064
- );
28065
- this.trackTail(this.replayMints());
28290
+ const statusUnsub = this.deps.session.subscribeStatus?.((status) => {
28291
+ if (status === "connected") this.heartbeat.recovered();
28292
+ });
28293
+ if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
28294
+ this.trackTail(this.runConvergencePass());
28066
28295
  this.trackTail(this.seedHeldStates());
28067
28296
  await this.view.fullPull().catch(() => void 0);
28068
28297
  this.receiveLoop.start(this.deps.receivePollMs);
@@ -28070,6 +28299,7 @@ var PaymentsFacade = class {
28070
28299
  /** §7 same-address restart gate: resolves only after in-flight ops settle. */
28071
28300
  async stop() {
28072
28301
  this.started = false;
28302
+ this.heartbeat.stop();
28073
28303
  this.receiveLoop.stop();
28074
28304
  for (const unsubscribe of this.unsubscribers) unsubscribe();
28075
28305
  this.unsubscribers = [];
@@ -28095,6 +28325,10 @@ var PaymentsFacade = class {
28095
28325
  history(page) {
28096
28326
  return this.historyStore.page(page ?? {});
28097
28327
  }
28328
+ /** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
28329
+ pendingTransfers() {
28330
+ return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
28331
+ }
28098
28332
  // ── money movement ─────────────────────────────────────────────────────────
28099
28333
  send(request) {
28100
28334
  return this.track(this.sendWithPolicy(request));
@@ -28107,6 +28341,22 @@ var PaymentsFacade = class {
28107
28341
  mint(coinId, amount) {
28108
28342
  return this.track(this.mintInner(coinId, amount));
28109
28343
  }
28344
+ // §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
28345
+ async resumeNow() {
28346
+ if (!this.started) return;
28347
+ await this.track(this.runConvergencePass());
28348
+ }
28349
+ /** One single-flighted pass + its reschedule: concurrent callers coalesce. */
28350
+ runConvergencePass() {
28351
+ return this.resumeFlight.run(async () => {
28352
+ const outcome = await this.converger.convergeOnce();
28353
+ const pending = await this.converger.pendingWork().catch(() => true);
28354
+ this.heartbeat.settle(outcome, pending);
28355
+ });
28356
+ }
28357
+ nowMs() {
28358
+ return (this.deps.now ?? Date.now)();
28359
+ }
28110
28360
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
28111
28361
  async sendWithPolicy(request) {
28112
28362
  const recipient = await this.requireSameNetworkRecipient(request.recipient);
@@ -28156,12 +28406,15 @@ var PaymentsFacade = class {
28156
28406
  return recipient;
28157
28407
  }
28158
28408
  async runAttempt(ctx) {
28409
+ this.activeMoneyOps.add(ctx.transferId);
28159
28410
  try {
28160
28411
  const { deliveryPending } = await this.machine.run(ctx.plan);
28161
28412
  this.settleSuccess(ctx, ctx.sourceIds);
28162
28413
  return { kind: "success", deliveryPending };
28163
28414
  } catch (err) {
28164
- return this.disposeFailedAttempt(ctx, err);
28415
+ return await this.disposeFailedAttempt(ctx, err);
28416
+ } finally {
28417
+ this.activeMoneyOps.delete(ctx.transferId);
28165
28418
  }
28166
28419
  }
28167
28420
  /**
@@ -28170,7 +28423,10 @@ var PaymentsFacade = class {
28170
28423
  * the remainder under a NEW transferId — never the full amount.
28171
28424
  */
28172
28425
  async consumePartial(ctx, disposition, run, attempt) {
28173
- run.pendingAny = run.pendingAny || disposition.deliveryPending;
28426
+ if (disposition.deliveryPending) {
28427
+ run.pendingAny = true;
28428
+ this.heartbeat.arm();
28429
+ }
28174
28430
  const partial = disposition.entry;
28175
28431
  run.delivered.push(...partial.committedTokenIds);
28176
28432
  run.firstPartialId ??= partial.transferId;
@@ -28328,6 +28584,7 @@ var PaymentsFacade = class {
28328
28584
  /** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
28329
28585
  settleKeepOpen(ctx) {
28330
28586
  this.queue.clearExpectedChange(ctx.transferId);
28587
+ this.heartbeat.arm();
28331
28588
  }
28332
28589
  settlePartial(ctx, partial) {
28333
28590
  this.ledger.cancel(ctx.transferId);
@@ -28364,6 +28621,7 @@ var PaymentsFacade = class {
28364
28621
  for (const id of run.settledShortfalls.splice(0)) {
28365
28622
  await this.machineStores.shortfalls.removeByKey(id).catch(() => void 0);
28366
28623
  }
28624
+ if (run.pendingAny) this.heartbeat.arm();
28367
28625
  const result = {
28368
28626
  id: transferId,
28369
28627
  status: run.pendingAny ? "confirmed" : "delivered",
@@ -28408,21 +28666,29 @@ var PaymentsFacade = class {
28408
28666
  if (!/^(?:[0-9a-f]{2})+$/.test(coinId) || amount <= 0n) {
28409
28667
  return { success: false, error: "coinId must be even-length lowercase hex and amount positive" };
28410
28668
  }
28411
- const engine = this.engine();
28412
28669
  const mintId = this.newId();
28413
- const params = this.mintParams(coinId, amount);
28670
+ this.activeMoneyOps.add(mintId);
28671
+ try {
28672
+ return await this.mintUnderJournal(mintId, coinId, amount);
28673
+ } finally {
28674
+ this.activeMoneyOps.delete(mintId);
28675
+ }
28676
+ }
28677
+ async mintUnderJournal(mintId, coinId, amount) {
28678
+ const engine = this.engine();
28414
28679
  const entry = {
28415
28680
  mintId,
28416
28681
  coinId,
28417
28682
  amount: amount.toString(),
28418
28683
  tokenId: "",
28419
- createdAt: (this.deps.now ?? Date.now)()
28684
+ createdAt: this.nowMs()
28420
28685
  };
28421
28686
  await this.machineStores.mintJournal.upsert(entry);
28422
28687
  let token;
28423
28688
  try {
28424
- token = await engine.mint(params, { transferId: mintId });
28689
+ token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
28425
28690
  } catch (err) {
28691
+ this.heartbeat.arm();
28426
28692
  return { success: false, error: messageOf(err) };
28427
28693
  }
28428
28694
  if (token.blob.tokenId !== entry.tokenId) {
@@ -28431,6 +28697,7 @@ var PaymentsFacade = class {
28431
28697
  try {
28432
28698
  await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
28433
28699
  } catch (err) {
28700
+ this.heartbeat.arm();
28434
28701
  return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
28435
28702
  }
28436
28703
  return { success: true, tokenId: token.blob.tokenId };
@@ -28453,18 +28720,22 @@ var PaymentsFacade = class {
28453
28720
  this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
28454
28721
  this.trackTail(this.view.delta());
28455
28722
  }
28723
+ /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
28456
28724
  async replayMints() {
28725
+ let resolved = 0;
28457
28726
  for (const entry of await this.machineStores.mintJournal.list()) {
28727
+ if (this.activeMoneyOps.has(entry.mintId)) continue;
28458
28728
  try {
28459
- await this.replayMint(entry);
28729
+ if (await this.replayMint(entry)) resolved += 1;
28460
28730
  } catch {
28461
28731
  }
28462
28732
  }
28733
+ return resolved;
28463
28734
  }
28464
28735
  async replayMint(entry) {
28465
28736
  if (entry.tokenId !== "" && await this.tokenInServerInventory(entry.tokenId)) {
28466
28737
  await this.machineStores.mintJournal.removeByKey(entry.mintId);
28467
- return;
28738
+ return true;
28468
28739
  }
28469
28740
  const engine = this.engine();
28470
28741
  if (!supportsDeterministicMint(engine)) {
@@ -28472,7 +28743,7 @@ var PaymentsFacade = class {
28472
28743
  transferId: entry.mintId,
28473
28744
  code: ATTENTION_MINT_UNRESOLVED
28474
28745
  });
28475
- return;
28746
+ return false;
28476
28747
  }
28477
28748
  const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
28478
28749
  transferId: entry.mintId
@@ -28481,6 +28752,7 @@ var PaymentsFacade = class {
28481
28752
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
28482
28753
  }
28483
28754
  await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
28755
+ return true;
28484
28756
  }
28485
28757
  mintParams(coinId, amount) {
28486
28758
  return {
@@ -28506,17 +28778,6 @@ var PaymentsFacade = class {
28506
28778
  newId() {
28507
28779
  return (this.deps.newId ?? randomUUID)();
28508
28780
  }
28509
- async runResume() {
28510
- const report = await resumeAll(this.machineDeps);
28511
- await this.requests.reconcile({
28512
- resumed: report.resumed,
28513
- conflicted: report.conflicted,
28514
- open: report.failed
28515
- });
28516
- if (report.resumed.length > 0 || report.conflicted.length > 0) {
28517
- await this.view.delta().catch(() => void 0);
28518
- }
28519
- }
28520
28781
  async seedHeldStates() {
28521
28782
  let page = await this.deps.storagePort.listInventory();
28522
28783
  for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
@@ -28530,7 +28791,7 @@ var PaymentsFacade = class {
28530
28791
  toUiToken(tokenId, coinId, amount) {
28531
28792
  const registry = this.deps.registry;
28532
28793
  const iconUrl = registry.getIconUrl(coinId);
28533
- const now = (this.deps.now ?? Date.now)();
28794
+ const now = this.nowMs();
28534
28795
  return {
28535
28796
  id: tokenId,
28536
28797
  coinId,
@@ -28925,6 +29186,7 @@ var WalletApiSession = class {
28925
29186
  reseedChain = Promise.resolve();
28926
29187
  reseedsPending = 0;
28927
29188
  handlers = /* @__PURE__ */ new Map();
29189
+ statusHandlers = /* @__PURE__ */ new Set();
28928
29190
  socket = null;
28929
29191
  started = false;
28930
29192
  stopped = false;
@@ -29075,10 +29337,16 @@ var WalletApiSession = class {
29075
29337
  status() {
29076
29338
  return this.lastStatus ?? "offline";
29077
29339
  }
29340
+ /** Transition feed off the existing emitStatus point (FacadeSession.subscribeStatus). */
29341
+ subscribeStatus(handler) {
29342
+ this.statusHandlers.add(handler);
29343
+ return () => this.statusHandlers.delete(handler);
29344
+ }
29078
29345
  emitStatus(status) {
29079
29346
  if (this.stopped || this.lastStatus === status) return;
29080
29347
  this.lastStatus = status;
29081
29348
  this.deps.emitStatus(status);
29349
+ for (const handler of this.statusHandlers) handler(status);
29082
29350
  }
29083
29351
  async connect() {
29084
29352
  if (this.stopped) return;