@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.
@@ -167,6 +167,8 @@ interface PendingTransfer {
167
167
  }
168
168
  type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
169
169
  interface PaymentsV2 {
170
+ prewarmSend(request: SendRequest): Promise<void>;
171
+ discardPrewarm(): void;
170
172
  assets(coinId?: string): Promise<Asset[]>;
171
173
  tokens(filter?: {
172
174
  coinId?: string;
@@ -167,6 +167,8 @@ interface PendingTransfer {
167
167
  }
168
168
  type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
169
169
  interface PaymentsV2 {
170
+ prewarmSend(request: SendRequest): Promise<void>;
171
+ discardPrewarm(): void;
170
172
  assets(coinId?: string): Promise<Asset[]>;
171
173
  tokens(filter?: {
172
174
  coinId?: string;
@@ -12273,6 +12273,90 @@ async function splitOpIndex(deps, envelope) {
12273
12273
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
12274
12274
  }
12275
12275
 
12276
+ // modules/payments-v2/mint-params.ts
12277
+ function mintParams(recipientPubkey, coinId, amount) {
12278
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12279
+ }
12280
+
12281
+ // modules/payments-v2/prewarm-cache.ts
12282
+ var PrewarmCache = class _PrewarmCache {
12283
+ entries = /* @__PURE__ */ new Map();
12284
+ generation = 0;
12285
+ static key(tokenId, stateHash) {
12286
+ return `${tokenId}:${stateHash}`;
12287
+ }
12288
+ /** Claim the cache for a warm about to start; supersedes any warm already running. */
12289
+ begin() {
12290
+ this.entries.clear();
12291
+ this.generation += 1;
12292
+ return this.generation;
12293
+ }
12294
+ /**
12295
+ * Publish a warm's result, unless it was cancelled or superseded while its
12296
+ * fetch was in flight — a cancel that only cleared what had landed so far
12297
+ * would be undone by the continuation of the very warm it cancelled.
12298
+ */
12299
+ commit(generation, warmed) {
12300
+ if (generation !== this.generation) return;
12301
+ for (const [tokenId, stateHash, bytes] of warmed) {
12302
+ this.entries.set(_PrewarmCache.key(tokenId, stateHash), bytes);
12303
+ }
12304
+ }
12305
+ get(tokenId, stateHash) {
12306
+ if (stateHash === void 0) return void 0;
12307
+ return this.entries.get(_PrewarmCache.key(tokenId, stateHash));
12308
+ }
12309
+ clear() {
12310
+ this.entries.clear();
12311
+ this.generation += 1;
12312
+ }
12313
+ get size() {
12314
+ return this.entries.size;
12315
+ }
12316
+ };
12317
+ async function warmSendSources(deps, request) {
12318
+ const ids = deps.queue.previewSelection(request);
12319
+ if (ids.length === 0) return;
12320
+ const generation = deps.cache.begin();
12321
+ const at = new Map(ids.map((id) => [id, deps.view.stateHashOf(id)]));
12322
+ let blobs;
12323
+ try {
12324
+ blobs = await deps.storagePort.getBlobs([...ids]);
12325
+ } catch {
12326
+ return;
12327
+ }
12328
+ const warmed = [];
12329
+ for (const [tokenId, bytes] of blobs) {
12330
+ const stateHash = at.get(tokenId);
12331
+ if (stateHash !== void 0) warmed.push([tokenId, stateHash, bytes]);
12332
+ }
12333
+ deps.cache.commit(generation, warmed);
12334
+ }
12335
+ async function takeSourceBlobs(deps, sourceIds) {
12336
+ const blobs = /* @__PURE__ */ new Map();
12337
+ const missing = [];
12338
+ let trustCache = deps.cache.size > 0;
12339
+ if (trustCache) {
12340
+ try {
12341
+ await deps.view.delta();
12342
+ } catch {
12343
+ trustCache = false;
12344
+ }
12345
+ }
12346
+ for (const tokenId of sourceIds) {
12347
+ const warm = trustCache ? deps.cache.get(tokenId, deps.view.stateHashOf(tokenId)) : void 0;
12348
+ if (warm === void 0) missing.push(tokenId);
12349
+ else blobs.set(tokenId, warm);
12350
+ }
12351
+ deps.cache.clear();
12352
+ if (missing.length > 0) {
12353
+ for (const [tokenId, bytes] of await deps.storagePort.getBlobs(missing)) {
12354
+ blobs.set(tokenId, bytes);
12355
+ }
12356
+ }
12357
+ return blobs;
12358
+ }
12359
+
12276
12360
  // modules/payments-v2/inventory/presentation.ts
12277
12361
  function toToken(tokenId, snapshot, asset, registry, flags) {
12278
12362
  const iconUrl = registry.getIconUrl(asset.coinId);
@@ -12359,6 +12443,13 @@ function priceOne(asset, quote) {
12359
12443
  fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12360
12444
  };
12361
12445
  }
12446
+ function transferringToken(tokenId, coinId, amount, registry, now) {
12447
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
12448
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12449
+ transferring: true,
12450
+ suspectedSpent: false
12451
+ });
12452
+ }
12362
12453
 
12363
12454
  // modules/payments-v2/machine/payload.ts
12364
12455
  function buildPayload(recipientPubkey, request, spend, spentStates) {
@@ -12597,6 +12688,15 @@ var InventoryView = class {
12597
12688
  }
12598
12689
  return out;
12599
12690
  }
12691
+ /**
12692
+ * Current mirrored state of an ACTIVE token, or undefined when it is unknown
12693
+ * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
12694
+ * only reusable while the token still sits at the state it was fetched at.
12695
+ */
12696
+ stateHashOf(tokenId) {
12697
+ const entry = this.mirror.get(tokenId);
12698
+ return entry !== void 0 && entry.status === "active" ? entry.stateHash : void 0;
12699
+ }
12600
12700
  tokens(registry, filter) {
12601
12701
  const out = [];
12602
12702
  for (const [tokenId, entry] of this.mirror) {
@@ -12784,6 +12884,7 @@ var InventoryView = class {
12784
12884
 
12785
12885
  // modules/payments-v2/receive/Receive.ts
12786
12886
  var ACK_BATCH_SIZE = 200;
12887
+ var REFRESH_INTERVAL_MS = 750;
12787
12888
  var POLL_INTERVAL_MS = 3e4;
12788
12889
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
12789
12890
  function receivedDedupKey(tokenId, stateHash) {
@@ -12800,6 +12901,7 @@ var Receive = class {
12800
12901
  drainFlight = new SingleFlight();
12801
12902
  pollTimer = null;
12802
12903
  unsubscribeWake = null;
12904
+ lastRefreshAt = 0;
12803
12905
  drainOnce() {
12804
12906
  return this.drainFlight.run(() => this.doDrain());
12805
12907
  }
@@ -12817,6 +12919,14 @@ var Receive = class {
12817
12919
  this.unsubscribeWake?.();
12818
12920
  this.unsubscribeWake = null;
12819
12921
  }
12922
+ /** Ask for a mirror refresh at most once per REFRESH_INTERVAL_MS. */
12923
+ maybeRefresh(deps) {
12924
+ if (deps.refreshView === void 0) return;
12925
+ const now = deps.now?.() ?? Date.now();
12926
+ if (now - this.lastRefreshAt < REFRESH_INTERVAL_MS) return;
12927
+ this.lastRefreshAt = now;
12928
+ deps.refreshView();
12929
+ }
12820
12930
  async doDrain() {
12821
12931
  const deps = this.deps;
12822
12932
  const engine = deps.engine();
@@ -12830,6 +12940,7 @@ var Receive = class {
12830
12940
  for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
12831
12941
  await this.processEntry(deps, engine, entry, pending, stored);
12832
12942
  if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
12943
+ if (stored.length > 0) this.maybeRefresh(deps);
12833
12944
  }
12834
12945
  await flushAcks(deps, pending, pageEpoch);
12835
12946
  const served = deps.delivery.incomingEpoch();
@@ -13516,6 +13627,31 @@ var SpendQueue = class {
13516
13627
  }
13517
13628
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
13518
13629
  }
13630
+ /**
13631
+ * The sources `plan()` WOULD pick right now, reserving nothing and queueing
13632
+ * nothing — a read-only probe used to warm blobs while the user is still
13633
+ * looking at the confirm screen.
13634
+ *
13635
+ * Deliberately side-effect free: reserving here would pin the user's balance
13636
+ * across think-time and strand it if they never press send. The consequence is
13637
+ * that the real `plan()` may pick differently (inventory moves, another send
13638
+ * reserves first); callers must treat the result as a HINT and tolerate a miss,
13639
+ * never as a commitment that these sources will be spent.
13640
+ */
13641
+ previewSelection(request) {
13642
+ if (this.destroyed) return [];
13643
+ let amount;
13644
+ try {
13645
+ amount = parseAmount(request.amount);
13646
+ } catch {
13647
+ return [];
13648
+ }
13649
+ const { freeView, freeTotal } = this.freeView(request.coinId);
13650
+ if (freeTotal < amount) return [];
13651
+ const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
13652
+ if (plan === null) return [];
13653
+ return [...plan.direct, ...plan.split !== void 0 ? [plan.split.tokenId] : []];
13654
+ }
13519
13655
  // Coverage math: free + Σ declared expected change of in-flight sends —
13520
13656
  // never the full amount of in-flight sources (§5.3).
13521
13657
  declareExpectedChange(transferId, coinId, amount) {
@@ -13704,7 +13840,7 @@ function composeFacadeParts(deps, hooks) {
13704
13840
  machineDeps,
13705
13841
  machine: new TransferMachine(machineDeps),
13706
13842
  heldStates,
13707
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
13843
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
13708
13844
  requests: buildRequests(deps, hooks),
13709
13845
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
13710
13846
  };
@@ -13771,7 +13907,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
13771
13907
  }
13772
13908
  };
13773
13909
  }
13774
- function buildReceive(deps, hooks, historyStore, heldStates) {
13910
+ function buildReceive(deps, hooks, historyStore, heldStates, view) {
13775
13911
  return new Receive({
13776
13912
  delivery: deps.deliveryPort,
13777
13913
  engine: () => hooks.engine(),
@@ -13785,6 +13921,11 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
13785
13921
  registry: deps.registry,
13786
13922
  recordReceived: (record) => recordReceived(historyStore, record),
13787
13923
  emit: (event, transfer) => deps.emit(event, transfer),
13924
+ // The mirror is the balance the wallet renders; refresh it as the drain runs
13925
+ // so a multi-token receive lands progressively instead of all at the end.
13926
+ refreshView: () => {
13927
+ void view.delta().catch(() => void 0);
13928
+ },
13788
13929
  attention: (transferId, code, detail) => {
13789
13930
  deps.emit(
13790
13931
  "transfer:attention",
@@ -13865,6 +14006,7 @@ var PaymentsFacade = class {
13865
14006
  now: () => this.nowMs()
13866
14007
  });
13867
14008
  }
14009
+ prewarmed = new PrewarmCache();
13868
14010
  view;
13869
14011
  ledger;
13870
14012
  queue;
@@ -13958,6 +14100,15 @@ var PaymentsFacade = class {
13958
14100
  return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
13959
14101
  }
13960
14102
  // ── money movement ─────────────────────────────────────────────────────────
14103
+ async prewarmSend(request) {
14104
+ await warmSendSources(this.warmDeps(), request);
14105
+ }
14106
+ discardPrewarm() {
14107
+ this.prewarmed.clear();
14108
+ }
14109
+ warmDeps() {
14110
+ return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
14111
+ }
13961
14112
  send(request) {
13962
14113
  return this.track(this.sendOutcome(request));
13963
14114
  }
@@ -14167,7 +14318,7 @@ var PaymentsFacade = class {
14167
14318
  }
14168
14319
  async materialize(transferId, recipientPubkey, request, spend, sourceIds) {
14169
14320
  const engine = this.engine();
14170
- const blobs = await this.deps.storagePort.getBlobs([...sourceIds]);
14321
+ const blobs = await takeSourceBlobs(this.warmDeps(), sourceIds);
14171
14322
  const sources = /* @__PURE__ */ new Map();
14172
14323
  const sourceTokens = [];
14173
14324
  const spentStates = {};
@@ -14180,7 +14331,7 @@ var PaymentsFacade = class {
14180
14331
  const keys = await engine.deliveryKeys(bytes);
14181
14332
  spentStates[tokenId] = { local: keys.stateHash, protocol: keys.stateHash };
14182
14333
  sources.set(tokenId, token);
14183
- sourceTokens.push(this.toUiToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId)));
14334
+ sourceTokens.push(transferringToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId), this.deps.registry, this.nowMs()));
14184
14335
  }
14185
14336
  const payload = buildPayload(recipientPubkey, request, spend, spentStates);
14186
14337
  const plan = {
@@ -14324,7 +14475,7 @@ var PaymentsFacade = class {
14324
14475
  await this.machineStores.mintJournal.upsert(entry);
14325
14476
  let token;
14326
14477
  try {
14327
- token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
14478
+ token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
14328
14479
  } catch (err) {
14329
14480
  this.heartbeat.arm();
14330
14481
  return { success: false, error: messageOf(err) };
@@ -14383,7 +14534,7 @@ var PaymentsFacade = class {
14383
14534
  });
14384
14535
  return false;
14385
14536
  }
14386
- const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
14537
+ const token = await engine.mint(mintParams(this.ownPubkeyBytes, entry.coinId, BigInt(entry.amount)), {
14387
14538
  transferId: entry.mintId
14388
14539
  });
14389
14540
  if (token.blob.tokenId !== entry.tokenId) {
@@ -14392,12 +14543,6 @@ var PaymentsFacade = class {
14392
14543
  await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
14393
14544
  return true;
14394
14545
  }
14395
- mintParams(coinId, amount) {
14396
- return {
14397
- recipientPubkey: this.ownPubkeyBytes,
14398
- value: { assets: [{ coinId, amount }] }
14399
- };
14400
- }
14401
14546
  async tokenInServerInventory(tokenId) {
14402
14547
  let page = await this.deps.storagePort.listInventory();
14403
14548
  for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
@@ -14426,12 +14571,6 @@ var PaymentsFacade = class {
14426
14571
  page = await this.deps.storagePort.listInventory(page.cursor);
14427
14572
  }
14428
14573
  }
14429
- toUiToken(tokenId, coinId, amount) {
14430
- const now = this.nowMs();
14431
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
14432
- const flags = { transferring: true, suspectedSpent: false };
14433
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, this.deps.registry, flags);
14434
- }
14435
14574
  track(op) {
14436
14575
  const settled = op.then(() => void 0, () => void 0);
14437
14576
  this.pendingOps.add(settled);