@unicitylabs/sphere-sdk 0.16.0 → 0.17.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.
Files changed (53) hide show
  1. package/dist/connect/index.cjs +11 -5
  2. package/dist/connect/index.cjs.map +1 -1
  3. package/dist/connect/index.d.cts +4 -1
  4. package/dist/connect/index.d.ts +4 -1
  5. package/dist/connect/index.js +11 -5
  6. package/dist/connect/index.js.map +1 -1
  7. package/dist/core/index.cjs +692 -268
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +69 -0
  10. package/dist/core/index.d.ts +69 -0
  11. package/dist/core/index.js +692 -268
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/impl/browser/connect/index.cjs +11 -5
  14. package/dist/impl/browser/connect/index.cjs.map +1 -1
  15. package/dist/impl/browser/connect/index.d.cts +3 -1
  16. package/dist/impl/browser/connect/index.d.ts +3 -1
  17. package/dist/impl/browser/connect/index.js +11 -5
  18. package/dist/impl/browser/connect/index.js.map +1 -1
  19. package/dist/impl/nodejs/connect/index.cjs +10 -4
  20. package/dist/impl/nodejs/connect/index.cjs.map +1 -1
  21. package/dist/impl/nodejs/connect/index.d.cts +1 -1
  22. package/dist/impl/nodejs/connect/index.d.ts +1 -1
  23. package/dist/impl/nodejs/connect/index.js +10 -4
  24. package/dist/impl/nodejs/connect/index.js.map +1 -1
  25. package/dist/impl/nodejs/index.d.cts +11 -0
  26. package/dist/impl/nodejs/index.d.ts +11 -0
  27. package/dist/impl/shared/wallet-api/index.d.cts +11 -0
  28. package/dist/impl/shared/wallet-api/index.d.ts +11 -0
  29. package/dist/impl/wallet-api-v2/index.cjs +2 -0
  30. package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
  31. package/dist/impl/wallet-api-v2/index.d.cts +13 -0
  32. package/dist/impl/wallet-api-v2/index.d.ts +13 -0
  33. package/dist/impl/wallet-api-v2/index.js +2 -0
  34. package/dist/impl/wallet-api-v2/index.js.map +1 -1
  35. package/dist/index.cjs +692 -268
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +63 -1
  38. package/dist/index.d.ts +63 -1
  39. package/dist/index.js +692 -268
  40. package/dist/index.js.map +1 -1
  41. package/dist/modules/payments-v2/index.cjs +767 -398
  42. package/dist/modules/payments-v2/index.cjs.map +1 -1
  43. package/dist/modules/payments-v2/index.d.cts +91 -14
  44. package/dist/modules/payments-v2/index.d.ts +91 -14
  45. package/dist/modules/payments-v2/index.js +767 -398
  46. package/dist/modules/payments-v2/index.js.map +1 -1
  47. package/dist/token-engine/index.cjs +132 -34
  48. package/dist/token-engine/index.cjs.map +1 -1
  49. package/dist/token-engine/index.d.cts +19 -0
  50. package/dist/token-engine/index.d.ts +19 -0
  51. package/dist/token-engine/index.js +132 -34
  52. package/dist/token-engine/index.js.map +1 -1
  53. package/package.json +1 -1
@@ -184,6 +184,135 @@ var STORE_KEYS = {
184
184
  knownSpends: "known-spends"
185
185
  };
186
186
 
187
+ // core/errors.ts
188
+ var SphereError = class extends Error {
189
+ code;
190
+ cause;
191
+ /**
192
+ * #441 deferred-paid linkage: the transferId of the possibly-committed send,
193
+ * stamped by `sendOnce` before the error leaves so a payment-request consumer
194
+ * can durably journal request→transfer and resolve 'paid' only when that
195
+ * transfer actually completes (never optimistically). Present ONLY on
196
+ * possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
197
+ */
198
+ transferId;
199
+ constructor(message, code, cause) {
200
+ super(message);
201
+ this.name = "SphereError";
202
+ this.code = code;
203
+ this.cause = cause;
204
+ }
205
+ };
206
+ var PartialSendConflictError = class extends SphereError {
207
+ /**
208
+ * The FIRST partial attempt's transferId — its intent stays open for resume,
209
+ * and it is the §6 handoff anchor. NOTE: `send()` may re-plan the remainder across
210
+ * SEVERAL internal attempts before giving up, so {@link committedTokenIds} can
211
+ * span MULTIPLE transferIds — they do NOT all map to this single id.
212
+ */
213
+ transferId;
214
+ /**
215
+ * Source token ids whose spend already certified on-chain (and whose finished
216
+ * output blob is journaled for delivery). Their value has already been
217
+ * delivered; NEVER re-send these. May accumulate across several internal
218
+ * remainder-re-plan attempts (so not all are journaled under {@link transferId}).
219
+ */
220
+ committedTokenIds;
221
+ /**
222
+ * The still-undelivered portion (base units, decimal string) — `send()` could
223
+ * not COMPLETE it from the remaining live sources (insufficient funds or a
224
+ * transient error; see `cause`). Re-plan ONLY this amount, under a new
225
+ * transferId; the delivered legs converge via the recipient's §6 claim.
226
+ */
227
+ remainingAmount;
228
+ constructor(message, transferId, committedTokenIds, remainingAmount, cause) {
229
+ super(message, "SEND_PARTIALLY_COMPLETED", cause);
230
+ this.name = "PartialSendConflictError";
231
+ this.transferId = transferId;
232
+ this.committedTokenIds = [...committedTokenIds];
233
+ this.remainingAmount = remainingAmount;
234
+ }
235
+ };
236
+ var POSSIBLY_COMMITTED_SEND_CODES = /* @__PURE__ */ new Set([
237
+ "SEND_SYNC_PENDING",
238
+ "CERTIFICATION_UNCONFIRMED",
239
+ "CHECKPOINT_PERSIST_FAILED",
240
+ "SPLIT_CHECKPOINT_LOST",
241
+ "CHECKPOINT_TRUSTBASE_MISMATCH",
242
+ "SEND_PARTIALLY_COMPLETED"
243
+ ]);
244
+ function isPossiblyCommittedSendOutcome(err) {
245
+ return isSphereError(err) && POSSIBLY_COMMITTED_SEND_CODES.has(err.code);
246
+ }
247
+ function isSphereError(err) {
248
+ return err instanceof SphereError;
249
+ }
250
+
251
+ // core/uuid.ts
252
+ function randomUUID() {
253
+ const c = globalThis.crypto;
254
+ if (typeof c !== "undefined" && typeof c.randomUUID === "function") {
255
+ return c.randomUUID();
256
+ }
257
+ const b = c.getRandomValues(new Uint8Array(16));
258
+ b[6] = b[6] & 15 | 64;
259
+ b[8] = b[8] & 63 | 128;
260
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, "0"));
261
+ return `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`;
262
+ }
263
+
264
+ // modules/payments-v2/async.ts
265
+ var SingleFlight = class {
266
+ flight = null;
267
+ run(fn) {
268
+ if (this.flight !== null) return this.flight;
269
+ const current = fn().finally(() => {
270
+ this.flight = null;
271
+ });
272
+ this.flight = current;
273
+ return current;
274
+ }
275
+ };
276
+ var SerialChain = class {
277
+ tail = Promise.resolve();
278
+ /** FIFO; starts after every prior fn settled; returns this fn's own outcome. */
279
+ enqueue(fn) {
280
+ const run = this.tail.then(fn, fn);
281
+ this.tail = run.then(
282
+ () => void 0,
283
+ () => void 0
284
+ );
285
+ return run;
286
+ }
287
+ };
288
+ function coalesced(run, observe) {
289
+ let inFlight = false;
290
+ let queued = false;
291
+ const start = () => {
292
+ inFlight = true;
293
+ let running;
294
+ try {
295
+ running = run();
296
+ } catch {
297
+ running = Promise.resolve();
298
+ }
299
+ observe?.(running);
300
+ void running.catch(() => void 0).finally(() => {
301
+ inFlight = false;
302
+ if (!queued) return;
303
+ queued = false;
304
+ start();
305
+ });
306
+ };
307
+ return () => {
308
+ if (inFlight) {
309
+ queued = true;
310
+ return;
311
+ }
312
+ start();
313
+ };
314
+ }
315
+
187
316
  // node_modules/@noble/hashes/utils.js
