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

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.
@@ -228,6 +228,8 @@ interface PendingTransfer {
228
228
  }
229
229
  type ConnectionStatus = 'connected' | 'degraded' | 'offline';
230
230
  interface PaymentsV2 {
231
+ prewarmSend(request: SendRequest): Promise<void>;
232
+ discardPrewarm(): void;
231
233
  assets(coinId?: string): Promise<Asset[]>;
232
234
  tokens(filter?: {
233
235
  coinId?: string;
@@ -1042,6 +1044,7 @@ declare const MAX_RESELECT = 8;
1042
1044
  declare const ATTENTION_MINT_UNRESOLVED = "mint:unresolved";
1043
1045
  declare class PaymentsFacade implements PaymentsV2 {
1044
1046
  private readonly deps;
1047
+ private readonly prewarmed;
1045
1048
  private readonly view;
1046
1049
  private readonly ledger;
1047
1050
  private readonly queue;
@@ -1086,6 +1089,9 @@ declare class PaymentsFacade implements PaymentsV2 {
1086
1089
  connectionStatus(): ConnectionStatus;
1087
1090
  /** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
1088
1091
  pendingTransfers(): Promise<PendingTransfer[]>;
1092
+ prewarmSend(request: SendRequest): Promise<void>;
1093
+ discardPrewarm(): void;
1094
+ private warmDeps;
1089
1095
  send(request: SendRequest): Promise<TransferResult>;
1090
1096
  receive(): Promise<{
1091
1097
  transfers: IncomingTransfer[];
@@ -1142,12 +1148,10 @@ declare class PaymentsFacade implements PaymentsV2 {
1142
1148
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
1143
1149
  private replayMints;
1144
1150
  private replayMint;
1145
- private mintParams;
1146
1151
  private tokenInServerInventory;
1147
1152
  private engine;
1148
1153
  private newId;
1149
1154
  private seedHeldStates;
1150
- private toUiToken;
1151
1155
  private track;
1152
1156
  private trackTail;
1153
1157
  }
@@ -228,6 +228,8 @@ interface PendingTransfer {
228
228
  }
229
229
  type ConnectionStatus = 'connected' | 'degraded' | 'offline';
230
230
  interface PaymentsV2 {
231
+ prewarmSend(request: SendRequest): Promise<void>;
232
+ discardPrewarm(): void;
231
233
  assets(coinId?: string): Promise<Asset[]>;
232
234
  tokens(filter?: {
233
235
  coinId?: string;
@@ -1042,6 +1044,7 @@ declare const MAX_RESELECT = 8;
1042
1044
  declare const ATTENTION_MINT_UNRESOLVED = "mint:unresolved";
1043
1045
  declare class PaymentsFacade implements PaymentsV2 {
1044
1046
  private readonly deps;
1047
+ private readonly prewarmed;
1045
1048
  private readonly view;
1046
1049
  private readonly ledger;
1047
1050
  private readonly queue;
@@ -1086,6 +1089,9 @@ declare class PaymentsFacade implements PaymentsV2 {
1086
1089
  connectionStatus(): ConnectionStatus;
1087
1090
  /** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
1088
1091
  pendingTransfers(): Promise<PendingTransfer[]>;
1092
+ prewarmSend(request: SendRequest): Promise<void>;
1093
+ discardPrewarm(): void;
1094
+ private warmDeps;
1089
1095
  send(request: SendRequest): Promise<TransferResult>;
1090
1096
  receive(): Promise<{
1091
1097
  transfers: IncomingTransfer[];
@@ -1142,12 +1148,10 @@ declare class PaymentsFacade implements PaymentsV2 {
1142
1148
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
1143
1149
  private replayMints;
1144
1150
  private replayMint;
1145
- private mintParams;
1146
1151
  private tokenInServerInventory;
1147
1152
  private engine;
1148
1153
  private newId;
1149
1154
  private seedHeldStates;
1150
- private toUiToken;
1151
1155
  private track;
1152
1156
  private trackTail;
1153
1157
  }
@@ -1600,6 +1600,90 @@ async function splitOpIndex(deps, envelope) {
1600
1600
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
1601
1601
  }
1602
1602
 
1603
+ // modules/payments-v2/mint-params.ts
1604
+ function mintParams(recipientPubkey, coinId, amount) {
1605
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1606
+ }
1607
+
1608
+ // modules/payments-v2/prewarm-cache.ts
1609
+ var PrewarmCache = class _PrewarmCache {
1610
+ entries = /* @__PURE__ */ new Map();
1611
+ generation = 0;
1612
+ static key(tokenId, stateHash) {
1613
+ return `${tokenId}:${stateHash}`;
1614
+ }
1615
+ /** Claim the cache for a warm about to start; supersedes any warm already running. */
1616
+ begin() {
1617
+ this.entries.clear();
1618
+ this.generation += 1;
1619
+ return this.generation;
1620
+ }
1621
+ /**
1622
+ * Publish a warm's result, unless it was cancelled or superseded while its
1623
+ * fetch was in flight — a cancel that only cleared what had landed so far
1624
+ * would be undone by the continuation of the very warm it cancelled.
1625
+ */
1626
+ commit(generation, warmed) {
1627
+ if (generation !== this.generation) return;
1628
+ for (const [tokenId, stateHash, bytes] of warmed) {
1629
+ this.entries.set(_PrewarmCache.key(tokenId, stateHash), bytes);
1630
+ }
1631
+ }
1632
+ get(tokenId, stateHash) {
1633
+ if (stateHash === void 0) return void 0;
1634
+ return this.entries.get(_PrewarmCache.key(tokenId, stateHash));
1635
+ }
1636
+ clear() {
1637
+ this.entries.clear();
1638
+ this.generation += 1;
1639
+ }
1640
+ get size() {
1641
+ return this.entries.size;
1642
+ }
1643
+ };
1644
+ async function warmSendSources(deps, request) {
1645
+ const ids = deps.queue.previewSelection(request);
1646
+ if (ids.length === 0) return;
1647
+ const generation = deps.cache.begin();
1648
+ const at = new Map(ids.map((id) => [id, deps.view.stateHashOf(id)]));
1649
+ let blobs;
1650
+ try {
1651
+ blobs = await deps.storagePort.getBlobs([...ids]);
1652
+ } catch {
1653
+ return;
1654
+ }
1655
+ const warmed = [];
1656
+ for (const [tokenId, bytes] of blobs) {
1657
+ const stateHash = at.get(tokenId);
1658
+ if (stateHash !== void 0) warmed.push([tokenId, stateHash, bytes]);
1659
+ }
1660
+ deps.cache.commit(generation, warmed);
1661
+ }
1662
+ async function takeSourceBlobs(deps, sourceIds) {
1663
+ const blobs = /* @__PURE__ */ new Map();
1664
+ const missing = [];
1665
+ let trustCache = deps.cache.size > 0;
1666
+ if (trustCache) {
1667
+ try {
1668
+ await deps.view.delta();
1669
+ } catch {
1670
+ trustCache = false;
1671
+ }
1672
+ }
1673
+ for (const tokenId of sourceIds) {
1674
+ const warm = trustCache ? deps.cache.get(tokenId, deps.view.stateHashOf(tokenId)) : void 0;
1675
+ if (warm === void 0) missing.push(tokenId);
1676
+ else blobs.set(tokenId, warm);
1677
+ }
1678
+ deps.cache.clear();
1679
+ if (missing.length > 0) {
1680
+ for (const [tokenId, bytes] of await deps.storagePort.getBlobs(missing)) {
1681
+ blobs.set(tokenId, bytes);
1682
+ }
1683
+ }
1684
+ return blobs;
1685
+ }
1686
+
1603
1687
  // modules/payments-v2/inventory/presentation.ts
1604
1688
  function toToken(tokenId, snapshot, asset, registry, flags) {
1605
1689
  const iconUrl = registry.getIconUrl(asset.coinId);
@@ -1686,6 +1770,13 @@ function priceOne(asset, quote) {
1686
1770
  fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
1687
1771
  };
1688
1772
  }
1773
+ function transferringToken(tokenId, coinId, amount, registry, now) {
1774
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
1775
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
1776
+ transferring: true,
1777
+ suspectedSpent: false
1778
+ });
1779
+ }
1689
1780
 
1690
1781
  // modules/payments-v2/machine/payload.ts
1691
1782
  function buildPayload(recipientPubkey, request, spend, spentStates) {
@@ -2774,6 +2865,15 @@ var InventoryView = class {
2774
2865
  }
2775
2866
  return out;
2776
2867
  }
2868
+ /**
2869
+ * Current mirrored state of an ACTIVE token, or undefined when it is unknown
2870
+ * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
2871
+ * only reusable while the token still sits at the state it was fetched at.
2872
+ */
2873
+ stateHashOf(tokenId) {
2874
+ const entry = this.mirror.get(tokenId);
2875
+ return entry !== void 0 && entry.status === "active" ? entry.stateHash : void 0;
2876
+ }
2777
2877
  tokens(registry, filter) {
2778
2878
  const out = [];
2779
2879
  for (const [tokenId, entry] of this.mirror) {
@@ -3785,6 +3885,31 @@ var SpendQueue = class {
3785
3885
  }
3786
3886
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
3787
3887
  }
3888
+ /**
3889
+ * The sources `plan()` WOULD pick right now, reserving nothing and queueing
3890
+ * nothing — a read-only probe used to warm blobs while the user is still
3891
+ * looking at the confirm screen.
3892
+ *
3893
+ * Deliberately side-effect free: reserving here would pin the user's balance
3894
+ * across think-time and strand it if they never press send. The consequence is
3895
+ * that the real `plan()` may pick differently (inventory moves, another send
3896
+ * reserves first); callers must treat the result as a HINT and tolerate a miss,
3897
+ * never as a commitment that these sources will be spent.
3898
+ */
3899
+ previewSelection(request) {
3900
+ if (this.destroyed) return [];
3901
+ let amount;
3902
+ try {
3903
+ amount = parseAmount(request.amount);
3904
+ } catch {
3905
+ return [];
3906
+ }
3907
+ const { freeView, freeTotal } = this.freeView(request.coinId);
3908
+ if (freeTotal < amount) return [];
3909
+ const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
3910
+ if (plan === null) return [];
3911
+ return [...plan.direct, ...plan.split !== void 0 ? [plan.split.tokenId] : []];
3912
+ }
3788
3913
  // Coverage math: free + Σ declared expected change of in-flight sends —
3789
3914
  // never the full amount of in-flight sources (§5.3).
3790
3915
  declareExpectedChange(transferId, coinId, amount) {
@@ -4134,6 +4259,7 @@ var PaymentsFacade = class {
4134
4259
  now: () => this.nowMs()
4135
4260
  });
4136
4261
  }
4262
+ prewarmed = new PrewarmCache();
4137
4263
  view;
4138
4264
  ledger;
4139
4265
  queue;
@@ -4227,6 +4353,15 @@ var PaymentsFacade = class {
4227
4353
  return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
4228
4354
  }
4229
4355
  // ── money movement ─────────────────────────────────────────────────────────
4356
+ async prewarmSend(request) {
4357
+ await warmSendSources(this.warmDeps(), request);
4358
+ }
4359
+ discardPrewarm() {
4360
+ this.prewarmed.clear();
4361
+ }
4362
+ warmDeps() {
4363
+ return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
4364
+ }
4230
4365
  send(request) {
4231
4366
  return this.track(this.sendOutcome(request));
4232
4367
  }
@@ -4436,7 +4571,7 @@ var PaymentsFacade = class {
4436
4571
  }
4437
4572
  async materialize(transferId, recipientPubkey, request, spend, sourceIds) {
4438
4573
  const engine = this.engine();
4439
- const blobs = await this.deps.storagePort.getBlobs([...sourceIds]);
4574
+ const blobs = await takeSourceBlobs(this.warmDeps(), sourceIds);
4440
4575
  const sources = /* @__PURE__ */ new Map();
4441
4576
  const sourceTokens = [];
4442
4577
  const spentStates = {};
@@ -4449,7 +4584,7 @@ var PaymentsFacade = class {
4449
4584
  const keys = await engine.deliveryKeys(bytes);
4450
4585
  spentStates[tokenId] = { local: keys.stateHash, protocol: keys.stateHash };
4451
4586
  sources.set(tokenId, token);
4452
- sourceTokens.push(this.toUiToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId)));
4587
+ sourceTokens.push(transferringToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId), this.deps.registry, this.nowMs()));
4453
4588
  }
