@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.
@@ -12359,6 +12359,90 @@ async function splitOpIndex(deps, envelope) {
12359
12359
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
12360
12360
  }
12361
12361
 
12362
+ // modules/payments-v2/mint-params.ts
12363
+ function mintParams(recipientPubkey, coinId, amount) {
12364
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12365
+ }
12366
+
12367
+ // modules/payments-v2/prewarm-cache.ts
12368
+ var PrewarmCache = class _PrewarmCache {
12369
+ entries = /* @__PURE__ */ new Map();
12370
+ generation = 0;
12371
+ static key(tokenId, stateHash) {
12372
+ return `${tokenId}:${stateHash}`;
12373
+ }
12374
+ /** Claim the cache for a warm about to start; supersedes any warm already running. */
12375
+ begin() {
12376
+ this.entries.clear();
12377
+ this.generation += 1;
12378
+ return this.generation;
12379
+ }
12380
+ /**
12381
+ * Publish a warm's result, unless it was cancelled or superseded while its
12382
+ * fetch was in flight — a cancel that only cleared what had landed so far
12383
+ * would be undone by the continuation of the very warm it cancelled.
12384
+ */
12385
+ commit(generation, warmed) {
12386
+ if (generation !== this.generation) return;
12387
+ for (const [tokenId, stateHash, bytes] of warmed) {
12388
+ this.entries.set(_PrewarmCache.key(tokenId, stateHash), bytes);
12389
+ }
12390
+ }
12391
+ get(tokenId, stateHash) {
12392
+ if (stateHash === void 0) return void 0;
12393
+ return this.entries.get(_PrewarmCache.key(tokenId, stateHash));
12394
+ }
12395
+ clear() {
12396
+ this.entries.clear();
12397
+ this.generation += 1;
12398
+ }
12399
+ get size() {
12400
+ return this.entries.size;
12401
+ }
12402
+ };
12403
+ async function warmSendSources(deps, request) {
12404
+ const ids = deps.queue.previewSelection(request);
12405
+ if (ids.length === 0) return;
12406
+ const generation = deps.cache.begin();
12407
+ const at = new Map(ids.map((id) => [id, deps.view.stateHashOf(id)]));
12408
+ let blobs;
12409
+ try {
12410
+ blobs = await deps.storagePort.getBlobs([...ids]);
12411
+ } catch {
12412
+ return;
12413
+ }
12414
+ const warmed = [];
12415
+ for (const [tokenId, bytes] of blobs) {
12416
+ const stateHash = at.get(tokenId);
12417
+ if (stateHash !== void 0) warmed.push([tokenId, stateHash, bytes]);
12418
+ }
12419
+ deps.cache.commit(generation, warmed);
12420
+ }
12421
+ async function takeSourceBlobs(deps, sourceIds) {
12422
+ const blobs = /* @__PURE__ */ new Map();
12423
+ const missing = [];
12424
+ let trustCache = deps.cache.size > 0;
12425
+ if (trustCache) {
12426
+ try {
12427
+ await deps.view.delta();
12428
+ } catch {
12429
+ trustCache = false;
12430
+ }
12431
+ }
12432
+ for (const tokenId of sourceIds) {
12433
+ const warm = trustCache ? deps.cache.get(tokenId, deps.view.stateHashOf(tokenId)) : void 0;
12434
+ if (warm === void 0) missing.push(tokenId);
12435
+ else blobs.set(tokenId, warm);
12436
+ }
12437
+ deps.cache.clear();
12438
+ if (missing.length > 0) {
12439
+ for (const [tokenId, bytes] of await deps.storagePort.getBlobs(missing)) {
12440
+ blobs.set(tokenId, bytes);
12441
+ }
12442
+ }
12443
+ return blobs;
12444
+ }
12445
+
12362
12446
  // modules/payments-v2/inventory/presentation.ts
12363
12447
  function toToken(tokenId, snapshot, asset, registry, flags) {
12364
12448
  const iconUrl = registry.getIconUrl(asset.coinId);
@@ -12445,6 +12529,13 @@ function priceOne(asset, quote) {
12445
12529
  fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12446
12530
  };
12447
12531
  }
12532
+ function transferringToken(tokenId, coinId, amount, registry, now) {
12533
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
12534
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12535
+ transferring: true,
12536
+ suspectedSpent: false
12537
+ });
12538
+ }
12448
12539
 
12449
12540
  // modules/payments-v2/machine/payload.ts
12450
12541
  function buildPayload(recipientPubkey, request, spend, spentStates) {
@@ -12683,6 +12774,15 @@ var InventoryView = class {
12683
12774
  }
12684
12775
  return out;
12685
12776
  }
