@unicitylabs/sphere-sdk 0.14.3 → 0.14.4

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.
@@ -9288,6 +9288,7 @@ var import_CertificationResponse = require("@unicitylabs/state-transition-sdk/li
9288
9288
  var import_StateId = require("@unicitylabs/state-transition-sdk/lib/api/StateId.js");
9289
9289
  var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js");
9290
9290
  var import_InclusionProofResponse = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js");
9291
+ var import_JsonRpcNetworkError = require("@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js");
9291
9292
  var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js");
9292
9293
  var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js");
9293
9294
  var import_InclusionProofVerificationRule = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js");
@@ -9453,6 +9454,86 @@ function decodeSpherePaymentData(bytes) {
9453
9454
  return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
9454
9455
  }
9455
9456
 
9457
+ // token-engine/proof-wait.ts
9458
+ var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
9459
+ function resolveProofTimeoutMs(configured) {
9460
+ if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
9461
+ if (!Number.isFinite(configured) || configured <= 0) {
9462
+ throw new SphereError(
9463
+ `proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
9464
+ "INVALID_CONFIG"
9465
+ );
9466
+ }
9467
+ return configured;
9468
+ }
9469
+ function lateDeliveringSignal(signal) {
9470
+ return new Proxy(signal, {
9471
+ get(target, prop) {
9472
+ if (prop === "addEventListener") {
9473
+ return (type, listener, options) => {
9474
+ if (type === "abort" && target.aborted) {
9475
+ queueMicrotask(() => {
9476
+ listener.call(target, new Event("abort"));
9477
+ });
9478
+ return;
9479
+ }
9480
+ target.addEventListener(type, listener, options);
9481
+ };
9482
+ }
9483
+ const value = Reflect.get(target, prop, target);
9484
+ return typeof value === "function" ? value.bind(target) : value;
9485
+ }
9486
+ });
9487
+ }
9488
+ function rejectOnAbort(signal) {
9489
+ return new Promise((_resolve, reject) => {
9490
+ if (signal.aborted) {
9491
+ reject(signal.reason);
9492
+ return;
9493
+ }
9494
+ signal.addEventListener(
9495
+ "abort",
9496
+ () => {
9497
+ reject(signal.reason);
9498
+ },
9499
+ { once: true }
9500
+ );
9501
+ });
9502
+ }
9503
+ async function awaitProofBounded(deps, transaction, external) {
9504
+ const controller = new AbortController();
9505
+ const relayAbort = () => {
9506
+ controller.abort(external?.reason);
9507
+ };
9508
+ if (external !== void 0) {
9509
+ if (external.aborted) relayAbort();
9510
+ else external.addEventListener("abort", relayAbort, { once: true });
9511
+ }
9512
+ const timer = setTimeout(() => {
9513
+ controller.abort(
9514
+ new SphereError(
9515
+ `inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
9516
+ "AGGREGATOR_ERROR"
9517
+ )
9518
+ );
9519
+ }, deps.timeoutMs);
9520
+ const wait = (0, import_InclusionProofUtils.waitInclusionProof)(
9521
+ deps.client,
9522
+ deps.trustBase,
9523
+ deps.predicateVerifier,
9524
+ transaction,
9525
+ lateDeliveringSignal(controller.signal),
9526
+ deps.intervalMs
9527
+ );
9528
+ wait.catch(() => void 0);
9529
+ try {
9530
+ return await Promise.race([wait, rejectOnAbort(controller.signal)]);
9531
+ } finally {
9532
+ clearTimeout(timer);
9533
+ external?.removeEventListener("abort", relayAbort);
9534
+ }
9535
+ }
9536
+
9456
9537
  // token-engine/errors.ts
