@unicitylabs/sphere-sdk 0.14.3 → 0.14.5

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.js CHANGED
@@ -9218,6 +9218,7 @@ import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-t
9218
9218
  import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
9219
9219
  import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
9220
9220
  import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
9221
+ import { JsonRpcNetworkError } from "@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js";
9221
9222
  import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
9222
9223
  import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
9223
9224
  import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
@@ -9383,6 +9384,75 @@ function decodeSpherePaymentData(bytes) {
9383
9384
  return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
9384
9385
  }
9385
9386
 
9387
+ // token-engine/proof-wait.ts
9388
+ var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
9389
+ function resolveProofTimeoutMs(configured) {
9390
+ if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
9391
+ if (!Number.isFinite(configured) || configured <= 0) {
9392
+ throw new SphereError(
9393
+ `proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
9394
+ "INVALID_CONFIG"
9395
+ );
9396
+ }
9397
+ return configured;
9398
+ }
9399
+ var TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
9400
+ function isTransientGatewayError(err) {
9401
+ return err instanceof JsonRpcNetworkError && TRANSIENT_HTTP_STATUSES.has(err.status);
9402
+ }
9403
+ function pause(ms, signal) {
9404
+ return new Promise((resolve) => {
9405
+ const timer = setTimeout(resolve, ms);
9406
+ signal.addEventListener(
9407
+ "abort",
9408
+ () => {
9409
+ clearTimeout(timer);
9410
+ resolve();
9411
+ },
9412
+ { once: true }
9413
+ );
9414
+ });
9415
+ }
9416
+ async function awaitProofBounded(deps, transaction, external) {
9417
+ const controller = new AbortController();
9418
+ const relayAbort = () => {
9419
+ controller.abort(external?.reason);
9420
+ };
9421
+ if (external !== void 0) {
9422
+ if (external.aborted) relayAbort();
9423
+ else external.addEventListener("abort", relayAbort, { once: true });
9424
+ }
9425
+ const timer = setTimeout(() => {
9426
+ controller.abort(
9427
+ new SphereError(
9428
+ `inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
9429
+ "AGGREGATOR_ERROR"
9430
+ )
9431
+ );
9432
+ }, deps.timeoutMs);
9433
+ try {
9434
+ for (; ; ) {
9435
+ try {
9436
+ return await waitInclusionProof(
9437
+ deps.client,
9438
+ deps.trustBase,
9439
+ deps.predicateVerifier,
9440
+ transaction,
9441
+ controller.signal,
9442
+ deps.intervalMs
9443
+ );
9444
+ } catch (err) {
9445
+ if (controller.signal.aborted) throw err;
9446
+ if (!isTransientGatewayError(err)) throw err;
9447
+ await pause(deps.intervalMs, controller.signal);
9448
+ }
9449
+ }
9450
+ } finally {
9451
+ clearTimeout(timer);
9452
+ external?.removeEventListener("abort", relayAbort);
9453
+ }
9454
+ }
9455
+
9386
9456
  // token-engine/errors.ts
