@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.
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) {
@@ -13532,6 +13632,31 @@ var SpendQueue = class {
13532
13632
  }
13533
13633
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
13534
13634
  }
13635
+ /**
13636
+ * The sources `plan()` WOULD pick right now, reserving nothing and queueing
13637
+ * nothing — a read-only probe used to warm blobs while the user is still
13638
+ * looking at the confirm screen.
13639
+ *
13640
+ * Deliberately side-effect free: reserving here would pin the user's balance
13641
+ * across think-time and strand it if they never press send. The consequence is
13642
+ * that the real `plan()` may pick differently (inventory moves, another send
13643
+ * reserves first); callers must treat the result as a HINT and tolerate a miss,
13644
+ * never as a commitment that these sources will be spent.
13645
+ */
13646
+ previewSelection(request) {
13647
+ if (this.destroyed) return [];
13648
+ let amount;
13649
+ try {
13650
+ amount = parseAmount(request.amount);
13651
+ } catch {
13652
+ return [];
13653
+ }
13654
+ const { freeView, freeTotal } = this.freeView(request.coinId);
13655
+ if (freeTotal < amount) return [];
13656
+ const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
13657
+ if (plan === null) return [];
13658
+ return [...plan.direct, ...plan.split !== void 0 ? [plan.split.tokenId] : []];
13659
+ }
13535
13660
  // Coverage math: free + Σ declared expected change of in-flight sends —
13536
13661
  // never the full amount of in-flight sources (§5.3).
13537
13662
  declareExpectedChange(transferId, coinId, amount) {
@@ -13881,6 +14006,7 @@ var PaymentsFacade = class {
13881
14006
  now: () => this.nowMs()
13882
14007
  });
13883
14008
  }
14009
+ prewarmed = new PrewarmCache();
13884
14010
  view;
13885
14011
  ledger;
13886
14012
  queue;
@@ -13974,6 +14100,15 @@ var PaymentsFacade = class {
13974
14100
  return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
13975
14101
  }
13976
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
+ }
13977
14112
  send(request) {
13978
14113
  return this.track(this.sendOutcome(request));
13979
14114
  }
@@ -14183,7 +14318,7 @@ var PaymentsFacade = class {
14183
14318
  }
14184
14319
  async materialize(transferId, recipientPubkey, request, spend, sourceIds) {
14185
14320
  const engine = this.engine();
14186
- const blobs = await this.deps.storagePort.getBlobs([...sourceIds]);
14321
+ const blobs = await takeSourceBlobs(this.warmDeps(), sourceIds);
14187
14322
  const sources = /* @__PURE__ */ new Map();
14188
14323
  const sourceTokens = [];
14189
14324
  const spentStates = {};
@@ -14196,7 +14331,7 @@ var PaymentsFacade = class {
14196
14331
  const keys = await engine.deliveryKeys(bytes);
14197
14332
  spentStates[tokenId] = { local: keys.stateHash, protocol: keys.stateHash };
14198
14333
  sources.set(tokenId, token);
14199
- 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()));
14200
14335
  }
14201
14336
  const payload = buildPayload(recipientPubkey, request, spend, spentStates);
14202
14337
  const plan = {
@@ -14340,7 +14475,7 @@ var PaymentsFacade = class {
14340
14475
  await this.machineStores.mintJournal.upsert(entry);
14341
14476
  let token;
14342
14477
  try {
14343
- token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
14478
+ token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
14344
14479
  } catch (err) {
14345
14480
  this.heartbeat.arm();
14346
14481
  return { success: false, error: messageOf(err) };
@@ -14399,7 +14534,7 @@ var PaymentsFacade = class {
14399
14534
  });
14400
14535
  return false;
14401
14536
  }
14402
- 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)), {
14403
14538
  transferId: entry.mintId
14404
14539
  });
14405
14540
  if (token.blob.tokenId !== entry.tokenId) {
@@ -14408,12 +14543,6 @@ var PaymentsFacade = class {
14408
14543
  await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
14409
14544
  return true;
14410
14545
  }
14411
- mintParams(coinId, amount) {
14412
- return {
14413
- recipientPubkey: this.ownPubkeyBytes,
14414
- value: { assets: [{ coinId, amount }] }
14415
- };
14416
- }
14417
14546
  async tokenInServerInventory(tokenId) {
14418
14547
  let page = await this.deps.storagePort.listInventory();
14419
14548
  for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
@@ -14442,12 +14571,6 @@ var PaymentsFacade = class {
14442
14571
  page = await this.deps.storagePort.listInventory(page.cursor);
14443
14572
  }
14444
14573
  }
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
14574
  track(op) {
14452
14575
  const settled = op.then(() => void 0, () => void 0);
14453
14576
  this.pendingOps.add(settled);