9457
9538
  var TransferConflictError = class extends SphereError {
9458
9539
  /**
@@ -9677,11 +9758,13 @@ var SphereTokenEngine = class {
9677
9758
  constructor(deps) {
9678
9759
  this.deps = deps;
9679
9760
  this.privateKeyHex = import_HexConverter.HexConverter.encode(deps.privateKey);
9761
+ this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
9680
9762
  }
9681
9763
  /** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
9682
9764
  deterministicMint = true;
9683
9765
  /** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
9684
9766
  privateKeyHex;
9767
+ proofTimeoutMs;
9685
9768
  // ── identity ────────────────────────────────────────────────────────────────
9686
9769
  getIdentity() {
9687
9770
  return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
@@ -10024,14 +10107,17 @@ var SphereTokenEngine = class {
10024
10107
  submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
10025
10108
  }
10026
10109
  try {
10027
- return await (0, import_InclusionProofUtils.waitInclusionProof)(
10028
- this.deps.client,
10029
- this.deps.trustBase,
10030
- this.deps.predicateVerifier,
10110
+ return await awaitProofBounded(
10111
+ {
10112
+ client: this.deps.client,
10113
+ trustBase: this.deps.trustBase,
10114
+ predicateVerifier: this.deps.predicateVerifier,
10115
+ timeoutMs: this.proofTimeoutMs,
10116
+ intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10117
+ // #683
10118
+ },
10031
10119
  transaction,
10032
- options?.signal,
10033
- this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10034
- // #683 finer poll cadence
10120
+ options?.signal
10035
10121
  );
10036
10122
  } catch (err) {
10037
10123
  if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
@@ -10047,6 +10133,13 @@ var SphereTokenEngine = class {
10047
10133
  );
10048
10134
  }
10049
10135
  }
10136
+ /**
10137
+ * One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
10138
+ * default deadline is unreliable — see lateDeliveringSignal — and a lost one
10139
+ * means an op that never settles, which also wedges PaymentsFacade.stop().
10140
+ * A deadline hit throws, and the caller maps that to ProofUnconfirmedError
10141
+ * (keep-open), which is the correct posture: the spend may be on-chain.
10142
+ */
10050
10143
  /** Fail fast if this engine's key does not own the token's current state. */
10051
10144
  assertOwned(token) {
10052
10145
  if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
@@ -10146,6 +10239,8 @@ async function createSphereTokenEngine(config) {
10146
10239
  networkId: trustBase.networkId,
10147
10240
  // #683: forward the (optional) proof-poll cadence; undefined → the engine default.
10148
10241
  proofPollIntervalMs: config.proofPollIntervalMs,
10242
+ proofTimeoutMs: config.proofTimeoutMs,
10243
+ // #739: was declared but never wired
10149
10244
  // Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
10150
10245
  ...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
10151
10246
  };
@@ -12055,6 +12150,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
12055
12150
  }
12056
12151
  return out;
12057
12152
  }