12777
+ /**
12778
+ * Current mirrored state of an ACTIVE token, or undefined when it is unknown
12779
+ * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
12780
+ * only reusable while the token still sits at the state it was fetched at.
12781
+ */
12782
+ stateHashOf(tokenId) {
12783
+ const entry = this.mirror.get(tokenId);
12784
+ return entry !== void 0 && entry.status === "active" ? entry.stateHash : void 0;
12785
+ }
12686
12786
  tokens(registry, filter) {
12687
12787
  const out = [];
12688
12788
  for (const [tokenId, entry] of this.mirror) {
@@ -12870,6 +12970,7 @@ var InventoryView = class {
12870
12970
 
12871
12971
  // modules/payments-v2/receive/Receive.ts
12872
12972
  var ACK_BATCH_SIZE = 200;
12973
+ var REFRESH_INTERVAL_MS = 750;
12873
12974
  var POLL_INTERVAL_MS = 3e4;
12874
12975
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
12875
12976
  function receivedDedupKey(tokenId, stateHash) {
@@ -12886,6 +12987,7 @@ var Receive = class {
12886
12987
  drainFlight = new SingleFlight();
12887
12988
  pollTimer = null;
12888
12989
  unsubscribeWake = null;
12990
+ lastRefreshAt = 0;
12889
12991
  drainOnce() {
12890
12992
  return this.drainFlight.run(() => this.doDrain());
12891
12993
  }
@@ -12903,6 +13005,14 @@ var Receive = class {
12903
13005
  this.unsubscribeWake?.();
12904
13006
  this.unsubscribeWake = null;
12905
13007
  }
13008
+ /** Ask for a mirror refresh at most once per REFRESH_INTERVAL_MS. */
13009
+ maybeRefresh(deps) {
13010
+ if (deps.refreshView === void 0) return;
13011
+ const now = deps.now?.() ?? Date.now();
13012
+ if (now - this.lastRefreshAt < REFRESH_INTERVAL_MS) return;
13013
+ this.lastRefreshAt = now;
13014
+ deps.refreshView();
13015
+ }
12906
13016
  async doDrain() {
12907
13017
  const deps = this.deps;
12908
13018
  const engine = deps.engine();
@@ -12916,6 +13026,7 @@ var Receive = class {
12916
13026
  for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
12917
13027
  await this.processEntry(deps, engine, entry, pending, stored);
12918
13028
  if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
13029
+ if (stored.length > 0) this.maybeRefresh(deps);
12919
13030
  }
12920
13031
  await flushAcks(deps, pending, pageEpoch);
12921
13032
  const served = deps.delivery.incomingEpoch();
@@ -13602,6 +13713,31 @@ var SpendQueue = class {
13602
13713
  }
13603
13714
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
13604
13715
  }
13716
+ /**
13717
+ * The sources `plan()` WOULD pick right now, reserving nothing and queueing
13718
+ * nothing — a read-only probe used to warm blobs while the user is still
13719
+ * looking at the confirm screen.
13720
+ *
13721
+ * Deliberately side-effect free: reserving here would pin the user's balance
13722
+ * across think-time and strand it if they never press send. The consequence is
13723
+ * that the real `plan()` may pick differently (inventory moves, another send
13724
+ * reserves first); callers must treat the result as a HINT and tolerate a miss,
13725
+ * never as a commitment that these sources will be spent.
13726
+ */
13727
+ previewSelection(request) {
13728
+ if (this.destroyed) return [];
13729
+ let amount;
13730
+ try {
13731
+ amount = parseAmount(request.amount);
13732
+ } catch {
13733
+ return [];
13734
+ }
13735
+ const { freeView, freeTotal } = this.freeView(request.coinId);
13736
+ if (freeTotal < amount) return [];
13737
+ const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
13738
+ if (plan === null) return [];
13739
+ return [...plan.direct, ...plan.split !== void 0 ? [plan.split.tokenId] : []];
13740
+ }
13605
13741
  // Coverage math: free + Σ declared expected change of in-flight sends —
13606
13742
  // never the full amount of in-flight sources (§5.3).
13607
13743
  declareExpectedChange(transferId, coinId, amount) {
@@ -13790,7 +13926,7 @@ function composeFacadeParts(deps, hooks) {
13790
13926
  machineDeps,
13791
13927
  machine: new TransferMachine(machineDeps),
13792
13928
  heldStates,
13793
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
13929
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
13794
13930
  requests: buildRequests(deps, hooks),
13795
13931
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
13796
13932
  };
@@ -13857,7 +13993,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
13857
13993
  }
13858
13994
  };
13859
13995
  }
13860
- function buildReceive(deps, hooks, historyStore, heldStates) {
13996
+ function buildReceive(deps, hooks, historyStore, heldStates, view) {
13861
13997
  return new Receive({
13862
13998
  delivery: deps.deliveryPort,
13863
13999
  engine: () => hooks.engine(),
@@ -13871,6 +14007,11 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
13871
14007
  registry: deps.registry,
13872
14008
  recordReceived: (record) => recordReceived(historyStore, record),
13873
14009
  emit: (event, transfer) => deps.emit(event, transfer),
14010
+ // The mirror is the balance the wallet renders; refresh it as the drain runs
14011
+ // so a multi-token receive lands progressively instead of all at the end.
14012
+ refreshView: () => {
14013
+ void view.delta().catch(() => void 0);
14014
+ },
13874
14015
  attention: (transferId, code, detail) => {
13875
14016
  deps.emit(
13876
14017
  "transfer:attention",
@@ -13951,6 +14092,7 @@ var PaymentsFacade = class {
13951
14092
  now: () => this.nowMs()
13952
14093
  });
13953
14094
  }
14095
+ prewarmed = new PrewarmCache();
13954
14096
  view;
13955
14097
  ledger;
13956
14098
  queue;
@@ -14044,6 +14186,15 @@ var PaymentsFacade = class {
14044
14186
  return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
14045
14187
  }
14046
14188
  // ── money movement ─────────────────────────────────────────────────────────
14189
+ async prewarmSend(request) {
14190
+ await warmSendSources(this.warmDeps(), request);
14191
+ }
14192
+ discardPrewarm() {
14193
+ this.prewarmed.clear();
14194
+ }
14195
+ warmDeps() {
14196
+ return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
14197
+ }
14047
14198
  send(request) {
14048
14199
  return this.track(this.sendOutcome(request));
14049
14200
  }
@@ -14253,7 +14404,7 @@ var PaymentsFacade = class {
14253
14404
  }
14254
14405
  async materialize(transferId, recipientPubkey, request, spend, sourceIds) {
14255
14406
  const engine = this.engine();
14256
- const blobs = await this.deps.storagePort.getBlobs([...sourceIds]);
14407
+ const blobs = await takeSourceBlobs(this.warmDeps(), sourceIds);
14257
14408
  const sources = /* @__PURE__ */ new Map();
14258
14409
  const sourceTokens = [];
14259
14410
  const spentStates = {};
@@ -14266,7 +14417,7 @@ var PaymentsFacade = class {
14266
14417
  const keys = await engine.deliveryKeys(bytes);
14267
14418
  spentStates[tokenId] = { local: keys.stateHash, protocol: keys.stateHash };
14268
14419
  sources.set(tokenId, token);
14269
- sourceTokens.push(this.toUiToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId)));
14420
+ sourceTokens.push(transferringToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId), this.deps.registry, this.nowMs()));
14270
14421
  }
14271
14422
  const payload = buildPayload(recipientPubkey, request, spend, spentStates);
14272
14423
  const plan = {
@@ -14410,7 +14561,7 @@ var PaymentsFacade = class {
14410
14561
  await this.machineStores.mintJournal.upsert(entry);
14411
14562
  let token;
14412
14563
  try {
14413
- token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
14564
+ token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
14414
14565
  } catch (err) {
14415
14566
  this.heartbeat.arm();
14416
14567
  return { success: false, error: messageOf(err) };
@@ -14469,7 +14620,7 @@ var PaymentsFacade = class {
14469
14620
  });
14470
14621
  return false;
14471
14622
  }
14472
- const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
14623
+ const token = await engine.mint(mintParams(this.ownPubkeyBytes, entry.coinId, BigInt(entry.amount)), {
14473
14624
  transferId: entry.mintId
14474
14625
  });
14475
14626
  if (token.blob.tokenId !== entry.tokenId) {
@@ -14478,12 +14629,6 @@ var PaymentsFacade = class {
14478
14629
  await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
14479
14630
  return true;
14480
14631
  }
14481
- mintParams(coinId, amount) {
14482
- return {
14483
- recipientPubkey: this.ownPubkeyBytes,
14484
- value: { assets: [{ coinId, amount }] }
14485
- };
14486
- }
14487
14632
  async tokenInServerInventory(tokenId) {
14488
14633
  let page = await this.deps.storagePort.listInventory();
14489
14634
  for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
@@ -14512,12 +14657,6 @@ var PaymentsFacade = class {
14512
14657
  page = await this.deps.storagePort.listInventory(page.cursor);
14513
14658
  }
14514
14659
  }
14515
- toUiToken(tokenId, coinId, amount) {
14516
- const now = this.nowMs();
14517
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
14518
- const flags = { transferring: true, suspectedSpent: false };
14519
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, this.deps.registry, flags);
14520
- }
14521
14660
  track(op) {
14522
14661
  const settled = op.then(() => void 0, () => void 0);
14523
14662
  this.pendingOps.add(settled);