9387
9457
  var TransferConflictError = class extends SphereError {
9388
9458
  /**
@@ -9607,11 +9677,13 @@ var SphereTokenEngine = class {
9607
9677
  constructor(deps) {
9608
9678
  this.deps = deps;
9609
9679
  this.privateKeyHex = HexConverter.encode(deps.privateKey);
9680
+ this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
9610
9681
  }
9611
9682
  /** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
9612
9683
  deterministicMint = true;
9613
9684
  /** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
9614
9685
  privateKeyHex;
9686
+ proofTimeoutMs;
9615
9687
  // ── identity ────────────────────────────────────────────────────────────────
9616
9688
  getIdentity() {
9617
9689
  return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
@@ -9954,14 +10026,17 @@ var SphereTokenEngine = class {
9954
10026
  submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
9955
10027
  }
9956
10028
  try {
9957
- return await waitInclusionProof(
9958
- this.deps.client,
9959
- this.deps.trustBase,
9960
- this.deps.predicateVerifier,
10029
+ return await awaitProofBounded(
10030
+ {
10031
+ client: this.deps.client,
10032
+ trustBase: this.deps.trustBase,
10033
+ predicateVerifier: this.deps.predicateVerifier,
10034
+ timeoutMs: this.proofTimeoutMs,
10035
+ intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10036
+ // #683
10037
+ },
9961
10038
  transaction,
9962
- options?.signal,
9963
- this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
9964
- // #683 finer poll cadence
10039
+ options?.signal
9965
10040
  );
9966
10041
  } catch (err) {
9967
10042
  if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
@@ -9977,6 +10052,13 @@ var SphereTokenEngine = class {
9977
10052
  );
9978
10053
  }
9979
10054
  }
10055
+ /**
10056
+ * One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
10057
+ * default deadline is unreliable — see lateDeliveringSignal — and a lost one
10058
+ * means an op that never settles, which also wedges PaymentsFacade.stop().
10059
+ * A deadline hit throws, and the caller maps that to ProofUnconfirmedError
10060
+ * (keep-open), which is the correct posture: the spend may be on-chain.
10061
+ */
9980
10062
  /** Fail fast if this engine's key does not own the token's current state. */
9981
10063
  assertOwned(token) {
9982
10064
  if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
@@ -10076,6 +10158,8 @@ async function createSphereTokenEngine(config) {
10076
10158
  networkId: trustBase.networkId,
10077
10159
  // #683: forward the (optional) proof-poll cadence; undefined → the engine default.
10078
10160
  proofPollIntervalMs: config.proofPollIntervalMs,
10161
+ proofTimeoutMs: config.proofTimeoutMs,
10162
+ // #739: was declared but never wired
10079
10163
  // Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
10080
10164
  ...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
10081
10165
  };
@@ -11985,6 +12069,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
11985
12069
  }
11986
12070
  return out;
11987
12071
  }