12153
+ async function deriveOpenIntentHolds(stores, decryptPayload) {
12154
+ const holds = /* @__PURE__ */ new Map();
12155
+ let complete = true;
12156
+ for (const entry of await stores.backstop.list()) {
12157
+ if (entry.disposition !== "open") continue;
12158
+ let payload = null;
12159
+ try {
12160
+ const raw = await decryptPayload(entry.payloadEnvelope);
12161
+ if (raw !== null && typeof raw === "object") payload = raw;
12162
+ } catch {
12163
+ complete = false;
12164
+ continue;
12165
+ }
12166
+ if (payload === null) {
12167
+ complete = false;
12168
+ continue;
12169
+ }
12170
+ const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
12171
+ const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
12172
+ holds.set(entry.transferId, [...direct, ...split]);
12173
+ }
12174
+ return { open: holds, complete };
12175
+ }
12058
12176
  function groupJournal(journal) {
12059
12177
  const byTransfer = /* @__PURE__ */ new Map();
12060
12178
  for (const entry of journal) {
@@ -12465,10 +12583,21 @@ var InventoryView = class {
12465
12583
  release(tokenId) {
12466
12584
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
12467
12585
  }
12586
+ pinned(tokenId) {
12587
+ return this.deps.isPinned?.(tokenId) ?? false;
12588
+ }
12589
+ /** In flight here OR pinned by a reservation: either way not spendable (#737). */
12590
+ held(tokenId) {
12591
+ return this.inFlight.has(tokenId) || this.pinned(tokenId);
12592
+ }
12593
+ // A PINNED token stays in the pool: the queue applies the ledger filter itself
12594
+ // and needs the entry to say how much is pinned when it refuses (#737). In
12595
+ // flight without a pin (spent, awaiting the mirror refresh) stays out.
12468
12596
  pool(coinId) {
12469
12597
  const out = [];
12470
12598
  for (const [tokenId, entry] of this.mirror) {
12471
- if (entry.status !== "active" || this.inFlight.has(tokenId)) continue;
12599
+ if (entry.status !== "active") continue;
12600
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
12472
12601
  if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
12473
12602
  const asset = entry.assets.find((a) => a.coinId === coinId);
12474
12603
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
@@ -12484,7 +12613,7 @@ var InventoryView = class {
12484
12613
  if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
12485
12614
  out.push(
12486
12615
  toToken(tokenId, entry, asset, registry, {
12487
- transferring: this.inFlight.has(tokenId),
12616
+ transferring: this.held(tokenId),
12488
12617
  suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
12489
12618
  })
12490
12619
  );
@@ -12492,7 +12621,7 @@ var InventoryView = class {
12492
12621
  return out;
12493
12622
  }
12494
12623
  async assets(registry, price) {
12495
- const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.inFlight.has(tokenId), registry);
12624
+ const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
12496
12625
  if (!price || raw.length === 0) return raw;
12497
12626
  return withPrices(raw, registry, price);
12498
12627
  }
@@ -13178,6 +13307,20 @@ var Requests = class {
13178
13307
  var ReservationLedger = class {
13179
13308
  reservations = /* @__PURE__ */ new Map();
13180
13309
  tokenHolder = /* @__PURE__ */ new Map();
13310
+ // #738: the held-set is RECONSTRUCTED from the open intents, so until
13311
+ // IntentPins proves every one of them "no holder" means unknown, not free.
13312
+ authoritative = false;
13313
+ /** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
13314
+ * Returns whether the gate actually moved, so callers signal transitions, not passes. */
13315
+ setAuthoritative(value) {
13316
+ const moved = this.authoritative !== value;
13317
+ this.authoritative = value;
13318
+ return moved;
13319
+ }
13320
+ /** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
13321
+ unprovenReason() {
13322
+ return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
13323
+ }
13181
13324
  // All-or-nothing: every entry is validated before any state mutates.
13182
13325
  reserve(reservationId, entries) {
13183
13326
  if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
@@ -13204,6 +13347,13 @@ var ReservationLedger = class {
13204
13347
  getFreeAmount(tokenId, tokenAmount) {
13205
13348
  return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
13206
13349
  }
13350
+ /** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
13351
+ holderOf(tokenId) {
13352
+ return this.tokenHolder.get(tokenId);
13353
+ }
13354
+ tokensOf(reservationId) {
13355
+ return [...this.reservations.get(reservationId) ?? []];
13356
+ }
13207
13357
  clear() {
13208
13358
  this.reservations.clear();
13209
13359
  this.tokenHolder.clear();
@@ -13216,6 +13366,44 @@ var ReservationLedger = class {
13216
13366
  }
13217
13367
  };
13218
13368
 
13369
+ // modules/payments-v2/select/pins.ts
13370
+ var IntentPins = class {
13371
+ constructor(deps) {
13372
+ this.deps = deps;
13373
+ }
13374
+ pinned = /* @__PURE__ */ new Set();
13375
+ /** The keep-open reservation IS this intent's pin from now on. */
13376
+ adopt(transferId) {
13377
+ this.pinned.add(transferId);
13378
+ }
13379
+ async sync() {
13380
+ const { open, complete } = await this.deps.openIntents();
13381
+ let changed = false;
13382
+ for (const transferId of [...this.pinned]) {
13383
+ if (open.has(transferId) || this.deps.isActive(transferId)) continue;
13384
+ this.pinned.delete(transferId);
13385
+ for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
13386
+ this.deps.ledger.cancel(transferId);
13387
+ changed = true;
13388
+ }
13389
+ for (const [transferId, tokenIds] of open) {
13390
+ if (this.deps.isActive(transferId)) continue;
13391
+ this.pinned.add(transferId);
13392
+ changed = this.pin(transferId, tokenIds) || changed;
13393
+ }
13394
+ const gateMoved = this.deps.ledger.setAuthoritative(complete);
13395
+ if (changed || gateMoved) this.deps.changed();
13396
+ }
13397
+ /** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
13398
+ pin(transferId, tokenIds) {
13399
+ if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
13400
+ const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
13401
+ if (free.length === 0) return false;
13402
+ this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
13403
+ return true;
13404
+ }
13405
+ };
13406
+
13219
13407
  // modules/payments-v2/select/selector.ts
13220
13408
  var DEFAULT_WORK_BUDGET = 2e4;
13221
13409
  var MAX_COMBINATION_SIZE = 5;
@@ -13306,6 +13494,11 @@ function searchCombinationsOfSize(window, size, target, search) {
13306
13494
  // modules/payments-v2/select/queue.ts
13307
13495
  var QUEUE_TIMEOUT_MS = 3e4;
13308
13496
  var QUEUE_MAX_SIZE = 100;
13497
+ function insufficientBalance(view, amount) {
13498
+ if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
13499
+ const message = view.pinnedTotal > 0n ? `Insufficient spendable balance: need ${amount.toString()}, ${view.freeTotal.toString()} free, ${view.pinnedTotal.toString()} pinned by ${String(view.pinnedBy)} transfer(s) still converging \u2014 see payments.pendingTransfers(); do not re-send.` : "Insufficient balance for this transaction";
13500
+ return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
13501
+ }
13309
13502
  var SpendQueue = class {
13310
13503
  constructor(deps) {
13311
13504
  this.deps = deps;
@@ -13318,12 +13511,10 @@ var SpendQueue = class {
13318
13511
  throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
13319
13512
  }
13320
13513
  const amount = parseAmount(request.amount);
13321
- const { freeView, freeTotal } = this.freeView(request.coinId);
13514
+ const view = this.freeView(request.coinId);
13515
+ const { freeView, freeTotal } = view;
13322
13516
  if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
13323
- throw new SphereError(
13324
- "Insufficient balance for this transaction",
13325
- "SEND_INSUFFICIENT_BALANCE"
13326
- );
13517
+ throw insufficientBalance(view, amount);
13327
13518
  }
13328
13519
  const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
13329
13520
  if (plan !== null) {
@@ -13373,11 +13564,10 @@ var SpendQueue = class {
13373
13564
  entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
13374
13565
  return "rejected";
13375
13566
  }
13376
- const { freeView, freeTotal } = this.freeView(entry.coinId);
13567
+ const view = this.freeView(entry.coinId);
13568
+ const { freeView, freeTotal } = view;
13377
13569
  if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
13378
- entry.reject(
13379
- new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
13380
- );
13570
+ entry.reject(insufficientBalance(view, entry.amount));
13381
13571
  return "rejected";
13382
13572
  }
13383
13573
  const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
@@ -13422,18 +13612,26 @@ var SpendQueue = class {
13422
13612
  entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
13423
13613
  if (entries.length === 0) this.queues.delete(coinId);
13424
13614
  }
13425
- // Whole-token semantics: only fully-free tokens enter the selector pool.
13615
+ // Whole-token semantics: only fully-free tokens enter the selector pool. What
13616
+ // the reservations hold back is counted too — the refusal has to name it (#737).
13426
13617
  freeView(coinId) {
13427
13618
  const freeView = [];
13428
13619
  let freeTotal = 0n;
13620
+ let pinnedTotal = 0n;
13621
+ const holders = /* @__PURE__ */ new Set();
13622
+ const unproven = this.deps.ledger.unprovenReason();
13429
13623
  for (const entry of this.deps.getPool(coinId)) {
13430
13624
  if (entry.amount <= 0n) continue;
13431
- if (this.deps.ledger.getFreeAmount(entry.tokenId, entry.amount) === entry.amount) {
13625
+ const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
13626
+ if (holder === void 0) {
13432
13627
  freeView.push(entry);
13433
13628
  freeTotal += entry.amount;
13629
+ } else {
13630
+ pinnedTotal += entry.amount;
13631
+ holders.add(holder);
13434
13632
  }
13435
13633
  }
13436
- return { freeView, freeTotal };
13634
+ return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
13437
13635
  }
13438
13636
  expectedChangeTotal(coinId) {
13439
13637
  let total = 0n;
@@ -13475,13 +13673,17 @@ function supportsDeterministicMint(engine) {
13475
13673
  }
13476
13674
  function composeFacadeParts(deps, hooks) {
13477
13675
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
13676
+ const ledger = new ReservationLedger();
13478
13677
  const view = new InventoryView({
13479
13678
  port: deps.storagePort,
13480
13679
  kv: deps.kv,
13481
13680
  emit: (event) => deps.emit(event, {}),
13681
+ // #737: a reserved token is unselectable, so it is never confirmed balance.
13682
+ // #738: while the held-set is unproven, EVERY token reads pinned — the report
13683
+ // must not call a token spendable that the queue is about to refuse to spend.
13684
+ isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
13482
13685
  ...deps.now !== void 0 ? { now: deps.now } : {}
13483
13686
  });
13484
- const ledger = new ReservationLedger();
13485
13687
  const queue = new SpendQueue({
13486
13688
  ledger,
13487
13689
  getPool: (coinId) => view.pool(coinId),
@@ -13502,6 +13704,7 @@ function composeFacadeParts(deps, hooks) {
13502
13704
  ownPubkeyBytes,
13503
13705
  view,
13504
13706
  ledger,
13707
+ pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
13505
13708
  queue,
13506
13709
  historyStore,
13507
13710
  machineStores,
@@ -13513,6 +13716,17 @@ function composeFacadeParts(deps, hooks) {
13513
13716
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
13514
13717
  };
13515
13718
  }
13719
+ function buildPins(deps, hooks, parts) {
13720
+ return new IntentPins({
13721
+ ledger: parts.ledger,
13722
+ openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
13723
+ isActive: (transferId) => hooks.isActiveOp(transferId),
13724
+ release: (tokenId) => {
13725
+ parts.view.release(tokenId);
13726
+ },
13727
+ changed: () => deps.emit("inventory:updated", {})
13728
+ });
13729
+ }
13516
13730
  function buildRestoreDeps(deps, machineDeps, machineStores, view) {
13517
13731
  return {
13518
13732
  stores: machineStores,
@@ -13622,7 +13836,8 @@ var PaymentsFacade = class {
13622
13836
  this.deps = deps;
13623
13837
  const parts = composeFacadeParts(deps, {
13624
13838
  engine: () => this.engine(),
13625
- send: (request) => this.send(request)
13839
+ send: (request) => this.send(request),
13840
+ isActiveOp: (id) => this.activeMoneyOps.has(id)
13626
13841
  });
13627
13842
  this.view = parts.view;
13628
13843
  this.ledger = parts.ledger;
@@ -13636,6 +13851,7 @@ var PaymentsFacade = class {
13636
13851
  this.ownPubkeyBytes = parts.ownPubkeyBytes;
13637
13852
  this.restoreDeps = parts.restoreDeps;
13638
13853
  this.requests = parts.requests;
13854
+ this.pins = parts.pins;
13639
13855
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
13640
13856
  this.heartbeat = new ConvergenceHeartbeat({
13641
13857
  now: () => this.nowMs(),
@@ -13678,6 +13894,8 @@ var PaymentsFacade = class {
13678
13894
  passChain = new SerialChain();
13679
13895
  /** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
13680
13896
  activeMoneyOps = /* @__PURE__ */ new Set();
13897
+ /** #737: the ledger holds exactly the sources of the still-open intents. */
13898
+ pins;
13681
13899
  // ── lifecycle ──────────────────────────────────────────────────────────────
13682
13900
  async start() {
13683
13901
  if (this.started) return;
@@ -13700,6 +13918,7 @@ var PaymentsFacade = class {
13700
13918
  });
13701
13919
  if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
13702
13920
  await this.deps.session.start();
13921
+ await this.pins.sync().catch(() => void 0);
13703
13922
  this.trackTail(this.runConvergencePass());
13704
13923
  this.trackTail(this.seedHeldStates());
13705
13924
  await this.view.fullPull().catch(() => void 0);
@@ -13770,6 +13989,7 @@ var PaymentsFacade = class {
13770
13989
  }
13771
13990
  async convergeBody() {
13772
13991
  const outcome = await this.converger.convergeOnce();
13992
+ await this.pins.sync().catch(() => void 0);
13773
13993
  const pending = await this.converger.pendingWork().catch(() => true);
13774
13994
  this.heartbeat.settle(outcome, pending);
13775
13995
  }
@@ -14009,6 +14229,7 @@ var PaymentsFacade = class {
14009
14229
  /** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
14010
14230
  settleKeepOpen(ctx) {
14011
14231
  this.queue.clearExpectedChange(ctx.transferId);
14232
+ this.pins.adopt(ctx.transferId);
14012
14233
  this.heartbeat.arm();
14013
14234
  }
14014
14235
  settlePartial(ctx, partial) {