@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.
package/dist/index.d.cts CHANGED
@@ -166,6 +166,8 @@ interface PendingTransfer {
166
166
  }
167
167
  type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
168
168
  interface PaymentsV2 {
169
+ prewarmSend(request: SendRequest): Promise<void>;
170
+ discardPrewarm(): void;
169
171
  assets(coinId?: string): Promise<Asset[]>;
170
172
  tokens(filter?: {
171
173
  coinId?: string;
package/dist/index.d.ts CHANGED
@@ -166,6 +166,8 @@ interface PendingTransfer {
166
166
  }
167
167
  type ConnectionStatus$1 = 'connected' | 'degraded' | 'offline';
168
168
  interface PaymentsV2 {
169
+ prewarmSend(request: SendRequest): Promise<void>;
170
+ discardPrewarm(): void;
169
171
  assets(coinId?: string): Promise<Asset[]>;
170
172
  tokens(filter?: {
171
173
  coinId?: string;
package/dist/index.js CHANGED
@@ -12289,6 +12289,90 @@ async function splitOpIndex(deps, envelope) {
12289
12289
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
12290
12290
  }
12291
12291
 
12292
+ // modules/payments-v2/mint-params.ts
12293
+ function mintParams(recipientPubkey, coinId, amount) {
12294
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12295
+ }
12296
+
12297
+ // modules/payments-v2/prewarm-cache.ts
12298
+ var PrewarmCache = class _PrewarmCache {
12299
+ entries = /* @__PURE__ */ new Map();
12300
+ generation = 0;
12301
+ static key(tokenId, stateHash) {
12302
+ return `${tokenId}:${stateHash}`;
12303
+ }
12304
+ /** Claim the cache for a warm about to start; supersedes any warm already running. */
12305
+ begin() {
12306
+ this.entries.clear();
12307
+ this.generation += 1;
12308
+ return this.generation;
12309
+ }
12310
+ /**
12311
+ * Publish a warm's result, unless it was cancelled or superseded while its
12312
+ * fetch was in flight — a cancel that only cleared what had landed so far
12313
+ * would be undone by the continuation of the very warm it cancelled.
12314
+ */
12315
+ commit(generation, warmed) {
12316
+ if (generation !== this.generation) return;
12317
+ for (const [tokenId, stateHash, bytes] of warmed) {
12318
+ this.entries.set(_PrewarmCache.key(tokenId, stateHash), bytes);
12319
+ }
12320
+ }
12321
+ get(tokenId, stateHash) {
12322
+ if (stateHash === void 0) return void 0;
12323
+ return this.entries.get(_PrewarmCache.key(tokenId, stateHash));
12324
+ }
12325
+ clear() {
12326
+ this.entries.clear();
12327
+ this.generation += 1;
12328
+ }
12329
+ get size() {
12330
+ return this.entries.size;
12331
+ }
12332
+ };
12333
+ async function warmSendSources(deps, request) {
12334
+ const ids = deps.queue.previewSelection(request);
12335
+ if (ids.length === 0) return;
12336
+ const generation = deps.cache.begin();
12337
+ const at = new Map(ids.map((id) => [id, deps.view.stateHashOf(id)]));
12338
+ let blobs;
12339
+ try {
12340
+ blobs = await deps.storagePort.getBlobs([...ids]);
12341
+ } catch {
12342
+ return;
12343
+ }
12344
+ const warmed = [];
12345
+ for (const [tokenId, bytes] of blobs) {
12346
+ const stateHash = at.get(tokenId);
12347
+ if (stateHash !== void 0) warmed.push([tokenId, stateHash, bytes]);
12348
+ }
12349
+ deps.cache.commit(generation, warmed);
12350
+ }
12351
+ async function takeSourceBlobs(deps, sourceIds) {
12352
+ const blobs = /* @__PURE__ */ new Map();
12353
+ const missing = [];
12354
+ let trustCache = deps.cache.size > 0;
12355
+ if (trustCache) {
12356
+ try {
12357
+ await deps.view.delta();
12358
+ } catch {
12359
+ trustCache = false;
12360
+ }
12361
+ }
12362
+ for (const tokenId of sourceIds) {
12363
+ const warm = trustCache ? deps.cache.get(tokenId, deps.view.stateHashOf(tokenId)) : void 0;
12364
+ if (warm === void 0) missing.push(tokenId);
12365
+ else blobs.set(tokenId, warm);
12366
+ }
12367
+ deps.cache.clear();
12368
+ if (missing.length > 0) {
12369
+ for (const [tokenId, bytes] of await deps.storagePort.getBlobs(missing)) {
12370
+ blobs.set(tokenId, bytes);
12371
+ }
12372
+ }
12373
+ return blobs;
12374
+ }
12375
+
12292
12376
  // modules/payments-v2/inventory/presentation.ts
12293
12377
  function toToken(tokenId, snapshot, asset, registry, flags) {
12294
12378
  const iconUrl = registry.getIconUrl(asset.coinId);
@@ -12375,6 +12459,13 @@ function priceOne(asset, quote) {
12375
12459
  fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12376
12460
  };
12377
12461
  }
12462
+ function transferringToken(tokenId, coinId, amount, registry, now) {
12463
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
12464
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12465
+ transferring: true,
12466
+ suspectedSpent: false
12467
+ });
12468
+ }
12378
12469
 
12379
12470
  // modules/payments-v2/machine/payload.ts
12380
12471
  function buildPayload(recipientPubkey, request, spend, spentStates) {
@@ -12613,6 +12704,15 @@ var InventoryView = class {
12613
12704
  }
12614
12705
  return out;
12615
12706
  }
12707
+ /**
12708
+ * Current mirrored state of an ACTIVE token, or undefined when it is unknown
12709
+ * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
12710
+ * only reusable while the token still sits at the state it was fetched at.
12711
+ */
12712
+ stateHashOf(tokenId) {
12713
+ const entry = this.mirror.get(tokenId);
12714
+ return entry !== void 0 && entry.status === "active" ? entry.stateHash : void 0;
12715
+ }
12616
12716
  tokens(registry, filter) {
12617
12717
  const out = [];
12618
12718
  for (const [tokenId, entry] of this.mirror) {
@@ -12800,6 +12900,7 @@ var InventoryView = class {
12800
12900
 
12801
12901
  // modules/payments-v2/receive/Receive.ts
12802
12902
  var ACK_BATCH_SIZE = 200;
12903
+ var REFRESH_INTERVAL_MS = 750;
12803
12904
  var POLL_INTERVAL_MS = 3e4;
12804
12905
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
12805
12906
  function receivedDedupKey(tokenId, stateHash) {
@@ -12816,6 +12917,7 @@ var Receive = class {
12816
12917
  drainFlight = new SingleFlight();
12817
12918
  pollTimer = null;
12818
12919
  unsubscribeWake = null;
12920
+ lastRefreshAt = 0;
12819
12921
  drainOnce() {
12820
12922
  return this.drainFlight.run(() => this.doDrain());
12821
12923
  }
@@ -12833,6 +12935,14 @@ var Receive = class {
12833
12935
  this.unsubscribeWake?.();
12834
12936
  this.unsubscribeWake = null;
12835
12937
  }
12938
+ /** Ask for a mirror refresh at most once per REFRESH_INTERVAL_MS. */
12939
+ maybeRefresh(deps) {
12940
+ if (deps.refreshView === void 0) return;
12941
+ const now = deps.now?.() ?? Date.now();
12942
+ if (now - this.lastRefreshAt < REFRESH_INTERVAL_MS) return;
12943
+ this.lastRefreshAt = now;
12944
+ deps.refreshView();
12945
+ }
12836
12946
  async doDrain() {
12837
12947
  const deps = this.deps;
12838
12948
  const engine = deps.engine();
@@ -12846,6 +12956,7 @@ var Receive = class {
12846
12956
  for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
12847
12957
  await this.processEntry(deps, engine, entry, pending, stored);
12848
12958
  if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
12959
+ if (stored.length > 0) this.maybeRefresh(deps);
12849
12960
  }
12850
12961
  await flushAcks(deps, pending, pageEpoch);
12851
12962
  const served = deps.delivery.incomingEpoch();
@@ -13532,6 +13643,31 @@ var SpendQueue = class {
13532
13643
  }
13533
13644
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
13534
13645
  }
13646
+ /**
13647
+ * The sources `plan()` WOULD pick right now, reserving nothing and queueing
13648
+ * nothing — a read-only probe used to warm blobs while the user is still
13649
+ * looking at the confirm screen.
13650
+ *
13651
+ * Deliberately side-effect free: reserving here would pin the user's balance
13652
+ * across think-time and strand it if they never press send. The consequence is
13653
+ * that the real `plan()` may pick differently (inventory moves, another send
13654
+ * reserves first); callers must treat the result as a HINT and tolerate a miss,
13655
+ * never as a commitment that these sources will be spent.
13656
+ */
13657
+ previewSelection(request) {
13658
+ if (this.destroyed) return [];
13659
+ let amount;
13660
+ try {
13661
+ amount = parseAmount(request.amount);
13662
+ } catch {
13663
+ return [];
13664
+ }
13665
+ const { freeView, freeTotal } = this.freeView(request.coinId);
13666
+ if (freeTotal < amount) return [];
13667
+ const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
13668
+ if (plan === null) return [];
13669
+ return [...plan.direct, ...plan.split !== void 0 ? [plan.split.tokenId] : []];
13670
+ }
13535
13671
  // Coverage math: free + Σ declared expected change of in-flight sends —
13536
13672
  // never the full amount of in-flight sources (§5.3).
13537
13673
  declareExpectedChange(transferId, coinId, amount) {
@@ -13720,7 +13856,7 @@ function composeFacadeParts(deps, hooks) {
13720
13856
  machineDeps,
13721
13857
  machine: new TransferMachine(machineDeps),
13722
13858
  heldStates,
13723
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
13859
+ receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, view),
13724
13860
  requests: buildRequests(deps, hooks),
13725
13861
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
13726
13862
  };
@@ -13787,7 +13923,7 @@ function buildMachineDeps(deps, hooks, historyStore) {
13787
13923
  }
13788
13924
  };
13789
13925
  }
13790
- function buildReceive(deps, hooks, historyStore, heldStates) {
13926
+ function buildReceive(deps, hooks, historyStore, heldStates, view) {
13791
13927
  return new Receive({
13792
13928
  delivery: deps.deliveryPort,
13793
13929
  engine: () => hooks.engine(),
@@ -13801,6 +13937,11 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
13801
13937
  registry: deps.registry,
13802
13938
  recordReceived: (record) => recordReceived(historyStore, record),
13803
13939
  emit: (event, transfer) => deps.emit(event, transfer),
13940
+ // The mirror is the balance the wallet renders; refresh it as the drain runs
13941
+ // so a multi-token receive lands progressively instead of all at the end.
13942
+ refreshView: () => {
13943
+ void view.delta().catch(() => void 0);
13944
+ },
13804
13945
  attention: (transferId, code, detail) => {
13805
13946
  deps.emit(
13806
13947
  "transfer:attention",
@@ -13881,6 +14022,7 @@ var PaymentsFacade = class {
13881
14022
  now: () => this.nowMs()
13882
14023
  });
13883
14024
  }
14025
+ prewarmed = new PrewarmCache();
13884
14026
  view;
13885
14027
  ledger;
13886
14028
  queue;
@@ -13974,6 +14116,15 @@ var PaymentsFacade = class {
13974
14116
  return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
13975
14117
  }
13976
14118
  // ── money movement ─────────────────────────────────────────────────────────
14119
+ async prewarmSend(request) {
14120
+ await warmSendSources(this.warmDeps(), request);
14121
+ }
14122
+ discardPrewarm() {
14123
+ this.prewarmed.clear();
14124
+ }
14125
+ warmDeps() {
14126
+ return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
14127
+ }
13977
14128
  send(request) {
13978
14129
  return this.track(this.sendOutcome(request));
13979
14130
  }
@@ -14183,7 +14334,7 @@ var PaymentsFacade = class {
14183
14334
  }
14184
14335
  async materialize(transferId, recipientPubkey, request, spend, sourceIds) {
14185
14336
  const engine = this.engine();
14186
- const blobs = await this.deps.storagePort.getBlobs([...sourceIds]);
14337
+ const blobs = await takeSourceBlobs(this.warmDeps(), sourceIds);
14187
14338
  const sources = /* @__PURE__ */ new Map();
14188
14339
  const sourceTokens = [];
14189
14340
  const spentStates = {};
@@ -14196,7 +14347,7 @@ var PaymentsFacade = class {
14196
14347
  const keys = await engine.deliveryKeys(bytes);
14197
14348
  spentStates[tokenId] = { local: keys.stateHash, protocol: keys.stateHash };
14198
14349
  sources.set(tokenId, token);
14199
- sourceTokens.push(this.toUiToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId)));
14350
+ sourceTokens.push(transferringToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId), this.deps.registry, this.nowMs()));
14200
14351
  }
14201
14352
  const payload = buildPayload(recipientPubkey, request, spend, spentStates);
14202
14353
  const plan = {
@@ -14340,7 +14491,7 @@ var PaymentsFacade = class {
14340
14491
  await this.machineStores.mintJournal.upsert(entry);
14341
14492
  let token;
14342
14493
  try {
14343
- token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
14494
+ token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
14344
14495
  } catch (err) {
14345
14496
  this.heartbeat.arm();
14346
14497
  return { success: false, error: messageOf(err) };
@@ -14399,7 +14550,7 @@ var PaymentsFacade = class {
14399
14550
  });
14400
14551
  return false;
14401
14552
  }
14402
- const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
14553
+ const token = await engine.mint(mintParams(this.ownPubkeyBytes, entry.coinId, BigInt(entry.amount)), {
14403
14554
  transferId: entry.mintId
14404
14555
  });
14405
14556
  if (token.blob.tokenId !== entry.tokenId) {
@@ -14408,12 +14559,6 @@ var PaymentsFacade = class {
14408
14559
  await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
14409
14560
  return true;
14410
14561
  }
14411
- mintParams(coinId, amount) {
14412
- return {
14413
- recipientPubkey: this.ownPubkeyBytes,
14414
- value: { assets: [{ coinId, amount }] }
14415
- };
14416
- }
14417
14562
  async tokenInServerInventory(tokenId) {
14418
14563
  let page = await this.deps.storagePort.listInventory();
14419
14564
  for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
@@ -14442,12 +14587,6 @@ var PaymentsFacade = class {
14442
14587
  page = await this.deps.storagePort.listInventory(page.cursor);
14443
14588
  }
14444
14589
  }
14445
- toUiToken(tokenId, coinId, amount) {
14446
- const now = this.nowMs();
14447
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
14448
- const flags = { transferring: true, suspectedSpent: false };
14449
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, this.deps.registry, flags);
14450
- }
14451
14590
  track(op) {
14452
14591
  const settled = op.then(() => void 0, () => void 0);
14453
14592
  this.pendingOps.add(settled);