188
317
  function isBytes(a) {
189
318
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
@@ -511,72 +640,6 @@ var sha256 = /* @__PURE__ */ createHasher(
511
640
  // core/crypto.ts
512
641
  var bip39 = __toESM(require("bip39"), 1);
513
642
  var import_crypto_js = __toESM(require("crypto-js"), 1);
514
-
515
- // core/errors.ts
516
- var SphereError = class extends Error {
517
- code;
518
- cause;
519
- /**
520
- * #441 deferred-paid linkage: the transferId of the possibly-committed send,
521
- * stamped by `sendOnce` before the error leaves so a payment-request consumer
522
- * can durably journal request→transfer and resolve 'paid' only when that
523
- * transfer actually completes (never optimistically). Present ONLY on
524
- * possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
525
- */
526
- transferId;
527
- constructor(message, code, cause) {
528
- super(message);
529
- this.name = "SphereError";
530
- this.code = code;
531
- this.cause = cause;
532
- }
533
- };
534
- var PartialSendConflictError = class extends SphereError {
535
- /**
536
- * The FIRST partial attempt's transferId — its intent stays open for resume,
537
- * and it is the §6 handoff anchor. NOTE: `send()` may re-plan the remainder across
538
- * SEVERAL internal attempts before giving up, so {@link committedTokenIds} can
539
- * span MULTIPLE transferIds — they do NOT all map to this single id.
540
- */
541
- transferId;
542
- /**
543
- * Source token ids whose spend already certified on-chain (and whose finished
544
- * output blob is journaled for delivery). Their value has already been
545
- * delivered; NEVER re-send these. May accumulate across several internal
546
- * remainder-re-plan attempts (so not all are journaled under {@link transferId}).
547
- */
548
- committedTokenIds;
549
- /**
550
- * The still-undelivered portion (base units, decimal string) — `send()` could
551
- * not COMPLETE it from the remaining live sources (insufficient funds or a
552
- * transient error; see `cause`). Re-plan ONLY this amount, under a new
553
- * transferId; the delivered legs converge via the recipient's §6 claim.
554
- */
555
- remainingAmount;
556
- constructor(message, transferId, committedTokenIds, remainingAmount, cause) {
557
- super(message, "SEND_PARTIALLY_COMPLETED", cause);
558
- this.name = "PartialSendConflictError";
559
- this.transferId = transferId;
560
- this.committedTokenIds = [...committedTokenIds];
561
- this.remainingAmount = remainingAmount;
562
- }
563
- };
564
- var POSSIBLY_COMMITTED_SEND_CODES = /* @__PURE__ */ new Set([
565
- "SEND_SYNC_PENDING",
566
- "CERTIFICATION_UNCONFIRMED",
567
- "CHECKPOINT_PERSIST_FAILED",
568
- "SPLIT_CHECKPOINT_LOST",
569
- "CHECKPOINT_TRUSTBASE_MISMATCH",
570
- "SEND_PARTIALLY_COMPLETED"
571
- ]);
572
- function isPossiblyCommittedSendOutcome(err) {
573
- return isSphereError(err) && POSSIBLY_COMMITTED_SEND_CODES.has(err.code);
574
- }
575
- function isSphereError(err) {
576
- return err instanceof SphereError;
577
- }
578
-
579
- // core/crypto.ts
580
643
  var CURVE_ORDER = BigInt(
581
644
  "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"
582
645
  );
@@ -591,71 +654,6 @@ function bytesToHex(bytes) {
591
654
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
592
655
  }
593
656
 
594
- // core/uuid.ts
595
- function randomUUID() {
596
- const c = globalThis.crypto;
597
- if (typeof c !== "undefined" && typeof c.randomUUID === "function") {
598
- return c.randomUUID();
599
- }
600
- const b = c.getRandomValues(new Uint8Array(16));
601
- b[6] = b[6] & 15 | 64;
602
- b[8] = b[8] & 63 | 128;
603
- const h = Array.from(b, (x) => x.toString(16).padStart(2, "0"));
604
- return `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`;
605
- }
606
-
607
- // modules/payments-v2/async.ts
608
- var SingleFlight = class {
609
- flight = null;
610
- run(fn) {
611
- if (this.flight !== null) return this.flight;
612
- const current = fn().finally(() => {
613
- this.flight = null;
614
- });
615
- this.flight = current;
616
- return current;
617
- }
618
- };
619
- var SerialChain = class {
620
- tail = Promise.resolve();
621
- /** FIFO; starts after every prior fn settled; returns this fn's own outcome. */
622
- enqueue(fn) {
623
- const run = this.tail.then(fn, fn);
624
- this.tail = run.then(
625
- () => void 0,
626
- () => void 0
627
- );
628
- return run;
629
- }
630
- };
631
- function coalesced(run, observe) {
632
- let inFlight = false;
633
- let queued = false;
634
- const start = () => {
635
- inFlight = true;
636
- let running;
637
- try {
638
- running = run();
639
- } catch {
640
- running = Promise.resolve();
641
- }
642
- observe?.(running);
643
- void running.catch(() => void 0).finally(() => {
644
- inFlight = false;
645
- if (!queued) return;
646
- queued = false;
647
- start();
648
- });
649
- };
650
- return () => {
651
- if (inFlight) {
652
- queued = true;
653
- return;
654
- }
655
- start();
656
- };
657
- }
658
-
659
657
  // modules/payments-v2/machine/journal.ts
660
658
  var MAX_DELIVERY_ATTEMPTS = 6;
661
659
  var RATE_LIMIT_DEFER_MS = 3e4;
@@ -834,6 +832,17 @@ function createMachineStores(kv) {
834
832
  };
835
833
  }
836
834
 
835
+ // modules/payments-v2/machine/payload-view.ts
836
+ function isWholeIntent(p) {
837
+ return p.kind === "whole" || p.kind === "coinless";
838
+ }
839
+ function splitOf(p) {
840
+ return p.kind === "coin" ? p.split : void 0;
841
+ }
842
+ function coinIdOf(p) {
843
+ return p.kind === "coin" ? p.coinId : void 0;
844
+ }
845
+
837
846
  // token-engine/errors.ts
838
847
  var TransferConflictError = class extends SphereError {
839
848
  /**
@@ -1075,13 +1084,14 @@ var TransferMachine = class {
1075
1084
  const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
1076
1085
  return { recipientBlob: finished.blob.token };
1077
1086
  }
1078
- const split = ctx.payload.split;
1087
+ const split = splitOf(ctx.payload);
1088
+ const splitCoinId = coinIdOf(ctx.payload);
1079
1089
  const { outputs } = await engine.split(
1080
1090
  {
1081
1091
  token: source,
1082
1092
  outputs: [
1083
- { recipientPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.splitAmount) },
1084
- { recipientPubkey: this.deps.ownPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.remainderAmount) }
1093
+ { recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
1094
+ { recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
1085
1095
  ]
1086
1096
  },
1087
1097
  opts
@@ -1203,7 +1213,9 @@ var TransferMachine = class {
1203
1213
  const entry = {
1204
1214
  transferId: r.transferId,
1205
1215
  remainingAmount: undelivered.toString(),
1206
- coinId: r.payload.coinId,
1216
+ // '' only on a token-addressed intent, which is one leg: it either lands or
1217
+ // conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
1218
+ coinId: coinIdOf(r.payload) ?? "",
1207
1219
  recipient: r.payload.recipient,
1208
1220
  committedTokenIds: committed.map((o) => o.op.sourceTokenId),
1209
1221
  createdAt: this.deps.now()
@@ -1229,8 +1241,16 @@ var TransferMachine = class {
1229
1241
  }
1230
1242
  async finishBookkeeping(engine, transferId, payload, phase, committed) {
1231
1243
  try {
1232
- const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
1233
- await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
1244
+ const coinId = coinIdOf(payload);
1245
+ const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
1246
+ const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
1247
+ await this.deps.recordHistory?.({
1248
+ transferId,
1249
+ payload,
1250
+ phase,
1251
+ committedAmount,
1252
+ ...committedAssets !== void 0 ? { committedAssets } : {}
1253
+ });
1234
1254
  } catch {
1235
1255
  }
1236
1256
  try {
@@ -1244,6 +1264,18 @@ var TransferMachine = class {
1244
1264
  * a journal-rehydrated resume leg has no source token, but every committed
1245
1265
  * leg carries its recipient blob, so the outcome-derived rule is uniform).
1246
1266
  */
1267
+ /** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
1268
+ async settledAssets(engine, committed) {
1269
+ const total = /* @__PURE__ */ new Map();
1270
+ for (const outcome of committed) {
1271
+ if (outcome.recipientBlob === void 0) continue;
1272
+ const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
1273
+ for (const asset of engine.readValue(token)?.assets ?? []) {
1274
+ total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
1275
+ }
1276
+ }
1277
+ return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
1278
+ }
1247
1279
  async settledAmount(engine, coinId, committed) {
1248
1280
  let total = 0n;
1249
1281
  for (const outcome of committed) {
@@ -1272,8 +1304,11 @@ var TransferMachine = class {
1272
1304
  }
1273
1305
  };
1274
1306
  function conflictAmount(engine, payload, op, source) {
1275
- if (op.kind === "split") return BigInt(payload.split.splitAmount);
1276
- return source !== void 0 ? engine.balanceOf(source, payload.coinId) : 0n;
1307
+ const split = splitOf(payload);
1308
+ if (op.kind === "split") return BigInt(split.splitAmount);
1309
+ const coinId = coinIdOf(payload);
1310
+ if (coinId === void 0) return 0n;
1311
+ return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
1277
1312
  }
1278
1313
 
1279
1314
  // modules/payments-v2/machine/resume.ts
@@ -1380,6 +1415,14 @@ async function runOne(ctx, job, report) {
1380
1415
  report.failed.push(job.transferId);
1381
1416
  return;
1382
1417
  }
1418
+ if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
1419
+ logger.warn(
1420
+ "PaymentsV2",
1421
+ `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
1422
+ );
1423
+ report.failed.push(job.transferId);
1424
+ return;
1425
+ }
1383
1426
  mine.set(op.sourceTokenId, token);
1384
1427
  }
1385
1428
  try {
@@ -1410,6 +1453,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
1410
1453
  }
1411
1454
  report.failed.push(transferId);
1412
1455
  }
1456
+ function validateCoinPayload(c) {
1457
+ if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
1458
+ throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
1459
+ }
1460
+ const s = c.split;
1461
+ if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
1462
+ throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
1463
+ }
1464
+ return { ...c, kind: "coin" };
1465
+ }
1466
+ function validateWholePayload(p) {
1467
+ if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
1468
+ throw new SphereError(
1469
+ "token intent payload must name exactly one source token",
1470
+ "VALIDATION_ERROR"
1471
+ );
1472
+ }
1473
+ if (p.split !== void 0) {
1474
+ throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
1475
+ }
1476
+ return { ...p, kind: "whole", direct: [p.direct[0]] };
1477
+ }
1413
1478
  function validatePayload(raw) {
1414
1479
  const p = raw !== null && typeof raw === "object" ? raw : null;
1415
1480
  if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
@@ -1418,14 +1483,17 @@ function validatePayload(raw) {
1418
1483
  "VALIDATION_ERROR"
1419
1484
  );
1420
1485
  }
1421
- if (typeof p.recipient !== "string" || typeof p.coinId !== "string" || typeof p.amount !== "string") {
1422
- throw new SphereError("intent payload is missing recipient/coinId/amount", "VALIDATION_ERROR");
1486
+ if (typeof p.recipient !== "string") {
1487
+ throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
1423
1488
  }
1424
- const s = p.split;
1425
- if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
1426
- throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
1489
+ const kind = p.kind ?? "coin";
1490
+ if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
1491
+ throw new SphereError(
1492
+ `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
1493
+ "VALIDATION_ERROR"
1494
+ );
1427
1495
  }
1428
- return p;
1496
+ return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
1429
1497
  }
