@unicitylabs/sphere-sdk 0.14.10 → 0.14.11-dev.2

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.
@@ -1645,6 +1645,90 @@ async function splitOpIndex(deps, envelope) {
1645
1645
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
1646
1646
  }
1647
1647
 
1648
+ // modules/payments-v2/mint-params.ts
1649
+ function mintParams(recipientPubkey, coinId, amount) {
1650
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1651
+ }
1652
+
1653
+ // modules/payments-v2/prewarm-cache.ts
1654
+ var PrewarmCache = class _PrewarmCache {
1655
+ entries = /* @__PURE__ */ new Map();
1656
+ generation = 0;
1657
+ static key(tokenId, stateHash) {
1658
+ return `${tokenId}:${stateHash}`;
1659
+ }
1660
+ /** Claim the cache for a warm about to start; supersedes any warm already running. */
1661
+ begin() {
1662
+ this.entries.clear();
1663
+ this.generation += 1;
1664
+ return this.generation;
1665
+ }
1666
+ /**
1667
+ * Publish a warm's result, unless it was cancelled or superseded while its
1668
+ * fetch was in flight — a cancel that only cleared what had landed so far
1669
+ * would be undone by the continuation of the very warm it cancelled.
1670
+ */
1671
+ commit(generation, warmed) {
1672
+ if (generation !== this.generation) return;
1673
+ for (const [tokenId, stateHash, bytes] of warmed) {
1674
+ this.entries.set(_PrewarmCache.key(tokenId, stateHash), bytes);
1675
+ }
1676
+ }
1677
+ get(tokenId, stateHash) {
1678
+ if (stateHash === void 0) return void 0;
1679
+ return this.entries.get(_PrewarmCache.key(tokenId, stateHash));
1680
+ }
1681
+ clear() {
1682
+ this.entries.clear();
1683
+ this.generation += 1;
1684
+ }
1685
+ get size() {
1686
+ return this.entries.size;
1687
+ }
1688
+ };
1689
+ async function warmSendSources(deps, request) {
1690
+ const ids = deps.queue.previewSelection(request);
1691
+ if (ids.length === 0) return;
1692
+ const generation = deps.cache.begin();
1693
+ const at = new Map(ids.map((id) => [id, deps.view.stateHashOf(id)]));
1694
+ let blobs;
1695
+ try {
1696
+ blobs = await deps.storagePort.getBlobs([...ids]);
1697
+ } catch {
1698
+ return;
1699
+ }
1700
+ const warmed = [];
1701
+ for (const [tokenId, bytes] of blobs) {
1702
+ const stateHash = at.get(tokenId);
1703
+ if (stateHash !== void 0) warmed.push([tokenId, stateHash, bytes]);
1704
+ }
1705
+ deps.cache.commit(generation, warmed);
1706
+ }
1707
+ async function takeSourceBlobs(deps, sourceIds) {
1708
+ const blobs = /* @__PURE__ */ new Map();
1709
+ const missing = [];
1710
+ let trustCache = deps.cache.size > 0;
1711
+ if (trustCache) {
1712
+ try {
1713
+ await deps.view.delta();
1714
+ } catch {
1715
+ trustCache = false;
1716
+ }
1717
+ }
1718
+ for (const tokenId of sourceIds) {
1719
+ const warm = trustCache ? deps.cache.get(tokenId, deps.view.stateHashOf(tokenId)) : void 0;
1720
+ if (warm === void 0) missing.push(tokenId);
1721
+ else blobs.set(tokenId, warm);
1722
+ }
1723
+ deps.cache.clear();
1724
+ if (missing.length > 0) {
1725
+ for (const [tokenId, bytes] of await deps.storagePort.getBlobs(missing)) {
1726
+ blobs.set(tokenId, bytes);
1727
+ }
1728
+ }
1729
+ return blobs;
1730
+ }
1731
+
1648
1732
  // modules/payments-v2/inventory/presentation.ts
1649
1733
  function toToken(tokenId, snapshot, asset, registry, flags) {
1650
1734
  const iconUrl = registry.getIconUrl(asset.coinId);
@@ -1731,6 +1815,13 @@ function priceOne(asset, quote) {
1731
1815
  fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
1732
1816
  };
1733
1817
  }
1818
+ function transferringToken(tokenId, coinId, amount, registry, now) {
1819
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
1820
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
1821
+ transferring: true,
1822
+ suspectedSpent: false
1823
+ });
1824
+ }
1734
1825
 
1735
1826
  // modules/payments-v2/machine/payload.ts
1736
1827
  function buildPayload(recipientPubkey, request, spend, spentStates) {
@@ -2819,6 +2910,15 @@ var InventoryView = class {
2819
2910
  }
2820
2911
  return out;
2821
2912
  }