4454
4589
  const payload = buildPayload(recipientPubkey, request, spend, spentStates);
4455
4590
  const plan = {
@@ -4593,7 +4728,7 @@ var PaymentsFacade = class {
4593
4728
  await this.machineStores.mintJournal.upsert(entry);
4594
4729
  let token;
4595
4730
  try {
4596
- token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
4731
+ token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
4597
4732
  } catch (err) {
4598
4733
  this.heartbeat.arm();
4599
4734
  return { success: false, error: messageOf(err) };
@@ -4652,7 +4787,7 @@ var PaymentsFacade = class {
4652
4787
  });
4653
4788
  return false;
4654
4789
  }
4655
- const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
4790
+ const token = await engine.mint(mintParams(this.ownPubkeyBytes, entry.coinId, BigInt(entry.amount)), {
4656
4791
  transferId: entry.mintId
4657
4792
  });
4658
4793
  if (token.blob.tokenId !== entry.tokenId) {
@@ -4661,12 +4796,6 @@ var PaymentsFacade = class {
4661
4796
  await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
4662
4797
  return true;
4663
4798
  }
4664
- mintParams(coinId, amount) {
4665
- return {
4666
- recipientPubkey: this.ownPubkeyBytes,
4667
- value: { assets: [{ coinId, amount }] }
4668
- };
4669
- }
4670
4799
  async tokenInServerInventory(tokenId) {
4671
4800
  let page = await this.deps.storagePort.listInventory();
4672
4801
  for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
@@ -4695,12 +4824,6 @@ var PaymentsFacade = class {
4695
4824
  page = await this.deps.storagePort.listInventory(page.cursor);
4696
4825
  }
4697
4826
  }
4698
- toUiToken(tokenId, coinId, amount) {
4699
- const now = this.nowMs();
4700
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
4701
- const flags = { transferring: true, suspectedSpent: false };
4702
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, this.deps.registry, flags);
4703
- }
4704
4827
  track(op) {
4705
4828
  const settled = op.then(() => void 0, () => void 0);
4706
4829
  this.pendingOps.add(settled);