1430
1498
 
1431
1499
  // modules/payments-v2/convergence.ts
@@ -1625,46 +1693,314 @@ async function openRow(entry, journal, decryptPayload) {
1625
1693
  } catch {
1626
1694
  payload = null;
1627
1695
  }
1628
- const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
1629
- const certified = distinctOpCount(journal);
1630
- return {
1631
- transferId: entry.transferId,
1632
- kind: "open",
1633
- recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
1634
- coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
1635
- amount: typeof payload?.amount === "string" ? payload.amount : "",
1636
- legs: { certified, total: Math.max(total, certified) },
1637
- deliveryPending: journal.length > 0,
1638
- createdAt: entry.createdAt
1639
- };
1696
+ const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
1697
+ const certified = distinctOpCount(journal);
1698
+ return {
1699
+ transferId: entry.transferId,
1700
+ kind: "open",
1701
+ recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
1702
+ ...subject(payload),
1703
+ legs: { certified, total: Math.max(total, certified) },
1704
+ deliveryPending: journal.length > 0,
1705
+ createdAt: entry.createdAt
1706
+ };
1707
+ }
1708
+ function subject(payload) {
1709
+ const rawKind = payload?.kind;
1710
+ if (rawKind === "whole" || rawKind === "coinless") {
1711
+ const tokenId = payload?.direct?.[0];
1712
+ return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
1713
+ }
1714
+ const coin = payload;
1715
+ return {
1716
+ coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
1717
+ amount: typeof coin?.amount === "string" ? coin.amount : ""
1718
+ };
1719
+ }
1720
+ function journalOnlyRow(transferId, entries) {
1721
+ const certified = distinctOpCount(entries);
1722
+ return {
1723
+ transferId,
1724
+ kind: "open",
1725
+ recipient: entries[0].recipientPubkey,
1726
+ coinId: "",
1727
+ amount: "",
1728
+ legs: { certified, total: certified },
1729
+ deliveryPending: true,
1730
+ createdAt: 0
1731
+ };
1732
+ }
1733
+ function shortfallRow(entry) {
1734
+ return {
1735
+ transferId: entry.transferId,
1736
+ kind: "shortfall",
1737
+ recipient: entry.recipient,
1738
+ coinId: entry.coinId,
1739
+ amount: entry.remainingAmount,
1740
+ legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
1741
+ deliveryPending: false,
1742
+ createdAt: entry.createdAt
1743
+ };
1744
+ }
1745
+ function distinctOpCount(entries) {
1746
+ return new Set(entries.map((entry) => entry.opIndex)).size;
1747
+ }
1748
+
1749
+ // modules/payments-v2/inventory/token-data.ts
1750
+ async function readTokenData(deps, tokenId) {
1751
+ if (deps.view.stateHashOf(tokenId) === void 0) {
1752
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
1753
+ }
1754
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
1755
+ if (bytes === void 0) {
1756
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
1757
+ }
1758
+ return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
1759
+ }
1760
+
1761
+ // modules/payments-v2/machine/payload.ts
1762
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
1763
+ return {
1764
+ v: 2,
1765
+ kind: "coin",
1766
+ recipient: recipientPubkey,
1767
+ coinId: request.coinId,
1768
+ amount: request.amount,
1769
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
1770
+ direct: [...spend.plan.direct],
1771
+ ...spend.plan.split !== void 0 ? {
1772
+ split: {
1773
+ tokenId: spend.plan.split.tokenId,
1774
+ splitAmount: spend.plan.split.splitAmount.toString(),
1775
+ remainderAmount: spend.plan.split.remainderAmount.toString()
1776
+ }
1777
+ } : {},
1778
+ spentStates
1779
+ };
1780
+ }
1781
+ function messageOf(err) {
1782
+ return err instanceof Error ? err.message : String(err);
1783
+ }
1784
+ function buildWholePayload(recipientPubkey, request, spentStates) {
1785
+ return {
1786
+ v: 2,
1787
+ kind: "whole",
1788
+ recipient: recipientPubkey,
1789
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
1790
+ direct: [request.tokenId],
1791
+ spentStates
1792
+ };
1793
+ }
1794
+
1795
+ // modules/payments-v2/mint-params.ts
1796
+ function mintParams(recipientPubkey, coinId, amount) {
1797
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1798
+ }
1799
+
1800
+ // modules/payments-v2/mint.ts
1801
+ function runMintUnderJournal(deps, input) {
1802
+ return mintUnderJournal(deps, input);
1803
+ }
1804
+ async function mintUnderJournal(deps, input) {
1805
+ const { mintId, coinId, amount } = input;
1806
+ const engine = deps.engine;
1807
+ const entry = {
1808
+ mintId,
1809
+ coinId,
1810
+ amount: amount.toString(),
1811
+ tokenId: "",
1812
+ createdAt: deps.now()
1813
+ };
1814
+ await deps.mintJournal.upsert(entry);
1815
+ let token;
1816
+ try {
1817
+ token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
1818
+ } catch (err) {
1819
+ deps.armHeartbeat();
1820
+ return { success: false, error: messageOf(err) };
1821
+ }
1822
+ if (token.blob.tokenId !== entry.tokenId) {
1823
+ await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
1824
+ }
1825
+ try {
1826
+ await finalizeMint(deps, mintId, token, coinId, amount.toString());
1827
+ } catch (err) {
1828
+ deps.armHeartbeat();
1829
+ return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
1830
+ }
1831
+ return { success: true, tokenId: token.blob.tokenId };
1832
+ }
1833
+ async function finalizeMint(deps, mintId, token, coinId, amount) {
1834
+ const engine = deps.engine;
1835
+ const bytes = token.blob.token;
1836
+ const digest = bytesToHex(sha256(bytes));
1837
+ const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
1838
+ const key = keys.get(digest);
1839
+ if (key === void 0) {
1840
+ throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
1841
+ }
1842
+ await deps.storagePort.applyDelta({
1843
+ transferId: mintId,
1844
+ spent: [],
1845
+ added: [{ tokenId: token.blob.tokenId, key }]
1846
+ });
1847
+ await deps.recordMint({
1848
+ tokenId: token.blob.tokenId,
1849
+ assets: [{ coinId, amount }]
1850
+ });
1851
+ await deps.mintJournal.removeByKey(mintId);
1852
+ deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
1853
+ deps.refreshView();
1854
+ }
1855
+
1856
+ // modules/payments-v2/inventory/presentation.ts
1857
+ function toToken(tokenId, snapshot, asset, registry, flags) {
1858
+ const iconUrl = registry.getIconUrl(asset.coinId);
1859
+ return {
1860
+ id: tokenId,
1861
+ coinId: asset.coinId,
1862
+ symbol: registry.getSymbol(asset.coinId),
1863
+ name: registry.getName(asset.coinId),
1864
+ decimals: registry.getDecimals(asset.coinId),
1865
+ ...iconUrl !== null ? { iconUrl } : {},
1866
+ amount: asset.amount,
1867
+ status: flags.transferring ? "transferring" : "confirmed",
1868
+ createdAt: snapshot.createdAt,
1869
+ updatedAt: snapshot.updatedAt,
1870
+ lazy: true,
1871
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {}
1872
+ };
1873
+ }
1874
+ function aggregateAssets(entries, isTransferring, registry) {
1875
+ const groups = /* @__PURE__ */ new Map();
1876
+ for (const [tokenId, entry] of entries) {
1877
+ const moving = isTransferring(tokenId);
1878
+ for (const asset of entry.assets) {
1879
+ let group = groups.get(asset.coinId);
1880
+ if (!group) {
1881
+ group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
1882
+ groups.set(asset.coinId, group);
1883
+ }
1884
+ if (moving) {
1885
+ group.transferring += BigInt(asset.amount);
1886
+ group.transferringCount += 1;
1887
+ } else {
1888
+ group.total += BigInt(asset.amount);
1889
+ group.count += 1;
1890
+ }
1891
+ }
1892
+ }
1893
+ return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
1894
+ }
1895
+ function toAsset(coinId, totals, registry) {
1896
+ const iconUrl = registry.getIconUrl(coinId);
1897
+ return {
1898
+ coinId,
1899
+ symbol: registry.getSymbol(coinId),
1900
+ name: registry.getName(coinId),
1901
+ decimals: registry.getDecimals(coinId),
1902
+ ...iconUrl !== null ? { iconUrl } : {},
1903
+ totalAmount: totals.total.toString(),
1904
+ tokenCount: totals.count,
1905
+ confirmedAmount: totals.total.toString(),
1906
+ unconfirmedAmount: "0",
1907
+ confirmedTokenCount: totals.count,
1908
+ unconfirmedTokenCount: 0,
1909
+ transferringTokenCount: totals.transferringCount,
1910
+ transferringAmount: totals.transferring.toString(),
1911
+ priceUsd: null,
1912
+ priceEur: null,
1913
+ change24h: null,
1914
+ fiatValueUsd: null,
1915
+ fiatValueEur: null
1916
+ };
1917
+ }
1918
+ function priceIdOf(registry, coinId) {
1919
+ return registry.getName(coinId).toLowerCase();
1920
+ }
1921
+ async function withPrices(raw, registry, price) {
1922
+ try {
1923
+ const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
1924
+ const quotes = await price.getPrices(ids);
1925
+ return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
1926
+ } catch {
1927
+ return raw;
1928
+ }
1640
1929
  }