2913
+ /**
2914
+ * Current mirrored state of an ACTIVE token, or undefined when it is unknown
2915
+ * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
2916
+ * only reusable while the token still sits at the state it was fetched at.
2917
+ */
2918
+ stateHashOf(tokenId) {
2919
+ const entry = this.mirror.get(tokenId);
2920
+ return entry !== void 0 && entry.status === "active" ? entry.stateHash : void 0;
2921
+ }
2822
2922
  tokens(registry, filter) {
2823
2923
  const out = [];
2824
2924
  for (const [tokenId, entry] of this.mirror) {
@@ -3098,6 +3198,7 @@ var logger = {
3098
3198
 
3099
3199
  // modules/payments-v2/receive/Receive.ts
3100
3200
  var ACK_BATCH_SIZE = 200;
3201
+ var REFRESH_INTERVAL_MS = 750;
3101
3202
  var POLL_INTERVAL_MS = 3e4;
3102
3203
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
3103
3204
  function receivedDedupKey(tokenId, stateHash) {
@@ -3114,6 +3215,7 @@ var Receive = class {
3114
3215
  drainFlight = new SingleFlight();
3115
3216
  pollTimer = null;
3116
3217
  unsubscribeWake = null;
3218
+ lastRefreshAt = 0;
3117
3219
  drainOnce() {
3118
3220
  return this.drainFlight.run(() => this.doDrain());
3119
3221
  }
@@ -3131,6 +3233,14 @@ var Receive = class {
3131
3233
  this.unsubscribeWake?.();
3132
3234
  this.unsubscribeWake = null;
3133
3235
  }
3236
+ /** Ask for a mirror refresh at most once per REFRESH_INTERVAL_MS. */
3237
+ maybeRefresh(deps) {
3238
+ if (deps.refreshView === void 0) return;
3239
+ const now = deps.now?.() ?? Date.now();
3240
+ if (now - this.lastRefreshAt < REFRESH_INTERVAL_MS) return;
3241
+ this.lastRefreshAt = now;
3242
+ deps.refreshView();
3243
+ }
3134
3244
  async doDrain() {
3135
3245
  const deps = this.deps;
3136
3246
  const engine = deps.engine();
@@ -3144,6 +3254,7 @@ var Receive = class {
3144
3254
  for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
3145
3255
  await this.processEntry(deps, engine, entry, pending, stored);
3146
3256
  if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
3257
+ if (stored.length > 0) this.maybeRefresh(deps);
3147
3258
  }
3148
3259
  await flushAcks(deps, pending, pageEpoch);
3149
3260
  const served = deps.delivery.incomingEpoch();
@@ -3830,6 +3941,31 @@ var SpendQueue = class {
3830
3941
  }
3831
3942
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
3832
3943
  }
3944
+ /**
3945
+ * The sources `plan()` WOULD pick right now, reserving nothing and queueing
3946
+ * nothing — a read-only probe used to warm blobs while the user is still
3947
+ * looking at the confirm screen.
3948
+ *
3949
+ * Deliberately side-effect free: reserving here would pin the user's balance
3950
+ * across think-time and strand it if they never press send. The consequence is
3951
+ * that the real `plan()` may pick differently (inventory moves, another send
3952
+ * reserves first); callers must treat the result as a HINT and tolerate a miss,
3953
+ * never as a commitment that these sources will be spent.
3954
+ */
3955
+ previewSelection(request) {
3956
+ if (this.destroyed) return [];
3957
+ let amount;
3958
+ try {
3959
+ amount = parseAmount(request.amount);
3960
+ } catch {
3961
+ return [];
3962
+ }
3963
+ const { freeView, freeTotal } = this.freeView(request.coinId);
3964
+ if (freeTotal < amount) return [];
3965
+ const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
3966
+ if (plan === null) return [];
3967
+ return [...plan.direct, ...plan.split !== void 0 ? [plan.split.tokenId] : []];
3968
+ }
3833
3969
  // Coverage math: free + Σ declared expected change of in-flight sends —
3834
3970
  // never the full amount of in-flight sources (§5.3).
3835
3971
  declareExpectedChange(transferId, coinId, amount) {
@@ -4018,7 +4154,7 @@ function composeFacadeParts(deps, hooks) {
4018
4154
  machineDeps,
4019
4155
  machine: new TransferMachine(machineDeps),
4020
4156
  heldStates,
4021
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
4157
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
4022
4158
  requests: buildRequests(deps, hooks),
4023
4159
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
4024
4160
  };
@@ -4085,7 +4221,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
4085
4221
  }
4086
4222
  };
4087
4223
  }
4088
- function buildReceive(deps, hooks, historyStore, heldStates) {
4224
+ function buildReceive(deps, hooks, historyStore, heldStates, view) {
4089
4225
  return new Receive({
4090
4226
  delivery: deps.deliveryPort,
4091
4227
  engine: () => hooks.engine(),
@@ -4099,6 +4235,11 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
4099
4235
  registry: deps.registry,
4100
4236
  recordReceived: (record) => recordReceived(historyStore, record),
4101
4237
  emit: (event, transfer) => deps.emit(event, transfer),
4238
+ // The mirror is the balance the wallet renders; refresh it as the drain runs
4239
+ // so a multi-token receive lands progressively instead of all at the end.
4240
+ refreshView: () => {
4241
+ void view.delta().catch(() => void 0);
4242
+ },
4102
4243
  attention: (transferId, code, detail) => {
4103
4244
  deps.emit(
4104
4245
  "transfer:attention",
@@ -4179,6 +4320,7 @@ var PaymentsFacade = class {
4179
4320
  now: () => this.nowMs()
4180
4321
  });
4181
4322
  }
4323
+ prewarmed = new PrewarmCache();
4182
4324
  view;
4183
4325
  ledger;
4184
4326
  queue;
@@ -4272,6 +4414,15 @@ var PaymentsFacade = class {
4272
4414
  return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
4273
4415
  }
4274
4416
  // ── money movement ─────────────────────────────────────────────────────────
4417
+ async prewarmSend(request) {
4418
+ await warmSendSources(this.warmDeps(), request);
4419
+ }
4420
+ discardPrewarm() {
4421
+ this.prewarmed.clear();
4422
+ }
4423
+ warmDeps() {
4424
+ return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
4425
+ }
4275
4426
  send(request) {
4276
4427
  return this.track(this.sendOutcome(request));
4277
4428
  }
@@ -4481,7 +4632,7 @@ var PaymentsFacade = class {
4481
4632
  }
4482
4633
  async materialize(transferId, recipientPubkey, request, spend, sourceIds) {
4483
4634
  const engine = this.engine();
4484
- const blobs = await this.deps.storagePort.getBlobs([...sourceIds]);
4635
+ const blobs = await takeSourceBlobs(this.warmDeps(), sourceIds);
4485
4636
  const sources = /* @__PURE__ */ new Map();
4486
4637
  const sourceTokens = [];
4487
4638
  const spentStates = {};
@@ -4494,7 +4645,7 @@ var PaymentsFacade = class {
4494
4645
  const keys = await engine.deliveryKeys(bytes);
4495
4646
  spentStates[tokenId] = { local: keys.stateHash, protocol: keys.stateHash };
4496
4647
  sources.set(tokenId, token);
4497
- sourceTokens.push(this.toUiToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId)));
4648
+ sourceTokens.push(transferringToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId), this.deps.registry, this.nowMs()));
4498
4649
  }
4499
4650
  const payload = buildPayload(recipientPubkey, request, spend, spentStates);
4500
4651
  const plan = {
@@ -4638,7 +4789,7 @@ var PaymentsFacade = class {
4638
4789
  await this.machineStores.mintJournal.upsert(entry);
4639
4790
  let token;
4640
4791
  try {
4641
- token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
4792
+ token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
4642
4793
  } catch (err) {
4643
4794
  this.heartbeat.arm();
4644
4795
  return { success: false, error: messageOf(err) };
@@ -4697,7 +4848,7 @@ var PaymentsFacade = class {
4697
4848
  });
4698
4849
  return false;
4699
4850
  }
4700
- const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
4851
+ const token = await engine.mint(mintParams(this.ownPubkeyBytes, entry.coinId, BigInt(entry.amount)), {
4701
4852
  transferId: entry.mintId
4702
4853
  });
4703
4854
  if (token.blob.tokenId !== entry.tokenId) {
@@ -4706,12 +4857,6 @@ var PaymentsFacade = class {
4706
4857
  await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
4707
4858
  return true;
4708
4859
  }
4709
- mintParams(coinId, amount) {
4710
- return {
4711
- recipientPubkey: this.ownPubkeyBytes,
4712
- value: { assets: [{ coinId, amount }] }
4713
- };
4714
- }
4715
4860
  async tokenInServerInventory(tokenId) {
4716
4861
  let page = await this.deps.storagePort.listInventory();
4717
4862
  for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
@@ -4740,12 +4885,6 @@ var PaymentsFacade = class {
4740
4885
  page = await this.deps.storagePort.listInventory(page.cursor);
4741
4886
  }
4742
4887
  }
4743
- toUiToken(tokenId, coinId, amount) {
4744
- const now = this.nowMs();
4745
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
4746
- const flags = { transferring: true, suspectedSpent: false };
4747
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, this.deps.registry, flags);
4748
- }
4749
4888
  track(op) {
4750
4889
  const settled = op.then(() => void 0, () => void 0);
4751
4890
  this.pendingOps.add(settled);