12072
+ async function deriveOpenIntentHolds(stores, decryptPayload) {
12073
+ const holds = /* @__PURE__ */ new Map();
12074
+ let complete = true;
12075
+ for (const entry of await stores.backstop.list()) {
12076
+ if (entry.disposition !== "open") continue;
12077
+ let payload = null;
12078
+ try {
12079
+ const raw = await decryptPayload(entry.payloadEnvelope);
12080
+ if (raw !== null && typeof raw === "object") payload = raw;
12081
+ } catch {
12082
+ complete = false;
12083
+ continue;
12084
+ }
12085
+ if (payload === null) {
12086
+ complete = false;
12087
+ continue;
12088
+ }
12089
+ const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
12090
+ const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
12091
+ holds.set(entry.transferId, [...direct, ...split]);
12092
+ }
12093
+ return { open: holds, complete };
12094
+ }
11988
12095
  function groupJournal(journal) {
11989
12096
  const byTransfer = /* @__PURE__ */ new Map();
11990
12097
  for (const entry of journal) {
@@ -12395,10 +12502,21 @@ var InventoryView = class {
12395
12502
  release(tokenId) {
12396
12503
  if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
12397
12504
  }
12505
+ pinned(tokenId) {
12506
+ return this.deps.isPinned?.(tokenId) ?? false;
12507
+ }
12508
+ /** In flight here OR pinned by a reservation: either way not spendable (#737). */
12509
+ held(tokenId) {
12510
+ return this.inFlight.has(tokenId) || this.pinned(tokenId);
12511
+ }
12512
+ // A PINNED token stays in the pool: the queue applies the ledger filter itself
12513
+ // and needs the entry to say how much is pinned when it refuses (#737). In
12514
+ // flight without a pin (spent, awaiting the mirror refresh) stays out.
12398
12515
  pool(coinId) {
12399
12516
  const out = [];
12400
12517
  for (const [tokenId, entry] of this.mirror) {
12401
- if (entry.status !== "active" || this.inFlight.has(tokenId)) continue;
12518
+ if (entry.status !== "active") continue;
12519
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
12402
12520
  if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
12403
12521
  const asset = entry.assets.find((a) => a.coinId === coinId);
12404
12522
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
@@ -12414,7 +12532,7 @@ var InventoryView = class {
12414
12532
  if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
12415
12533
  out.push(
12416
12534
  toToken(tokenId, entry, asset, registry, {
12417
- transferring: this.inFlight.has(tokenId),
12535
+ transferring: this.held(tokenId),
12418
12536
  suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
12419
12537
  })
12420
12538
  );
@@ -12422,7 +12540,7 @@ var InventoryView = class {
12422
12540
  return out;
12423
12541
  }
12424
12542
  async assets(registry, price) {
12425
- const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.inFlight.has(tokenId), registry);
12543
+ const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
12426
12544
  if (!price || raw.length === 0) return raw;
12427
12545
  return withPrices(raw, registry, price);
12428
12546
  }
@@ -13108,6 +13226,20 @@ var Requests = class {
13108
13226
  var ReservationLedger = class {
13109
13227
  reservations = /* @__PURE__ */ new Map();
13110
13228
  tokenHolder = /* @__PURE__ */ new Map();
13229
+ // #738: the held-set is RECONSTRUCTED from the open intents, so until
13230
+ // IntentPins proves every one of them "no holder" means unknown, not free.
13231
+ authoritative = false;
13232
+ /** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
13233
+ * Returns whether the gate actually moved, so callers signal transitions, not passes. */
13234
+ setAuthoritative(value) {
13235
+ const moved = this.authoritative !== value;
13236
+ this.authoritative = value;
13237
+ return moved;
13238
+ }
13239
+ /** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
13240
+ unprovenReason() {
13241
+ return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
13242
+ }
13111
13243
  // All-or-nothing: every entry is validated before any state mutates.
13112
13244
  reserve(reservationId, entries) {
13113
13245
  if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
@@ -13134,6 +13266,13 @@ var ReservationLedger = class {
13134
13266
  getFreeAmount(tokenId, tokenAmount) {
13135
13267
  return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
13136
13268
  }
13269
+ /** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
13270
+ holderOf(tokenId) {
13271
+ return this.tokenHolder.get(tokenId);
13272
+ }
13273
+ tokensOf(reservationId) {
13274
+ return [...this.reservations.get(reservationId) ?? []];
13275
+ }
13137
13276
  clear() {
13138
13277
  this.reservations.clear();
13139
13278
  this.tokenHolder.clear();
@@ -13146,6 +13285,44 @@ var ReservationLedger = class {
13146
13285
  }
13147
13286
  };
13148
13287
 
13288
+ // modules/payments-v2/select/pins.ts
13289
+ var IntentPins = class {
13290
+ constructor(deps) {
13291
+ this.deps = deps;
13292
+ }
13293
+ pinned = /* @__PURE__ */ new Set();
13294
+ /** The keep-open reservation IS this intent's pin from now on. */
13295
+ adopt(transferId) {
13296
+ this.pinned.add(transferId);
13297
+ }
13298
+ async sync() {
13299
+ const { open, complete } = await this.deps.openIntents();
13300
+ let changed = false;
13301
+ for (const transferId of [...this.pinned]) {
13302
+ if (open.has(transferId) || this.deps.isActive(transferId)) continue;
13303
+ this.pinned.delete(transferId);
13304
+ for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
13305
+ this.deps.ledger.cancel(transferId);
13306
+ changed = true;
13307
+ }
13308
+ for (const [transferId, tokenIds] of open) {
13309
+ if (this.deps.isActive(transferId)) continue;
13310
+ this.pinned.add(transferId);
13311
+ changed = this.pin(transferId, tokenIds) || changed;
13312
+ }
13313
+ const gateMoved = this.deps.ledger.setAuthoritative(complete);
13314
+ if (changed || gateMoved) this.deps.changed();
13315
+ }
13316
+ /** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
13317
+ pin(transferId, tokenIds) {
13318
+ if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
13319
+ const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
13320
+ if (free.length === 0) return false;
13321
+ this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
13322
+ return true;
13323
+ }
13324
+ };
13325
+
13149
13326
  // modules/payments-v2/select/selector.ts
13150
13327
  var DEFAULT_WORK_BUDGET = 2e4;
13151
13328
  var MAX_COMBINATION_SIZE = 5;
@@ -13236,6 +13413,11 @@ function searchCombinationsOfSize(window, size, target, search) {
13236
13413
  // modules/payments-v2/select/queue.ts
13237
13414
  var QUEUE_TIMEOUT_MS = 3e4;
13238
13415
  var QUEUE_MAX_SIZE = 100;
13416
+ function insufficientBalance(view, amount) {
13417
+ if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
13418
+ 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";
13419
+ return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
13420
+ }
13239
13421
  var SpendQueue = class {
13240
13422
  constructor(deps) {
13241
13423
  this.deps = deps;
@@ -13248,12 +13430,10 @@ var SpendQueue = class {
13248
13430
  throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
13249
13431
  }
13250
13432
  const amount = parseAmount(request.amount);
13251
- const { freeView, freeTotal } = this.freeView(request.coinId);
13433
+ const view = this.freeView(request.coinId);
13434
+ const { freeView, freeTotal } = view;
13252
13435
  if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
13253
- throw new SphereError(
13254
- "Insufficient balance for this transaction",
13255
- "SEND_INSUFFICIENT_BALANCE"
13256
- );
13436
+ throw insufficientBalance(view, amount);
13257
13437
  }
13258
13438
  const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
13259
13439
  if (plan !== null) {
@@ -13303,11 +13483,10 @@ var SpendQueue = class {
13303
13483
  entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
13304
13484
  return "rejected";
13305
13485
  }
13306
- const { freeView, freeTotal } = this.freeView(entry.coinId);
13486
+ const view = this.freeView(entry.coinId);
13487
+ const { freeView, freeTotal } = view;
13307
13488
  if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
13308
- entry.reject(
13309
- new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
13310
- );
13489
+ entry.reject(insufficientBalance(view, entry.amount));
13311
13490
  return "rejected";
13312
13491
  }
13313
13492
  const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
@@ -13352,18 +13531,26 @@ var SpendQueue = class {
13352
13531
  entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
13353
13532
  if (entries.length === 0) this.queues.delete(coinId);
13354
13533
  }
13355
- // Whole-token semantics: only fully-free tokens enter the selector pool.
13534
+ // Whole-token semantics: only fully-free tokens enter the selector pool. What
13535
+ // the reservations hold back is counted too — the refusal has to name it (#737).
13356
13536
  freeView(coinId) {
13357
13537
  const freeView = [];
13358
13538
  let freeTotal = 0n;
13539
+ let pinnedTotal = 0n;
13540
+ const holders = /* @__PURE__ */ new Set();
13541
+ const unproven = this.deps.ledger.unprovenReason();
13359
13542
  for (const entry of this.deps.getPool(coinId)) {
13360
13543
  if (entry.amount <= 0n) continue;
13361
- if (this.deps.ledger.getFreeAmount(entry.tokenId, entry.amount) === entry.amount) {
13544
+ const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
13545
+ if (holder === void 0) {
13362
13546
  freeView.push(entry);
13363
13547
  freeTotal += entry.amount;
13548
+ } else {
13549
+ pinnedTotal += entry.amount;
13550
+ holders.add(holder);
13364
13551
  }
13365
13552
  }
13366
- return { freeView, freeTotal };
13553
+ return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
13367
13554
  }
13368
13555
  expectedChangeTotal(coinId) {
13369
13556
  let total = 0n;
@@ -13405,13 +13592,17 @@ function supportsDeterministicMint(engine) {
13405
13592
  }
13406
13593
  function composeFacadeParts(deps, hooks) {
13407
13594
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
13595
+ const ledger = new ReservationLedger();
13408
13596
  const view = new InventoryView({
13409
13597
  port: deps.storagePort,
13410
13598
  kv: deps.kv,
13411
13599
  emit: (event) => deps.emit(event, {}),
13600
+ // #737: a reserved token is unselectable, so it is never confirmed balance.
13601
+ // #738: while the held-set is unproven, EVERY token reads pinned — the report
13602
+ // must not call a token spendable that the queue is about to refuse to spend.
13603
+ isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
13412
13604
  ...deps.now !== void 0 ? { now: deps.now } : {}
13413
13605
  });
13414
- const ledger = new ReservationLedger();
13415
13606
  const queue = new SpendQueue({
13416
13607
  ledger,
13417
13608
  getPool: (coinId) => view.pool(coinId),
@@ -13432,6 +13623,7 @@ function composeFacadeParts(deps, hooks) {
13432
13623
  ownPubkeyBytes,
13433
13624
  view,
13434
13625
  ledger,
13626
+ pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
13435
13627
  queue,
13436
13628
  historyStore,
13437
13629
  machineStores,
@@ -13443,6 +13635,17 @@ function composeFacadeParts(deps, hooks) {
13443
13635
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
13444
13636
  };
13445
13637
  }
13638
+ function buildPins(deps, hooks, parts) {
13639
+ return new IntentPins({
13640
+ ledger: parts.ledger,
13641
+ openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
13642
+ isActive: (transferId) => hooks.isActiveOp(transferId),
13643
+ release: (tokenId) => {
13644
+ parts.view.release(tokenId);
13645
+ },
13646
+ changed: () => deps.emit("inventory:updated", {})
13647
+ });
13648
+ }
13446
13649
  function buildRestoreDeps(deps, machineDeps, machineStores, view) {
13447
13650
  return {
13448
13651
  stores: machineStores,
@@ -13552,7 +13755,8 @@ var PaymentsFacade = class {
13552
13755
  this.deps = deps;
13553
13756
  const parts = composeFacadeParts(deps, {
13554
13757
  engine: () => this.engine(),
13555
- send: (request) => this.send(request)
13758
+ send: (request) => this.send(request),
13759
+ isActiveOp: (id) => this.activeMoneyOps.has(id)
13556
13760
  });
13557
13761
  this.view = parts.view;
13558
13762
  this.ledger = parts.ledger;
@@ -13566,6 +13770,7 @@ var PaymentsFacade = class {
13566
13770
  this.ownPubkeyBytes = parts.ownPubkeyBytes;
13567
13771
  this.restoreDeps = parts.restoreDeps;
13568
13772
  this.requests = parts.requests;
13773
+ this.pins = parts.pins;
13569
13774
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
13570
13775
  this.heartbeat = new ConvergenceHeartbeat({
13571
13776
  now: () => this.nowMs(),
@@ -13608,6 +13813,8 @@ var PaymentsFacade = class {
13608
13813
  passChain = new SerialChain();
13609
13814
  /** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
13610
13815
  activeMoneyOps = /* @__PURE__ */ new Set();
13816
+ /** #737: the ledger holds exactly the sources of the still-open intents. */
13817
+ pins;
13611
13818
  // ── lifecycle ──────────────────────────────────────────────────────────────
13612
13819
  async start() {
13613
13820
  if (this.started) return;
@@ -13630,6 +13837,7 @@ var PaymentsFacade = class {
13630
13837
  });
13631
13838
  if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
13632
13839
  await this.deps.session.start();
13840
+ await this.pins.sync().catch(() => void 0);
13633
13841
  this.trackTail(this.runConvergencePass());
13634
13842
  this.trackTail(this.seedHeldStates());
13635
13843
  await this.view.fullPull().catch(() => void 0);
@@ -13700,6 +13908,7 @@ var PaymentsFacade = class {
13700
13908
  }
13701
13909
  async convergeBody() {
13702
13910
  const outcome = await this.converger.convergeOnce();
13911
+ await this.pins.sync().catch(() => void 0);
13703
13912
  const pending = await this.converger.pendingWork().catch(() => true);
13704
13913
  this.heartbeat.settle(outcome, pending);
13705
13914
  }
@@ -13939,6 +14148,7 @@ var PaymentsFacade = class {
13939
14148
  /** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
13940
14149
  settleKeepOpen(ctx) {
13941
14150
  this.queue.clearExpectedChange(ctx.transferId);
14151
+ this.pins.adopt(ctx.transferId);
13942
14152
  this.heartbeat.arm();
13943
14153
  }
13944
14154
  settlePartial(ctx, partial) {