1641
- function journalOnlyRow(transferId, entries) {
1642
- const certified = distinctOpCount(entries);
1930
+ function priceOne(asset, quote) {
1931
+ if (!quote) return asset;
1932
+ const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
1643
1933
  return {
1644
- transferId,
1645
- kind: "open",
1646
- recipient: entries[0].recipientPubkey,
1647
- coinId: "",
1648
- amount: "",
1649
- legs: { certified, total: certified },
1650
- deliveryPending: true,
1651
- createdAt: 0
1934
+ ...asset,
1935
+ priceUsd: quote.priceUsd,
1936
+ priceEur: quote.priceEur ?? null,
1937
+ change24h: quote.change24h ?? null,
1938
+ fiatValueUsd: human * quote.priceUsd,
1939
+ fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
1652
1940
  };
1653
1941
  }
1654
- function shortfallRow(entry) {
1655
- return {
1656
- transferId: entry.transferId,
1657
- kind: "shortfall",
1658
- recipient: entry.recipient,
1659
- coinId: entry.coinId,
1660
- amount: entry.remainingAmount,
1661
- legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
1662
- deliveryPending: false,
1663
- createdAt: entry.createdAt
1942
+ function transferringToken(tokenId, coinId, amount, registry, now) {
1943
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
1944
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
1945
+ transferring: true,
1946
+ suspectedSpent: false
1947
+ });
1948
+ }
1949
+
1950
+ // modules/payments-v2/send-whole.ts
1951
+ async function materializeWholeSpend(deps, input) {
1952
+ const { engine } = deps;
1953
+ const tokenId = input.request.tokenId;
1954
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
1955
+ if (bytes === void 0) {
1956
+ throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
1957
+ }
1958
+ const token = await engine.decodeToken({ tokenId, token: bytes });
1959
+ if (token.valueEnvelope === "bare_collection") {
1960
+ throw new SphereError(
1961
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
1962
+ "VALIDATION_ERROR"
1963
+ );
1964
+ }
1965
+ if (input.requireCoinless && token.value !== null) {
1966
+ throw new SphereError(
1967
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
1968
+ "VALIDATION_ERROR"
1969
+ );
1970
+ }
1971
+ const keys = await engine.deliveryKeys(bytes);
1972
+ const payload = buildWholePayload(input.recipientPubkey, input.request, {
1973
+ [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
1974
+ });
1975
+ const plan = {
1976
+ transferId: input.transferId,
1977
+ recipientPubkey: input.recipientPubkey,
1978
+ payload,
1979
+ ops: buildOps(payload),
1980
+ sources: /* @__PURE__ */ new Map([[tokenId, token]]),
1981
+ ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
1664
1982
  };
1983
+ const sourceTokens = (token.value?.assets ?? []).map(
1984
+ (asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
1985
+ );
1986
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
1665
1987
  }
1666
- function distinctOpCount(entries) {
1667
- return new Set(entries.map((entry) => entry.opIndex)).size;
1988
+
1989
+ // modules/payments-v2/send-errors.ts
1990
+ function partialize(err, run) {
1991
+ if (run.delivered.length === 0) return err;
1992
+ return new PartialSendConflictError(
1993
+ "Part of your payment was sent; the remaining amount could not be completed (see cause). The delivered portion is final \u2014 re-plan only the shortfall, never the full amount.",
1994
+ run.firstPartialId ?? "",
1995
+ [...run.delivered],
1996
+ run.amount,
1997
+ err
1998
+ );
1999
+ }
2000
+ function stampTransferId(err, transferId) {
2001
+ if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
2002
+ err.transferId = transferId;
2003
+ }
1668
2004
  }
1669
2005
 
1670
2006
  // modules/payments-v2/recipient.ts
@@ -1715,11 +2051,6 @@ async function splitOpIndex(deps, envelope) {
1715
2051
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
1716
2052
  }
1717
2053
 
1718
- // modules/payments-v2/mint-params.ts
1719
- function mintParams(recipientPubkey, coinId, amount) {
1720
- return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1721
- }
1722
-
1723
2054
  // modules/payments-v2/prewarm-cache.ts
1724
2055
  var PrewarmCache = class _PrewarmCache {
1725
2056
  entries = /* @__PURE__ */ new Map();
@@ -1799,123 +2130,6 @@ async function takeSourceBlobs(deps, sourceIds) {
1799
2130
  return blobs;
1800
2131
  }
1801
2132
 
1802
- // modules/payments-v2/inventory/presentation.ts
1803
- function toToken(tokenId, snapshot, asset, registry, flags) {
1804
- const iconUrl = registry.getIconUrl(asset.coinId);
1805
- return {
1806
- id: tokenId,
1807
- coinId: asset.coinId,
1808
- symbol: registry.getSymbol(asset.coinId),
1809
- name: registry.getName(asset.coinId),
1810
- decimals: registry.getDecimals(asset.coinId),
1811
- ...iconUrl !== null ? { iconUrl } : {},
1812
- amount: asset.amount,
1813
- status: flags.transferring ? "transferring" : "confirmed",
1814
- createdAt: snapshot.createdAt,
1815
- updatedAt: snapshot.updatedAt,
1816
- lazy: true,
1817
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
1818
- };
1819
- }
1820
- function aggregateAssets(entries, isTransferring, registry) {
1821
- const groups = /* @__PURE__ */ new Map();
1822
- for (const [tokenId, entry] of entries) {
1823
- const moving = isTransferring(tokenId);
1824
- for (const asset of entry.assets) {
1825
- let group = groups.get(asset.coinId);
1826
- if (!group) {
1827
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
1828
- groups.set(asset.coinId, group);
1829
- }
1830
- if (moving) {
1831
- group.transferring += BigInt(asset.amount);
1832
- group.transferringCount += 1;
1833
- } else {
1834
- group.total += BigInt(asset.amount);
1835
- group.count += 1;
1836
- }
1837
- }
1838
- }
1839
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
1840
- }
1841
- function toAsset(coinId, totals, registry) {
1842
- const iconUrl = registry.getIconUrl(coinId);
1843
- return {
1844
- coinId,
1845
- symbol: registry.getSymbol(coinId),
1846
- name: registry.getName(coinId),
1847
- decimals: registry.getDecimals(coinId),
1848
- ...iconUrl !== null ? { iconUrl } : {},
1849
- totalAmount: totals.total.toString(),
1850
- tokenCount: totals.count,
1851
- confirmedAmount: totals.total.toString(),
1852
- unconfirmedAmount: "0",
1853
- confirmedTokenCount: totals.count,
1854
- unconfirmedTokenCount: 0,
1855
- transferringTokenCount: totals.transferringCount,
1856
- transferringAmount: totals.transferring.toString(),
1857
- priceUsd: null,
1858
- priceEur: null,
1859
- change24h: null,
1860
- fiatValueUsd: null,
1861
- fiatValueEur: null
1862
- };
1863
- }
1864
- function priceIdOf(registry, coinId) {
1865
- return registry.getName(coinId).toLowerCase();
1866
- }
1867
- async function withPrices(raw, registry, price) {
1868
- try {
1869
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
1870
- const quotes = await price.getPrices(ids);
1871
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
1872
- } catch {
1873
- return raw;
1874
- }
1875
- }
1876
- function priceOne(asset, quote) {
1877
- if (!quote) return asset;
1878
- const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
1879
- return {
1880
- ...asset,
1881
- priceUsd: quote.priceUsd,
1882
- priceEur: quote.priceEur ?? null,
1883
- change24h: quote.change24h ?? null,
1884
- fiatValueUsd: human * quote.priceUsd,
1885
- fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
1886
- };
1887
- }
1888
- function transferringToken(tokenId, coinId, amount, registry, now) {
1889
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
1890
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
1891
- transferring: true,
1892
- suspectedSpent: false
1893
- });
1894
- }
1895
-
1896
- // modules/payments-v2/machine/payload.ts
1897
- function buildPayload(recipientPubkey, request, spend, spentStates) {
1898
- return {
1899
- v: 2,
1900
- recipient: recipientPubkey,
1901
- coinId: request.coinId,
1902
- amount: request.amount,
1903
- ...request.memo !== void 0 ? { memo: request.memo } : {},
1904
- direct: [...spend.plan.direct],
1905
- ...spend.plan.split !== void 0 ? {
1906
- split: {
1907
- tokenId: spend.plan.split.tokenId,
1908
- splitAmount: spend.plan.split.splitAmount.toString(),
1909
- remainderAmount: spend.plan.split.remainderAmount.toString()
1910
- }
1911
- } : {},
1912
- spentStates
1913
- };
1914
- }
1915
- function messageOf(err) {
1916
- return err instanceof Error ? err.message : String(err);
1917
- }
1918
-
1919
2133
  // node_modules/@noble/ciphers/utils.js
1920
2134
  function isBytes2(a) {
1921
2135
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
@@ -2767,6 +2981,14 @@ function decryptField(key, envelope) {
2767
2981
  }
2768
2982
 
2769
2983
  // modules/payments-v2/history/History.ts
2984
+ function wireAssets(assets) {
2985
+ return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
2986
+ }
2987
+ function isPermanentReject(err) {
2988
+ const status = err?.status;
2989
+ if (typeof status !== "number") return false;
2990
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
2991
+ }
2770
2992
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
2771
2993
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
2772
2994
  var History = class {
@@ -2791,7 +3013,7 @@ var History = class {
2791
3013
  dedupKey: input.transferId,
2792
3014
  id: (this.deps.newId ?? randomUUID)(),
2793
3015
  type: "SENT",
2794
- assets: [{ coinId: input.coinId, amount: input.amount }],
3016
+ assets: wireAssets(input.assets),
2795
3017
  ts: this.ts(input.timestamp),
2796
3018
  transferId: input.transferId,
2797
3019
  ...this.wireTokenId(input.tokenId),
@@ -2804,7 +3026,7 @@ var History = class {
2804
3026
  dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
2805
3027
  id: (this.deps.newId ?? randomUUID)(),
2806
3028
  type: "RECEIVED",
2807
- assets: [{ coinId: input.coinId, amount: input.amount }],
3029
+ assets: wireAssets(input.assets),
2808
3030
  ts: this.ts(input.timestamp),
2809
3031
  ...input.transferId !== void 0 ? { transferId: input.transferId } : {},
2810
3032
  ...this.wireTokenId(input.tokenId),
@@ -2817,7 +3039,7 @@ var History = class {
2817
3039
  dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
2818
3040
  id: (this.deps.newId ?? randomUUID)(),
2819
3041
  type: "MINT",
2820
- assets: [{ coinId: input.coinId, amount: input.amount }],
3042
+ assets: wireAssets(input.assets),
2821
3043
  ts: this.ts(input.timestamp),
2822
3044
  ...this.wireTokenId(input.tokenId)
2823
3045
  }));
@@ -2828,7 +3050,10 @@ var History = class {
2828
3050
  try {
2829
3051
  await this.deps.client.postHistory([record]);
2830
3052
  } catch (err) {
2831
- this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
3053
+ this.deps.log?.(
3054
+ isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
3055
+ err
3056
+ );
2832
3057
  return;
2833
3058
  }
2834
3059
  this.deps.emit("history:updated", this.toEntry(record));
@@ -2895,6 +3120,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
2895
3120
  function stateKey(tokenId, stateHash) {
2896
3121
  return `${tokenId}:${stateHash}`;
2897
3122
  }
3123
+ function unchanged(prev, item, tokenType) {
3124
+ return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
3125
+ }
3126
+ function isCoinless(item, prev) {
3127
+ if (item.status !== "active") return prev?.coinless ?? false;
3128
+ return (item.assets?.length ?? 0) === 0;
3129
+ }
2898
3130
  var InventoryView = class {
2899
3131
  constructor(deps) {
2900
3132
  this.deps = deps;
@@ -2988,14 +3220,26 @@ var InventoryView = class {
2988
3220
  pool(coinId) {
2989
3221
  const out = [];
2990
3222
  for (const [tokenId, entry] of this.mirror) {
2991
- if (entry.status !== "active") continue;
2992
- if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
2993
- if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
3223
+ if (!this.isSpendable(tokenId, entry)) continue;
2994
3224
  const asset = entry.assets.find((a) => a.coinId === coinId);
2995
3225
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
2996
3226
  }
2997
3227
  return out;
2998
3228
  }
3229
+ /**
3230
+ * The eligibility gates every spend shares, coin or token — ONE definition, so a
3231
+ * single probe covers both verbs and neither can drift from the other.
3232
+ */
3233
+ isSpendable(tokenId, entry) {
3234
+ if (entry.status !== "active") return false;
3235
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
3236
+ return !this.suspected.has(stateKey(tokenId, entry.stateHash));
3237
+ }
3238
+ /** Is this NAMED token spendable whole — coinless or valued (#777)? */
3239
+ spendableToken(tokenId) {
3240
+ const entry = this.mirror.get(tokenId);
3241
+ return entry !== void 0 && this.isSpendable(tokenId, entry);
3242
+ }
2999
3243
  /**
3000
3244
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
3001
3245
  * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
@@ -3021,6 +3265,29 @@ var InventoryView = class {
3021
3265
  }
3022
3266
  return out;
3023
3267
  }
3268
+ /**
3269
+ * Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
3270
+ * exactly when it is not, so no mirror row can appear in both reads.
3271
+ */
3272
+ coinless(registry) {
3273
+ const out = [];
3274
+ for (const [tokenId, entry] of this.mirror) {
3275
+ if (entry.status !== "active" || !entry.coinless) continue;
3276
+ const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
3277
+ out.push({
3278
+ tokenId,
3279
+ ...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
3280
+ ...meta ? { name: meta.name } : {},
3281
+ ...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
3282
+ stateHash: entry.stateHash,
3283
+ transferring: this.held(tokenId),
3284
+ ...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
3285
+ createdAt: entry.createdAt,
3286
+ updatedAt: entry.updatedAt
3287
+ });
3288
+ }
3289
+ return out;
3290
+ }
3024
3291
  async assets(registry, price) {
3025
3292
  const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
3026
3293
  if (!price || raw.length === 0) return raw;
@@ -3092,14 +3359,19 @@ var InventoryView = class {
3092
3359
  applyOne(item, now) {
3093
3360
  const prev = this.mirror.get(item.tokenId);
3094
3361
  if (prev && item.seq < prev.seq) return false;
3095
- if (prev && prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash) {
3096
- return false;
3097
- }
3362
+ const tokenType = item.tokenType ?? prev?.tokenType;
3363
+ if (prev && unchanged(prev, item, tokenType)) return false;
3098
3364
  this.mirror.set(item.tokenId, {
3099
3365
  stateHash: item.stateHash,
3100
3366
  seq: item.seq,
3101
3367
  status: item.status,
3102
- assets: item.assets ?? prev?.assets ?? [],
3368
+ // ONLY a tombstone inherits: it omits assets for an unrelated reason and
3369
+ // recoverRemoved must know the amount it restores. An ACTIVE row that omits
3370
+ // them is stating coinlessness, and inheriting there would keep stale assets
3371
+ // in tokens() while `coinless` is true — the row in BOTH reads (§16).
3372
+ assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
3373
+ coinless: isCoinless(item, prev),
3374
+ ...tokenType !== void 0 ? { tokenType } : {},
3103
3375
  createdAt: prev?.createdAt ?? now,
3104
3376
  updatedAt: now
3105
3377
  });
@@ -3190,6 +3462,73 @@ var InventoryView = class {
3190
3462
  }
3191
3463
  };
3192
3464
 
3465
+ // token-engine/sdk.ts
3466
+ var import_StateTransitionClient = require("@unicitylabs/state-transition-sdk/lib/StateTransitionClient.js");
3467
+ var import_AggregatorClient = require("@unicitylabs/state-transition-sdk/lib/api/AggregatorClient.js");
3468
+ var import_NetworkId = require("@unicitylabs/state-transition-sdk/lib/api/NetworkId.js");
3469
+ var import_CertificationData = require("@unicitylabs/state-transition-sdk/lib/api/CertificationData.js");
3470
+ var import_CertificationResponse = require("@unicitylabs/state-transition-sdk/lib/api/CertificationResponse.js");
3471
+ var import_StateId = require("@unicitylabs/state-transition-sdk/lib/api/StateId.js");
3472
+ var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js");
3473
+ var import_InclusionProofResponse = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js");
3474
+ var import_JsonRpcNetworkError = require("@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js");
3475
+ var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js");
3476
+ var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js");
3477
+ var import_InclusionProofVerificationRule = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js");
3478
+ var import_Token = require("@unicitylabs/state-transition-sdk/lib/transaction/Token.js");
3479
+ var import_MintTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransaction.js");
3480
+ var import_TransferTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction.js");
3481
+ var import_CertifiedMintTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/CertifiedMintTransaction.js");
3482
+ var import_CertifiedTransferTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/CertifiedTransferTransaction.js");
3483
+ var import_TokenId = require("@unicitylabs/state-transition-sdk/lib/transaction/TokenId.js");
3484
+ var import_TokenType = require("@unicitylabs/state-transition-sdk/lib/transaction/TokenType.js");
3485
+ var import_TokenSalt = require("@unicitylabs/state-transition-sdk/lib/transaction/TokenSalt.js");
3486
+ var import_StateMask = require("@unicitylabs/state-transition-sdk/lib/transaction/StateMask.js");
3487
+ var import_MintJustificationVerifierService = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/MintJustificationVerifierService.js");
3488
+ var import_TokenIssuanceVerifierService = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/TokenIssuanceVerifierService.js");
3489
+ var import_VerificationContext = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/VerificationContext.js");
3490
+ var import_UnicityCertificateVerifier = require("@unicitylabs/state-transition-sdk/lib/api/bft/verification/UnicityCertificateVerifier.js");
3491
+ var import_UnicitySealQuorumSignaturesVerificationRule = require("@unicitylabs/state-transition-sdk/lib/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.js");
3492
+ var import_VerifiedSealCache = require("@unicitylabs/state-transition-sdk/lib/api/bft/verification/VerifiedSealCache.js");
3493
+ var import_Secp256k1SignatureVerifier = require("@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/Secp256k1SignatureVerifier.js");
3494
+ var import_WorkerTokenVerifier = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/worker/WorkerTokenVerifier.js");
3495
+ var import_EncodedPredicate = require("@unicitylabs/state-transition-sdk/lib/predicate/EncodedPredicate.js");
3496
+ var import_SignaturePredicate = require("@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicate.js");
3497
+ var import_SignaturePredicateUnlockScript = require("@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicateUnlockScript.js");
3498
+ var import_BurnPredicate = require("@unicitylabs/state-transition-sdk/lib/predicate/builtin/BurnPredicate.js");
3499
+ var import_PredicateVerifierService = require("@unicitylabs/state-transition-sdk/lib/predicate/verification/PredicateVerifierService.js");
3500
+ var import_SigningService = require("@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/SigningService.js");
3501
+ var import_Signature = require("@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/Signature.js");
3502
+ var import_MintSigningService = require("@unicitylabs/state-transition-sdk/lib/crypto/MintSigningService.js");
3503
+ var import_HashAlgorithm = require("@unicitylabs/state-transition-sdk/lib/crypto/hash/HashAlgorithm.js");
3504
+ var import_DataHash = require("@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHash.js");
3505
+ var import_DataHasher = require("@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasher.js");
3506
+ var import_DataHasherFactory = require("@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasherFactory.js");
3507
+ var import_CborSerializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js");
3508
+ var import_CborDeserializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js");
3509
+ var import_CborError = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js");
3510
+ var import_CborReader = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js");
3511
+ var import_MajorType = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js");
3512
+ var import_Asset = require("@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js");
3513
+ var import_AssetId = require("@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js");
3514
+ var import_PaymentAssetCollection = require("@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js");
3515
+ var import_TokenSplit = require("@unicitylabs/state-transition-sdk/lib/payment/TokenSplit.js");
3516
+ var import_SplitTokenRequest = require("@unicitylabs/state-transition-sdk/lib/payment/SplitTokenRequest.js");
3517
+ var import_SplitToken = require("@unicitylabs/state-transition-sdk/lib/payment/SplitToken.js");
3518
+ var import_SplitAllocationProof = require("@unicitylabs/state-transition-sdk/lib/payment/SplitAllocationProof.js");
3519
+ var import_SplitMintJustification = require("@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustification.js");
3520
+ var import_SplitMintJustificationVerifier = require("@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustificationVerifier.js");
3521
+ var import_VerificationStatus = require("@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js");
3522
+ var import_VerificationResult = require("@unicitylabs/state-transition-sdk/lib/verification/VerificationResult.js");
3523
+ var import_HexConverter = require("@unicitylabs/state-transition-sdk/lib/util/HexConverter.js");
3524
+ var import_BigintConverter = require("@unicitylabs/state-transition-sdk/lib/util/BigintConverter.js");
3525
+ var import_BitString = require("@unicitylabs/state-transition-sdk/lib/util/BitString.js");
3526
+
3527
+ // token-engine/value-envelope.ts
3528
+ function isCoinlessEnvelope(envelope) {
3529
+ return envelope.startsWith("none_");
3530
+ }
3531
+
3193
3532
  // modules/payments-v2/receive/Receive.ts
3194
3533
  var ACK_BATCH_SIZE = 200;
3195
3534
  var REFRESH_INTERVAL_MS = 2500;
@@ -3330,7 +3669,12 @@ async function screen(deps, engine, entry) {
3330
3669
  }
3331
3670
  return {
3332
3671
  kind: "accept",
3333
- record: { tokenId: keys.tokenId, stateHash: keys.stateHash, assets: toAssetAmounts(token) }
3672
+ record: {
3673
+ tokenId: keys.tokenId,
3674
+ stateHash: keys.stateHash,
3675
+ assets: toAssetAmounts(token),
3676
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
3677
+ }
3334
3678
  };
3335
3679
  }
3336
3680
  async function announce(deps, entry, record) {
@@ -3341,6 +3685,7 @@ async function announce(deps, entry, record) {
3341
3685
  tokenId: record.tokenId,
3342
3686
  stateHash: record.stateHash,
3343
3687
  assets: record.assets,
3688
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
3344
3689
  ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
3345
3690
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
3346
3691
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
@@ -3354,6 +3699,19 @@ async function announce(deps, entry, record) {
3354
3699
  senderPubkey: entry.senderPubkey ?? "",
3355
3700
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
3356
3701
  tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
3702
+ // #777: named here rather than mapped from assets, which announced an EMPTY list.
3703
+ ...record.tokenType !== void 0 ? {
3704
+ coinless: [
3705
+ {
3706
+ tokenId: record.tokenId,
3707
+ tokenType: record.tokenType,
3708
+ stateHash: record.stateHash,
3709
+ transferring: false,
3710
+ createdAt: receivedAt,
3711
+ updatedAt: receivedAt
3712
+ }
3713
+ ]
3714
+ } : {},
3357
3715
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
3358
3716
  receivedAt
3359
3717
  };
@@ -4019,6 +4377,34 @@ var SpendQueue = class {
4019
4377
  }
4020
4378
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
4021
4379
  }
4380
+ /**
4381
+ * #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
4382
+ * for. A coin plan waits because some OTHER combination may free up; a named
4383
+ * token is either free now or held by a reservation that only its own transfer
4384
+ * can release, so queueing would block until a timeout on a spend that cannot
4385
+ * become possible.
4386
+ */
4387
+ planWhole(reservationId, tokenId) {
4388
+ if (this.destroyed) {
4389
+ throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
4390
+ }
4391
+ const unproven = this.deps.ledger.unprovenReason();
4392
+ if (unproven !== null) {
4393
+ throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
4394
+ }
4395
+ if (this.deps.spendableToken?.(tokenId) !== true) {
4396
+ throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
4397
+ }
4398
+ const holder = this.deps.ledger.holderOf(tokenId);
4399
+ if (holder !== void 0) {
4400
+ throw new SphereError(
4401
+ `Token ${tokenId} is already reserved by transfer ${holder}`,
4402
+ "VALIDATION_ERROR"
4403
+ );
4404
+ }
4405
+ this.deps.ledger.reserve(reservationId, [{ tokenId }]);
4406
+ return { reservationId, plan: { direct: [tokenId] } };
4407
+ }
4022
4408
  /**
4023
4409
  * The sources `plan()` WOULD pick right now, reserving nothing and queueing
4024
4410
  * nothing — a read-only probe used to warm blobs while the user is still
@@ -4208,6 +4594,7 @@ function composeFacadeParts(deps, hooks) {
4208
4594
  const queue = new SpendQueue({
4209
4595
  ledger,
4210
4596
  getPool: (coinId) => view.pool(coinId),
4597
+ spendableToken: (tokenId) => view.spendableToken(tokenId),
4211
4598
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
4212
4599
  });
4213
4600
  const historyStore = new History({
@@ -4288,13 +4675,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
4288
4675
  ...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
4289
4676
  emit: deps.emit,
4290
4677
  now: deps.now ?? Date.now,
4291
- recordHistory: async ({ transferId, payload, committedAmount }) => {
4678
+ recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
4292
4679
  await historyStore.recordSent({
4293
4680
  transferId,
4294
- coinId: payload.coinId,
4295
- // §5.9: the SETTLED amount (machine-computed from the certified
4296
- // recipient blobs), never payload.amount — the plan.
4297
- amount: committedAmount,
4681
+ // §5.9: the SETTLED amount, never payload.amount — the plan. A token
4682
+ // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
4683
+ ...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
4298
4684
  recipientPubkey: payload.recipient,
4299
4685
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
4300
4686
  });
@@ -4329,12 +4715,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
4329
4715
  });
4330
4716
  }
4331
4717
  async function recordReceived(historyStore, record) {
4332
- const first = record.assets[0];
4333
4718
  await historyStore.recordReceived({
4334
4719
  tokenId: record.tokenId,
4335
4720
  stateHash: record.stateHash,
4336
- coinId: first?.coinId ?? "",
4337
- amount: first?.amount ?? "0",
4721
+ assets: record.assets,
4338
4722
  ...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
4339
4723
  ...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
4340
4724
  ...record.memo !== void 0 ? { memo: record.memo } : {},
@@ -4482,6 +4866,13 @@ var PaymentsFacade = class {
4482
4866
  tokens(filter) {
4483
4867
  return this.view.tokens(this.deps.registry, filter);
4484
4868
  }
4869
+ coinless() {
4870
+ return this.view.coinless(this.deps.registry);
4871
+ }
4872
+ tokenData(tokenId) {
4873
+ const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
4874
+ return readTokenData(deps, tokenId);
4875
+ }
4485
4876
  history(page) {
4486
4877
  return this.historyStore.page(page ?? {});
4487
4878
  }
@@ -4504,7 +4895,14 @@ var PaymentsFacade = class {
4504
4895
  return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
4505
4896
  }
4506
4897
  send(request) {
4507
- return this.track(this.sendOutcome(request));
4898
+ return this.track(this.runJob({ kind: "coin", request }, request.amount));
4899
+ }
4900
+ sendWholeToken(request) {
4901
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
4902
+ }
4903
+ /** NFT-scoped twin: refuses a valued source. */
4904
+ sendCoinless(request) {
4905
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
4508
4906
  }
4509
4907
  async receive() {
4510
4908
  const transfers = await this.track(this.receiveLoop.drainOnce());
@@ -4536,12 +4934,25 @@ var PaymentsFacade = class {
4536
4934
  nowMs() {
4537
4935
  return (this.deps.now ?? Date.now)();
4538
4936
  }
4937
+ mintDeps() {
4938
+ return {
4939
+ engine: this.engine(),
4940
+ mintJournal: this.machineStores.mintJournal,
4941
+ storagePort: this.deps.storagePort,
4942
+ recordMint: (input) => this.historyStore.recordMint(input),
4943
+ armHeartbeat: () => this.heartbeat.arm(),
4944
+ noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
4945
+ refreshView: () => this.trackTail(this.view.delta()),
4946
+ ownPubkeyBytes: this.ownPubkeyBytes,
4947
+ now: () => this.nowMs()
4948
+ };
4949
+ }
4539
4950
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
4540
4951
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
4541
4952
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
4542
- async sendOutcome(request) {
4953
+ async runJob(job, amount) {
4543
4954
  const run = {
4544
- amount: request.amount,
4955
+ amount,
4545
4956
  delivered: [],
4546
4957
  sentTokens: [],
4547
4958
  tokenTransfers: [],
@@ -4551,7 +4962,7 @@ var PaymentsFacade = class {
4551
4962
  lastTransferId: ""
4552
4963
  };
4553
4964
  try {
4554
- return await this.sendWithPolicy(request, run);
4965
+ return await this.sendWithPolicy(job, run);
4555
4966
  } catch (err) {
4556
4967
  this.emitCleanFailure(err, run.lastTransferId);
4557
4968
  throw err;
@@ -4571,19 +4982,19 @@ var PaymentsFacade = class {
4571
4982
  };
4572
4983
  this.deps.emit("transfer:updated", failed);
4573
4984
  }
4574
- async sendWithPolicy(request, run) {
4575
- const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
4985
+ async sendWithPolicy(job, run) {
4986
+ const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
4576
4987
  for (let attempt = 0; ; attempt++) {
4577
4988
  let ctx;
4578
4989
  try {
4579
- ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
4990
+ ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
4580
4991
  } catch (err) {
4581
- throw this.partialize(err, run);
4992
+ throw partialize(err, run);
4582
4993
  }
4583
4994
  const disposition = await this.runAttempt(ctx);
4584
- if (disposition.kind === "rethrow") throw this.partialize(disposition.error, run);
4995
+ if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
4585
4996
  if (disposition.kind === "retry-full") {
4586
- if (attempt >= MAX_RESELECT) throw this.partialize(disposition.error, run);
4997
+ if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
4587
4998
  continue;
4588
4999
  }
4589
5000
  if (disposition.kind === "success") {
@@ -4639,7 +5050,7 @@ var PaymentsFacade = class {
4639
5050
  async disposeFailedAttempt(ctx, err) {
4640
5051
  if (isPossiblyCommittedSendOutcome(err)) {
4641
5052
  this.settleKeepOpen(ctx);
4642
- this.stampTransferId(err, ctx.transferId);
5053
+ stampTransferId(err, ctx.transferId);
4643
5054
  return { kind: "rethrow", error: err };
4644
5055
  }
4645
5056
  const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
@@ -4673,7 +5084,7 @@ var PaymentsFacade = class {
4673
5084
  async disposeConvergeFailure(ctx, err) {
4674
5085
  if (isPossiblyCommittedSendOutcome(err)) {
4675
5086
  this.settleKeepOpen(ctx);
4676
- this.stampTransferId(err, ctx.transferId);
5087
+ stampTransferId(err, ctx.transferId);
4677
5088
  return { kind: "rethrow", error: err };
4678
5089
  }
4679
5090
  if (classifyError(err) === "conflict") {
@@ -4684,9 +5095,29 @@ var PaymentsFacade = class {
4684
5095
  }
4685
5096
  return { kind: "rethrow", error: err };
4686
5097
  }
4687
- async planAndMaterialize(recipientPubkey, request, run) {
5098
+ async planAndMaterialize(recipientPubkey, job, run) {
4688
5099
  const transferId = this.newId();
4689
5100
  run.lastTransferId = transferId;
5101
+ if (job.kind === "whole") {
5102
+ const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
5103
+ const sourceIds2 = this.markPlanned(transferId, "", spend2);
5104
+ try {
5105
+ return await materializeWholeSpend(
5106
+ { engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
5107
+ {
5108
+ transferId,
5109
+ recipientPubkey,
5110
+ request: job.request,
5111
+ sourceIds: sourceIds2,
5112
+ requireCoinless: job.requireCoinless
5113
+ }
5114
+ );
5115
+ } catch (err) {
5116
+ this.settleFailure(transferId, "", sourceIds2);
5117
+ throw err;
5118
+ }
5119
+ }
5120
+ const request = { ...job.request, amount: run.amount };
4690
5121
  const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
4691
5122
  const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
4692
5123
  const sourceIds = this.markPlanned(transferId, request.coinId, spend);
@@ -4820,22 +5251,6 @@ var PaymentsFacade = class {
4820
5251
  return result;
4821
5252
  }
4822
5253
  /** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
4823
- partialize(err, run) {
4824
- if (run.delivered.length === 0) return err;
4825
- return new PartialSendConflictError(
4826
- "Part of your payment was sent; the remaining amount could not be completed (see cause). The delivered portion is final \u2014 re-plan only the shortfall, never the full amount.",
4827
- run.firstPartialId ?? "",
4828
- run.delivered,
4829
- run.amount,
4830
- err
4831
- );
4832
- }
4833
- /** #441: possibly-committed errors must carry the transferId for the settling journal. */
4834
- stampTransferId(err, transferId) {
4835
- if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
4836
- err.transferId = transferId;
4837
- }
4838
- }
4839
5254
  async softAbort(transferId) {
4840
5255
  try {
4841
5256
  await this.deps.client.abortIntent(transferId);
@@ -4851,57 +5266,11 @@ var PaymentsFacade = class {
4851
5266
  const mintId = this.newId();
4852
5267
  this.activeMoneyOps.add(mintId);
4853
5268
  try {
4854
- return await this.mintUnderJournal(mintId, coinId, amount);
5269
+ return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
4855
5270
  } finally {
4856
5271
  this.activeMoneyOps.delete(mintId);
4857
5272
  }
4858
5273
  }
4859
- async mintUnderJournal(mintId, coinId, amount) {
4860
- const engine = this.engine();
4861
- const entry = {
4862
- mintId,
4863
- coinId,
4864
- amount: amount.toString(),
4865
- tokenId: "",
4866
- createdAt: this.nowMs()
4867
- };
4868
- await this.machineStores.mintJournal.upsert(entry);
4869
- let token;
4870
- try {
4871
- token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
4872
- } catch (err) {
4873
- this.heartbeat.arm();
4874
- return { success: false, error: messageOf(err) };
4875
- }
4876
- if (token.blob.tokenId !== entry.tokenId) {
4877
- await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
4878
- }
4879
- try {
4880
- await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
4881
- } catch (err) {
4882
- this.heartbeat.arm();
4883
- return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
4884
- }
4885
- return { success: true, tokenId: token.blob.tokenId };
4886
- }
4887
- async finalizeMint(engine, mintId, token, coinId, amount) {
4888
- const bytes = token.blob.token;
4889
- const digest = bytesToHex(sha256(bytes));
4890
- const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
4891
- const key = keys.get(digest);
4892
- if (key === void 0) {
4893
- throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
4894
- }
4895
- await this.deps.storagePort.applyDelta({
4896
- transferId: mintId,
4897
- spent: [],
4898
- added: [{ tokenId: token.blob.tokenId, key }]
4899
- });
4900
- await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
4901
- await this.machineStores.mintJournal.removeByKey(mintId);
4902
- this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
4903
- this.trackTail(this.view.delta());
4904
- }
4905
5274
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
4906
5275
  async replayMints() {
4907
5276
  let resolved = 0;
@@ -4933,7 +5302,7 @@ var PaymentsFacade = class {
4933
5302
  if (token.blob.tokenId !== entry.tokenId) {
4934
5303
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
4935
5304
  }
4936
- await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
5305
+ await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
4937
5306
  return true;
4938
5307
  }
4939
5308
  async tokenInServerInventory(tokenId) {