@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
@@ -137,6 +137,135 @@ var STORE_KEYS = {
137
137
  knownSpends: "known-spends"
138
138
  };
139
139
 
140
+ // core/errors.ts
141
+ var SphereError = class extends Error {
142
+ code;
143
+ cause;
144
+ /**
145
+ * #441 deferred-paid linkage: the transferId of the possibly-committed send,
146
+ * stamped by `sendOnce` before the error leaves so a payment-request consumer
147
+ * can durably journal request→transfer and resolve 'paid' only when that
148
+ * transfer actually completes (never optimistically). Present ONLY on
149
+ * possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
150
+ */
151
+ transferId;
152
+ constructor(message, code, cause) {
153
+ super(message);
154
+ this.name = "SphereError";
155
+ this.code = code;
156
+ this.cause = cause;
157
+ }
158
+ };
159
+ var PartialSendConflictError = class extends SphereError {
160
+ /**
161
+ * The FIRST partial attempt's transferId — its intent stays open for resume,
162
+ * and it is the §6 handoff anchor. NOTE: `send()` may re-plan the remainder across
163
+ * SEVERAL internal attempts before giving up, so {@link committedTokenIds} can
164
+ * span MULTIPLE transferIds — they do NOT all map to this single id.
165
+ */
166
+ transferId;
167
+ /**
168
+ * Source token ids whose spend already certified on-chain (and whose finished
169
+ * output blob is journaled for delivery). Their value has already been
170
+ * delivered; NEVER re-send these. May accumulate across several internal
171
+ * remainder-re-plan attempts (so not all are journaled under {@link transferId}).
172
+ */
173
+ committedTokenIds;
174
+ /**
175
+ * The still-undelivered portion (base units, decimal string) — `send()` could
176
+ * not COMPLETE it from the remaining live sources (insufficient funds or a
177
+ * transient error; see `cause`). Re-plan ONLY this amount, under a new
178
+ * transferId; the delivered legs converge via the recipient's §6 claim.
179
+ */
180
+ remainingAmount;
181
+ constructor(message, transferId, committedTokenIds, remainingAmount, cause) {
182
+ super(message, "SEND_PARTIALLY_COMPLETED", cause);
183
+ this.name = "PartialSendConflictError";
184
+ this.transferId = transferId;
185
+ this.committedTokenIds = [...committedTokenIds];
186
+ this.remainingAmount = remainingAmount;
187
+ }
188
+ };
189
+ var POSSIBLY_COMMITTED_SEND_CODES = /* @__PURE__ */ new Set([
190
+ "SEND_SYNC_PENDING",
191
+ "CERTIFICATION_UNCONFIRMED",
192
+ "CHECKPOINT_PERSIST_FAILED",
193
+ "SPLIT_CHECKPOINT_LOST",
194
+ "CHECKPOINT_TRUSTBASE_MISMATCH",
195
+ "SEND_PARTIALLY_COMPLETED"
196
+ ]);
197
+ function isPossiblyCommittedSendOutcome(err) {
198
+ return isSphereError(err) && POSSIBLY_COMMITTED_SEND_CODES.has(err.code);
199
+ }
200
+ function isSphereError(err) {
201
+ return err instanceof SphereError;
202
+ }
203
+
204
+ // core/uuid.ts
205
+ function randomUUID() {
206
+ const c = globalThis.crypto;
207
+ if (typeof c !== "undefined" && typeof c.randomUUID === "function") {
208
+ return c.randomUUID();
209
+ }
210
+ const b = c.getRandomValues(new Uint8Array(16));
211
+ b[6] = b[6] & 15 | 64;
212
+ b[8] = b[8] & 63 | 128;
213
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, "0"));
214
+ 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]}`;
215
+ }
216
+
217
+ // modules/payments-v2/async.ts
218
+ var SingleFlight = class {
219
+ flight = null;
220
+ run(fn) {
221
+ if (this.flight !== null) return this.flight;
222
+ const current = fn().finally(() => {
223
+ this.flight = null;
224
+ });
225
+ this.flight = current;
226
+ return current;
227
+ }
228
+ };
229
+ var SerialChain = class {
230
+ tail = Promise.resolve();
231
+ /** FIFO; starts after every prior fn settled; returns this fn's own outcome. */
232
+ enqueue(fn) {
233
+ const run = this.tail.then(fn, fn);
234
+ this.tail = run.then(
235
+ () => void 0,
236
+ () => void 0
237
+ );
238
+ return run;
239
+ }
240
+ };
241
+ function coalesced(run, observe) {
242
+ let inFlight = false;
243
+ let queued = false;
244
+ const start = () => {
245
+ inFlight = true;
246
+ let running;
247
+ try {
248
+ running = run();
249
+ } catch {
250
+ running = Promise.resolve();
251
+ }
252
+ observe?.(running);
253
+ void running.catch(() => void 0).finally(() => {
254
+ inFlight = false;
255
+ if (!queued) return;
256
+ queued = false;
257
+ start();
258
+ });
259
+ };
260
+ return () => {
261
+ if (inFlight) {
262
+ queued = true;
263
+ return;
264
+ }
265
+ start();
266
+ };
267
+ }
268
+
140
269
  // node_modules/@noble/hashes/utils.js
141
270
  function isBytes(a) {
142
271
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
@@ -464,72 +593,6 @@ var sha256 = /* @__PURE__ */ createHasher(
464
593
  // core/crypto.ts
465
594
  import * as bip39 from "bip39";
466
595
  import CryptoJS from "crypto-js";
467
-
468
- // core/errors.ts
469
- var SphereError = class extends Error {
470
- code;
471
- cause;
472
- /**
473
- * #441 deferred-paid linkage: the transferId of the possibly-committed send,
474
- * stamped by `sendOnce` before the error leaves so a payment-request consumer
475
- * can durably journal request→transfer and resolve 'paid' only when that
476
- * transfer actually completes (never optimistically). Present ONLY on
477
- * possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
478
- */
479
- transferId;
480
- constructor(message, code, cause) {
481
- super(message);
482
- this.name = "SphereError";
483
- this.code = code;
484
- this.cause = cause;
485
- }
486
- };
487
- var PartialSendConflictError = class extends SphereError {
488
- /**
489
- * The FIRST partial attempt's transferId — its intent stays open for resume,
490
- * and it is the §6 handoff anchor. NOTE: `send()` may re-plan the remainder across
491
- * SEVERAL internal attempts before giving up, so {@link committedTokenIds} can
492
- * span MULTIPLE transferIds — they do NOT all map to this single id.
493
- */
494
- transferId;
495
- /**
496
- * Source token ids whose spend already certified on-chain (and whose finished
497
- * output blob is journaled for delivery). Their value has already been
498
- * delivered; NEVER re-send these. May accumulate across several internal
499
- * remainder-re-plan attempts (so not all are journaled under {@link transferId}).
500
- */
501
- committedTokenIds;
502
- /**
503
- * The still-undelivered portion (base units, decimal string) — `send()` could
504
- * not COMPLETE it from the remaining live sources (insufficient funds or a
505
- * transient error; see `cause`). Re-plan ONLY this amount, under a new
506
- * transferId; the delivered legs converge via the recipient's §6 claim.
507
- */
508
- remainingAmount;
509
- constructor(message, transferId, committedTokenIds, remainingAmount, cause) {
510
- super(message, "SEND_PARTIALLY_COMPLETED", cause);
511
- this.name = "PartialSendConflictError";
512
- this.transferId = transferId;
513
- this.committedTokenIds = [...committedTokenIds];
514
- this.remainingAmount = remainingAmount;
515
- }
516
- };
517
- var POSSIBLY_COMMITTED_SEND_CODES = /* @__PURE__ */ new Set([
518
- "SEND_SYNC_PENDING",
519
- "CERTIFICATION_UNCONFIRMED",
520
- "CHECKPOINT_PERSIST_FAILED",
521
- "SPLIT_CHECKPOINT_LOST",
522
- "CHECKPOINT_TRUSTBASE_MISMATCH",
523
- "SEND_PARTIALLY_COMPLETED"
524
- ]);
525
- function isPossiblyCommittedSendOutcome(err) {
526
- return isSphereError(err) && POSSIBLY_COMMITTED_SEND_CODES.has(err.code);
527
- }
528
- function isSphereError(err) {
529
- return err instanceof SphereError;
530
- }
531
-
532
- // core/crypto.ts
533
596
  var CURVE_ORDER = BigInt(
534
597
  "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"
535
598
  );
@@ -544,71 +607,6 @@ function bytesToHex(bytes) {
544
607
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
545
608
  }
546
609
 
547
- // core/uuid.ts
548
- function randomUUID() {
549
- const c = globalThis.crypto;
550
- if (typeof c !== "undefined" && typeof c.randomUUID === "function") {
551
- return c.randomUUID();
552
- }
553
- const b = c.getRandomValues(new Uint8Array(16));
554
- b[6] = b[6] & 15 | 64;
555
- b[8] = b[8] & 63 | 128;
556
- const h = Array.from(b, (x) => x.toString(16).padStart(2, "0"));
557
- 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]}`;
558
- }
559
-
560
- // modules/payments-v2/async.ts
561
- var SingleFlight = class {
562
- flight = null;
563
- run(fn) {
564
- if (this.flight !== null) return this.flight;
565
- const current = fn().finally(() => {
566
- this.flight = null;
567
- });
568
- this.flight = current;
569
- return current;
570
- }
571
- };
572
- var SerialChain = class {
573
- tail = Promise.resolve();
574
- /** FIFO; starts after every prior fn settled; returns this fn's own outcome. */
575
- enqueue(fn) {
576
- const run = this.tail.then(fn, fn);
577
- this.tail = run.then(
578
- () => void 0,
579
- () => void 0
580
- );
581
- return run;
582
- }
583
- };
584
- function coalesced(run, observe) {
585
- let inFlight = false;
586
- let queued = false;
587
- const start = () => {
588
- inFlight = true;
589
- let running;
590
- try {
591
- running = run();
592
- } catch {
593
- running = Promise.resolve();
594
- }
595
- observe?.(running);
596
- void running.catch(() => void 0).finally(() => {
597
- inFlight = false;
598
- if (!queued) return;
599
- queued = false;
600
- start();
601
- });
602
- };
603
- return () => {
604
- if (inFlight) {
605
- queued = true;
606
- return;
607
- }
608
- start();
609
- };
610
- }
611
-
612
610
  // modules/payments-v2/machine/journal.ts
613
611
  var MAX_DELIVERY_ATTEMPTS = 6;
614
612
  var RATE_LIMIT_DEFER_MS = 3e4;
@@ -787,6 +785,17 @@ function createMachineStores(kv) {
787
785
  };
788
786
  }
789
787
 
788
+ // modules/payments-v2/machine/payload-view.ts
789
+ function isWholeIntent(p) {
790
+ return p.kind === "whole" || p.kind === "coinless";
791
+ }
792
+ function splitOf(p) {
793
+ return p.kind === "coin" ? p.split : void 0;
794
+ }
795
+ function coinIdOf(p) {
796
+ return p.kind === "coin" ? p.coinId : void 0;
797
+ }
798
+
790
799
  // token-engine/errors.ts
791
800
  var TransferConflictError = class extends SphereError {
792
801
  /**
@@ -1028,13 +1037,14 @@ var TransferMachine = class {
1028
1037
  const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
1029
1038
  return { recipientBlob: finished.blob.token };
1030
1039
  }
1031
- const split = ctx.payload.split;
1040
+ const split = splitOf(ctx.payload);
1041
+ const splitCoinId = coinIdOf(ctx.payload);
1032
1042
  const { outputs } = await engine.split(
1033
1043
  {
1034
1044
  token: source,
1035
1045
  outputs: [
1036
- { recipientPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.splitAmount) },
1037
- { recipientPubkey: this.deps.ownPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.remainderAmount) }
1046
+ { recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
1047
+ { recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
1038
1048
  ]
1039
1049
  },
1040
1050
  opts
@@ -1156,7 +1166,9 @@ var TransferMachine = class {
1156
1166
  const entry = {
1157
1167
  transferId: r.transferId,
1158
1168
  remainingAmount: undelivered.toString(),
1159
- coinId: r.payload.coinId,
1169
+ // '' only on a token-addressed intent, which is one leg: it either lands or
1170
+ // conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
1171
+ coinId: coinIdOf(r.payload) ?? "",
1160
1172
  recipient: r.payload.recipient,
1161
1173
  committedTokenIds: committed.map((o) => o.op.sourceTokenId),
1162
1174
  createdAt: this.deps.now()
@@ -1182,8 +1194,16 @@ var TransferMachine = class {
1182
1194
  }
1183
1195
  async finishBookkeeping(engine, transferId, payload, phase, committed) {
1184
1196
  try {
1185
- const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
1186
- await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
1197
+ const coinId = coinIdOf(payload);
1198
+ const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
1199
+ const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
1200
+ await this.deps.recordHistory?.({
1201
+ transferId,
1202
+ payload,
1203
+ phase,
1204
+ committedAmount,
1205
+ ...committedAssets !== void 0 ? { committedAssets } : {}
1206
+ });
1187
1207
  } catch {
1188
1208
  }
1189
1209
  try {
@@ -1197,6 +1217,18 @@ var TransferMachine = class {
1197
1217
  * a journal-rehydrated resume leg has no source token, but every committed
1198
1218
  * leg carries its recipient blob, so the outcome-derived rule is uniform).
1199
1219
  */
1220
+ /** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
1221
+ async settledAssets(engine, committed) {
1222
+ const total = /* @__PURE__ */ new Map();
1223
+ for (const outcome of committed) {
1224
+ if (outcome.recipientBlob === void 0) continue;
1225
+ const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
1226
+ for (const asset of engine.readValue(token)?.assets ?? []) {
1227
+ total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
1228
+ }
1229
+ }
1230
+ return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
1231
+ }
1200
1232
  async settledAmount(engine, coinId, committed) {
1201
1233
  let total = 0n;
1202
1234
  for (const outcome of committed) {
@@ -1225,8 +1257,11 @@ var TransferMachine = class {
1225
1257
  }
1226
1258
  };
1227
1259
  function conflictAmount(engine, payload, op, source) {
1228
- if (op.kind === "split") return BigInt(payload.split.splitAmount);
1229
- return source !== void 0 ? engine.balanceOf(source, payload.coinId) : 0n;
1260
+ const split = splitOf(payload);
1261
+ if (op.kind === "split") return BigInt(split.splitAmount);
1262
+ const coinId = coinIdOf(payload);
1263
+ if (coinId === void 0) return 0n;
1264
+ return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
1230
1265
  }
1231
1266
 
1232
1267
  // modules/payments-v2/machine/resume.ts
@@ -1333,6 +1368,14 @@ async function runOne(ctx, job, report) {
1333
1368
  report.failed.push(job.transferId);
1334
1369
  return;
1335
1370
  }
1371
+ if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
1372
+ logger.warn(
1373
+ "PaymentsV2",
1374
+ `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
1375
+ );
1376
+ report.failed.push(job.transferId);
1377
+ return;
1378
+ }
1336
1379
  mine.set(op.sourceTokenId, token);
1337
1380
  }
1338
1381
  try {
@@ -1363,6 +1406,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
1363
1406
  }
1364
1407
  report.failed.push(transferId);
1365
1408
  }
1409
+ function validateCoinPayload(c) {
1410
+ if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
1411
+ throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
1412
+ }
1413
+ const s = c.split;
1414
+ if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
1415
+ throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
1416
+ }
1417
+ return { ...c, kind: "coin" };
1418
+ }
1419
+ function validateWholePayload(p) {
1420
+ if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
1421
+ throw new SphereError(
1422
+ "token intent payload must name exactly one source token",
1423
+ "VALIDATION_ERROR"
1424
+ );
1425
+ }
1426
+ if (p.split !== void 0) {
1427
+ throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
1428
+ }
1429
+ return { ...p, kind: "whole", direct: [p.direct[0]] };
1430
+ }
1366
1431
  function validatePayload(raw) {
1367
1432
  const p = raw !== null && typeof raw === "object" ? raw : null;
1368
1433
  if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
@@ -1371,14 +1436,17 @@ function validatePayload(raw) {
1371
1436
  "VALIDATION_ERROR"
1372
1437
  );
1373
1438
  }
1374
- if (typeof p.recipient !== "string" || typeof p.coinId !== "string" || typeof p.amount !== "string") {
1375
- throw new SphereError("intent payload is missing recipient/coinId/amount", "VALIDATION_ERROR");
1439
+ if (typeof p.recipient !== "string") {
1440
+ throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
1376
1441
  }
1377
- const s = p.split;
1378
- if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
1379
- throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
1442
+ const kind = p.kind ?? "coin";
1443
+ if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
1444
+ throw new SphereError(
1445
+ `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
1446
+ "VALIDATION_ERROR"
1447
+ );
1380
1448
  }
1381
- return p;
1449
+ return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
1382
1450
  }
1383
1451
 
1384
1452
  // modules/payments-v2/convergence.ts
@@ -1578,46 +1646,314 @@ async function openRow(entry, journal, decryptPayload) {
1578
1646
  } catch {
1579
1647
  payload = null;
1580
1648
  }
1581
- const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
1582
- const certified = distinctOpCount(journal);
1583
- return {
1584
- transferId: entry.transferId,
1585
- kind: "open",
1586
- recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
1587
- coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
1588
- amount: typeof payload?.amount === "string" ? payload.amount : "",
1589
- legs: { certified, total: Math.max(total, certified) },
1590
- deliveryPending: journal.length > 0,
1591
- createdAt: entry.createdAt
1592
- };
1649
+ const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
1650
+ const certified = distinctOpCount(journal);
1651
+ return {
1652
+ transferId: entry.transferId,
1653
+ kind: "open",
1654
+ recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
1655
+ ...subject(payload),
1656
+ legs: { certified, total: Math.max(total, certified) },
1657
+ deliveryPending: journal.length > 0,
1658
+ createdAt: entry.createdAt
1659
+ };
1660
+ }
1661
+ function subject(payload) {
1662
+ const rawKind = payload?.kind;
1663
+ if (rawKind === "whole" || rawKind === "coinless") {
1664
+ const tokenId = payload?.direct?.[0];
1665
+ return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
1666
+ }
1667
+ const coin = payload;
1668
+ return {
1669
+ coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
1670
+ amount: typeof coin?.amount === "string" ? coin.amount : ""
1671
+ };
1672
+ }
1673
+ function journalOnlyRow(transferId, entries) {
1674
+ const certified = distinctOpCount(entries);
1675
+ return {
1676
+ transferId,
1677
+ kind: "open",
1678
+ recipient: entries[0].recipientPubkey,
1679
+ coinId: "",
1680
+ amount: "",
1681
+ legs: { certified, total: certified },
1682
+ deliveryPending: true,
1683
+ createdAt: 0
1684
+ };
1685
+ }
1686
+ function shortfallRow(entry) {
1687
+ return {
1688
+ transferId: entry.transferId,
1689
+ kind: "shortfall",
1690
+ recipient: entry.recipient,
1691
+ coinId: entry.coinId,
1692
+ amount: entry.remainingAmount,
1693
+ legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
1694
+ deliveryPending: false,
1695
+ createdAt: entry.createdAt
1696
+ };
1697
+ }
1698
+ function distinctOpCount(entries) {
1699
+ return new Set(entries.map((entry) => entry.opIndex)).size;
1700
+ }
1701
+
1702
+ // modules/payments-v2/inventory/token-data.ts
1703
+ async function readTokenData(deps, tokenId) {
1704
+ if (deps.view.stateHashOf(tokenId) === void 0) {
1705
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
1706
+ }
1707
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
1708
+ if (bytes === void 0) {
1709
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
1710
+ }
1711
+ return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
1712
+ }
1713
+
1714
+ // modules/payments-v2/machine/payload.ts
1715
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
1716
+ return {
1717
+ v: 2,
1718
+ kind: "coin",
1719
+ recipient: recipientPubkey,
1720
+ coinId: request.coinId,
1721
+ amount: request.amount,
1722
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
1723
+ direct: [...spend.plan.direct],
1724
+ ...spend.plan.split !== void 0 ? {
1725
+ split: {
1726
+ tokenId: spend.plan.split.tokenId,
1727
+ splitAmount: spend.plan.split.splitAmount.toString(),
1728
+ remainderAmount: spend.plan.split.remainderAmount.toString()
1729
+ }
1730
+ } : {},
1731
+ spentStates
1732
+ };
1733
+ }
1734
+ function messageOf(err) {
1735
+ return err instanceof Error ? err.message : String(err);
1736
+ }
1737
+ function buildWholePayload(recipientPubkey, request, spentStates) {
1738
+ return {
1739
+ v: 2,
1740
+ kind: "whole",
1741
+ recipient: recipientPubkey,
1742
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
1743
+ direct: [request.tokenId],
1744
+ spentStates
1745
+ };
1746
+ }
1747
+
1748
+ // modules/payments-v2/mint-params.ts
1749
+ function mintParams(recipientPubkey, coinId, amount) {
1750
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1751
+ }
1752
+
1753
+ // modules/payments-v2/mint.ts
1754
+ function runMintUnderJournal(deps, input) {
1755
+ return mintUnderJournal(deps, input);
1756
+ }
1757
+ async function mintUnderJournal(deps, input) {
1758
+ const { mintId, coinId, amount } = input;
1759
+ const engine = deps.engine;
1760
+ const entry = {
1761
+ mintId,
1762
+ coinId,
1763
+ amount: amount.toString(),
1764
+ tokenId: "",
1765
+ createdAt: deps.now()
1766
+ };
1767
+ await deps.mintJournal.upsert(entry);
1768
+ let token;
1769
+ try {
1770
+ token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
1771
+ } catch (err) {
1772
+ deps.armHeartbeat();
1773
+ return { success: false, error: messageOf(err) };
1774
+ }
1775
+ if (token.blob.tokenId !== entry.tokenId) {
1776
+ await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
1777
+ }
1778
+ try {
1779
+ await finalizeMint(deps, mintId, token, coinId, amount.toString());
1780
+ } catch (err) {
1781
+ deps.armHeartbeat();
1782
+ return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
1783
+ }
1784
+ return { success: true, tokenId: token.blob.tokenId };
1785
+ }
1786
+ async function finalizeMint(deps, mintId, token, coinId, amount) {
1787
+ const engine = deps.engine;
1788
+ const bytes = token.blob.token;
1789
+ const digest = bytesToHex(sha256(bytes));
1790
+ const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
1791
+ const key = keys.get(digest);
1792
+ if (key === void 0) {
1793
+ throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
1794
+ }
1795
+ await deps.storagePort.applyDelta({
1796
+ transferId: mintId,
1797
+ spent: [],
1798
+ added: [{ tokenId: token.blob.tokenId, key }]
1799
+ });
1800
+ await deps.recordMint({
1801
+ tokenId: token.blob.tokenId,
1802
+ assets: [{ coinId, amount }]
1803
+ });
1804
+ await deps.mintJournal.removeByKey(mintId);
1805
+ deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
1806
+ deps.refreshView();
1807
+ }
1808
+
1809
+ // modules/payments-v2/inventory/presentation.ts
1810
+ function toToken(tokenId, snapshot, asset, registry, flags) {
1811
+ const iconUrl = registry.getIconUrl(asset.coinId);
1812
+ return {
1813
+ id: tokenId,
1814
+ coinId: asset.coinId,
1815
+ symbol: registry.getSymbol(asset.coinId),
1816
+ name: registry.getName(asset.coinId),
1817
+ decimals: registry.getDecimals(asset.coinId),
1818
+ ...iconUrl !== null ? { iconUrl } : {},
1819
+ amount: asset.amount,
1820
+ status: flags.transferring ? "transferring" : "confirmed",
1821
+ createdAt: snapshot.createdAt,
1822
+ updatedAt: snapshot.updatedAt,
1823
+ lazy: true,
1824
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {}
1825
+ };
1826
+ }
1827
+ function aggregateAssets(entries, isTransferring, registry) {
1828
+ const groups = /* @__PURE__ */ new Map();
1829
+ for (const [tokenId, entry] of entries) {
1830
+ const moving = isTransferring(tokenId);
1831
+ for (const asset of entry.assets) {
1832
+ let group = groups.get(asset.coinId);
1833
+ if (!group) {
1834
+ group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
1835
+ groups.set(asset.coinId, group);
1836
+ }
1837
+ if (moving) {
1838
+ group.transferring += BigInt(asset.amount);
1839
+ group.transferringCount += 1;
1840
+ } else {
1841
+ group.total += BigInt(asset.amount);
1842
+ group.count += 1;
1843
+ }
1844
+ }
1845
+ }
1846
+ return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
1847
+ }
1848
+ function toAsset(coinId, totals, registry) {
1849
+ const iconUrl = registry.getIconUrl(coinId);
1850
+ return {
1851
+ coinId,
1852
+ symbol: registry.getSymbol(coinId),
1853
+ name: registry.getName(coinId),
1854
+ decimals: registry.getDecimals(coinId),
1855
+ ...iconUrl !== null ? { iconUrl } : {},
1856
+ totalAmount: totals.total.toString(),
1857
+ tokenCount: totals.count,
1858
+ confirmedAmount: totals.total.toString(),
1859
+ unconfirmedAmount: "0",
1860
+ confirmedTokenCount: totals.count,
1861
+ unconfirmedTokenCount: 0,
1862
+ transferringTokenCount: totals.transferringCount,
1863
+ transferringAmount: totals.transferring.toString(),
1864
+ priceUsd: null,
1865
+ priceEur: null,
1866
+ change24h: null,
1867
+ fiatValueUsd: null,
1868
+ fiatValueEur: null
1869
+ };
1870
+ }
1871
+ function priceIdOf(registry, coinId) {
1872
+ return registry.getName(coinId).toLowerCase();
1873
+ }
1874
+ async function withPrices(raw, registry, price) {
1875
+ try {
1876
+ const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
1877
+ const quotes = await price.getPrices(ids);
1878
+ return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
1879
+ } catch {
1880
+ return raw;
1881
+ }
1593
1882
  }
1594
- function journalOnlyRow(transferId, entries) {
1595
- const certified = distinctOpCount(entries);
1883
+ function priceOne(asset, quote) {
1884
+ if (!quote) return asset;
1885
+ const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
1596
1886
  return {
1597
- transferId,
1598
- kind: "open",
1599
- recipient: entries[0].recipientPubkey,
1600
- coinId: "",
1601
- amount: "",
1602
- legs: { certified, total: certified },
1603
- deliveryPending: true,
1604
- createdAt: 0
1887
+ ...asset,
1888
+ priceUsd: quote.priceUsd,
1889
+ priceEur: quote.priceEur ?? null,
1890
+ change24h: quote.change24h ?? null,
1891
+ fiatValueUsd: human * quote.priceUsd,
1892
+ fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
1605
1893
  };
1606
1894
  }
1607
- function shortfallRow(entry) {
1608
- return {
1609
- transferId: entry.transferId,
1610
- kind: "shortfall",
1611
- recipient: entry.recipient,
1612
- coinId: entry.coinId,
1613
- amount: entry.remainingAmount,
1614
- legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
1615
- deliveryPending: false,
1616
- createdAt: entry.createdAt
1895
+ function transferringToken(tokenId, coinId, amount, registry, now) {
1896
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
1897
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
1898
+ transferring: true,
1899
+ suspectedSpent: false
1900
+ });
1901
+ }
1902
+
1903
+ // modules/payments-v2/send-whole.ts
1904
+ async function materializeWholeSpend(deps, input) {
1905
+ const { engine } = deps;
1906
+ const tokenId = input.request.tokenId;
1907
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
1908
+ if (bytes === void 0) {
1909
+ throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
1910
+ }
1911
+ const token = await engine.decodeToken({ tokenId, token: bytes });
1912
+ if (token.valueEnvelope === "bare_collection") {
1913
+ throw new SphereError(
1914
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
1915
+ "VALIDATION_ERROR"
1916
+ );
1917
+ }
1918
+ if (input.requireCoinless && token.value !== null) {
1919
+ throw new SphereError(
1920
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
1921
+ "VALIDATION_ERROR"
1922
+ );
1923
+ }
1924
+ const keys = await engine.deliveryKeys(bytes);
1925
+ const payload = buildWholePayload(input.recipientPubkey, input.request, {
1926
+ [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
1927
+ });
1928
+ const plan = {
1929
+ transferId: input.transferId,
1930
+ recipientPubkey: input.recipientPubkey,
1931
+ payload,
1932
+ ops: buildOps(payload),
1933
+ sources: /* @__PURE__ */ new Map([[tokenId, token]]),
1934
+ ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
1617
1935
  };
1936
+ const sourceTokens = (token.value?.assets ?? []).map(
1937
+ (asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
1938
+ );
1939
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
1618
1940
  }
1619
- function distinctOpCount(entries) {
1620
- return new Set(entries.map((entry) => entry.opIndex)).size;
1941
+
1942
+ // modules/payments-v2/send-errors.ts
1943
+ function partialize(err, run) {
1944
+ if (run.delivered.length === 0) return err;
1945
+ return new PartialSendConflictError(
1946
+ "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.",
1947
+ run.firstPartialId ?? "",
1948
+ [...run.delivered],
1949
+ run.amount,
1950
+ err
1951
+ );
1952
+ }
1953
+ function stampTransferId(err, transferId) {
1954
+ if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
1955
+ err.transferId = transferId;
1956
+ }
1621
1957
  }
1622
1958
 
1623
1959
  // modules/payments-v2/recipient.ts
@@ -1668,11 +2004,6 @@ async function splitOpIndex(deps, envelope) {
1668
2004
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
1669
2005
  }
1670
2006
 
1671
- // modules/payments-v2/mint-params.ts
1672
- function mintParams(recipientPubkey, coinId, amount) {
1673
- return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1674
- }
1675
-
1676
2007
  // modules/payments-v2/prewarm-cache.ts
1677
2008
  var PrewarmCache = class _PrewarmCache {
1678
2009
  entries = /* @__PURE__ */ new Map();
@@ -1752,123 +2083,6 @@ async function takeSourceBlobs(deps, sourceIds) {
1752
2083
  return blobs;
1753
2084
  }
1754
2085
 
1755
- // modules/payments-v2/inventory/presentation.ts
1756
- function toToken(tokenId, snapshot, asset, registry, flags) {
1757
- const iconUrl = registry.getIconUrl(asset.coinId);
1758
- return {
1759
- id: tokenId,
1760
- coinId: asset.coinId,
1761
- symbol: registry.getSymbol(asset.coinId),
1762
- name: registry.getName(asset.coinId),
1763
- decimals: registry.getDecimals(asset.coinId),
1764
- ...iconUrl !== null ? { iconUrl } : {},
1765
- amount: asset.amount,
1766
- status: flags.transferring ? "transferring" : "confirmed",
1767
- createdAt: snapshot.createdAt,
1768
- updatedAt: snapshot.updatedAt,
1769
- lazy: true,
1770
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
1771
- };
1772
- }
1773
- function aggregateAssets(entries, isTransferring, registry) {
1774
- const groups = /* @__PURE__ */ new Map();
1775
- for (const [tokenId, entry] of entries) {
1776
- const moving = isTransferring(tokenId);
1777
- for (const asset of entry.assets) {
1778
- let group = groups.get(asset.coinId);
1779
- if (!group) {
1780
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
1781
- groups.set(asset.coinId, group);
1782
- }
1783
- if (moving) {
1784
- group.transferring += BigInt(asset.amount);
1785
- group.transferringCount += 1;
1786
- } else {
1787
- group.total += BigInt(asset.amount);
1788
- group.count += 1;
1789
- }
1790
- }
1791
- }
1792
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
1793
- }
1794
- function toAsset(coinId, totals, registry) {
1795
- const iconUrl = registry.getIconUrl(coinId);
1796
- return {
1797
- coinId,
1798
- symbol: registry.getSymbol(coinId),
1799
- name: registry.getName(coinId),
1800
- decimals: registry.getDecimals(coinId),
1801
- ...iconUrl !== null ? { iconUrl } : {},
1802
- totalAmount: totals.total.toString(),
1803
- tokenCount: totals.count,
1804
- confirmedAmount: totals.total.toString(),
1805
- unconfirmedAmount: "0",
1806
- confirmedTokenCount: totals.count,
1807
- unconfirmedTokenCount: 0,
1808
- transferringTokenCount: totals.transferringCount,
1809
- transferringAmount: totals.transferring.toString(),
1810
- priceUsd: null,
1811
- priceEur: null,
1812
- change24h: null,
1813
- fiatValueUsd: null,
1814
- fiatValueEur: null
1815
- };
1816
- }
1817
- function priceIdOf(registry, coinId) {
1818
- return registry.getName(coinId).toLowerCase();
1819
- }
1820
- async function withPrices(raw, registry, price) {
1821
- try {
1822
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
1823
- const quotes = await price.getPrices(ids);
1824
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
1825
- } catch {
1826
- return raw;
1827
- }
1828
- }
1829
- function priceOne(asset, quote) {
1830
- if (!quote) return asset;
1831
- const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
1832
- return {
1833
- ...asset,
1834
- priceUsd: quote.priceUsd,
1835
- priceEur: quote.priceEur ?? null,
1836
- change24h: quote.change24h ?? null,
1837
- fiatValueUsd: human * quote.priceUsd,
1838
- fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
1839
- };
1840
- }
1841
- function transferringToken(tokenId, coinId, amount, registry, now) {
1842
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
1843
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
1844
- transferring: true,
1845
- suspectedSpent: false
1846
- });
1847
- }
1848
-
1849
- // modules/payments-v2/machine/payload.ts
1850
- function buildPayload(recipientPubkey, request, spend, spentStates) {
1851
- return {
1852
- v: 2,
1853
- recipient: recipientPubkey,
1854
- coinId: request.coinId,
1855
- amount: request.amount,
1856
- ...request.memo !== void 0 ? { memo: request.memo } : {},
1857
- direct: [...spend.plan.direct],
1858
- ...spend.plan.split !== void 0 ? {
1859
- split: {
1860
- tokenId: spend.plan.split.tokenId,
1861
- splitAmount: spend.plan.split.splitAmount.toString(),
1862
- remainderAmount: spend.plan.split.remainderAmount.toString()
1863
- }
1864
- } : {},
1865
- spentStates
1866
- };
1867
- }
1868
- function messageOf(err) {
1869
- return err instanceof Error ? err.message : String(err);
1870
- }
1871
-
1872
2086
  // node_modules/@noble/ciphers/utils.js
1873
2087
  function isBytes2(a) {
1874
2088
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
@@ -2720,6 +2934,14 @@ function decryptField(key, envelope) {
2720
2934
  }
2721
2935
 
2722
2936
  // modules/payments-v2/history/History.ts
2937
+ function wireAssets(assets) {
2938
+ return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
2939
+ }
2940
+ function isPermanentReject(err) {
2941
+ const status = err?.status;
2942
+ if (typeof status !== "number") return false;
2943
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
2944
+ }
2723
2945
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
2724
2946
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
2725
2947
  var History = class {
@@ -2744,7 +2966,7 @@ var History = class {
2744
2966
  dedupKey: input.transferId,
2745
2967
  id: (this.deps.newId ?? randomUUID)(),
2746
2968
  type: "SENT",
2747
- assets: [{ coinId: input.coinId, amount: input.amount }],
2969
+ assets: wireAssets(input.assets),
2748
2970
  ts: this.ts(input.timestamp),
2749
2971
  transferId: input.transferId,
2750
2972
  ...this.wireTokenId(input.tokenId),
@@ -2757,7 +2979,7 @@ var History = class {
2757
2979
  dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
2758
2980
  id: (this.deps.newId ?? randomUUID)(),
2759
2981
  type: "RECEIVED",
2760
- assets: [{ coinId: input.coinId, amount: input.amount }],
2982
+ assets: wireAssets(input.assets),
2761
2983
  ts: this.ts(input.timestamp),
2762
2984
  ...input.transferId !== void 0 ? { transferId: input.transferId } : {},
2763
2985
  ...this.wireTokenId(input.tokenId),
@@ -2770,7 +2992,7 @@ var History = class {
2770
2992
  dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
2771
2993
  id: (this.deps.newId ?? randomUUID)(),
2772
2994
  type: "MINT",
2773
- assets: [{ coinId: input.coinId, amount: input.amount }],
2995
+ assets: wireAssets(input.assets),
2774
2996
  ts: this.ts(input.timestamp),
2775
2997
  ...this.wireTokenId(input.tokenId)
2776
2998
  }));
@@ -2781,7 +3003,10 @@ var History = class {
2781
3003
  try {
2782
3004
  await this.deps.client.postHistory([record]);
2783
3005
  } catch (err) {
2784
- this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
3006
+ this.deps.log?.(
3007
+ isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
3008
+ err
3009
+ );
2785
3010
  return;
2786
3011
  }
2787
3012
  this.deps.emit("history:updated", this.toEntry(record));
@@ -2848,6 +3073,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
2848
3073
  function stateKey(tokenId, stateHash) {
2849
3074
  return `${tokenId}:${stateHash}`;
2850
3075
  }
3076
+ function unchanged(prev, item, tokenType) {
3077
+ return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
3078
+ }
3079
+ function isCoinless(item, prev) {
3080
+ if (item.status !== "active") return prev?.coinless ?? false;
3081
+ return (item.assets?.length ?? 0) === 0;
3082
+ }
2851
3083
  var InventoryView = class {
2852
3084
  constructor(deps) {
2853
3085
  this.deps = deps;
@@ -2941,14 +3173,26 @@ var InventoryView = class {
2941
3173
  pool(coinId) {
2942
3174
  const out = [];
2943
3175
  for (const [tokenId, entry] of this.mirror) {
2944
- if (entry.status !== "active") continue;
2945
- if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
2946
- if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
3176
+ if (!this.isSpendable(tokenId, entry)) continue;
2947
3177
  const asset = entry.assets.find((a) => a.coinId === coinId);
2948
3178
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
2949
3179
  }
2950
3180
  return out;
2951
3181
  }
3182
+ /**
3183
+ * The eligibility gates every spend shares, coin or token — ONE definition, so a
3184
+ * single probe covers both verbs and neither can drift from the other.
3185
+ */
3186
+ isSpendable(tokenId, entry) {
3187
+ if (entry.status !== "active") return false;
3188
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
3189
+ return !this.suspected.has(stateKey(tokenId, entry.stateHash));
3190
+ }
3191
+ /** Is this NAMED token spendable whole — coinless or valued (#777)? */
3192
+ spendableToken(tokenId) {
3193
+ const entry = this.mirror.get(tokenId);
3194
+ return entry !== void 0 && this.isSpendable(tokenId, entry);
3195
+ }
2952
3196
  /**
2953
3197
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
2954
3198
  * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
@@ -2974,6 +3218,29 @@ var InventoryView = class {
2974
3218
  }
2975
3219
  return out;
2976
3220
  }
3221
+ /**
3222
+ * Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
3223
+ * exactly when it is not, so no mirror row can appear in both reads.
3224
+ */
3225
+ coinless(registry) {
3226
+ const out = [];
3227
+ for (const [tokenId, entry] of this.mirror) {
3228
+ if (entry.status !== "active" || !entry.coinless) continue;
3229
+ const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
3230
+ out.push({
3231
+ tokenId,
3232
+ ...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
3233
+ ...meta ? { name: meta.name } : {},
3234
+ ...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
3235
+ stateHash: entry.stateHash,
3236
+ transferring: this.held(tokenId),
3237
+ ...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
3238
+ createdAt: entry.createdAt,
3239
+ updatedAt: entry.updatedAt
3240
+ });
3241
+ }
3242
+ return out;
3243
+ }
2977
3244
  async assets(registry, price) {
2978
3245
  const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
2979
3246
  if (!price || raw.length === 0) return raw;
@@ -3045,14 +3312,19 @@ var InventoryView = class {
3045
3312
  applyOne(item, now) {
3046
3313
  const prev = this.mirror.get(item.tokenId);
3047
3314
  if (prev && item.seq < prev.seq) return false;
3048
- if (prev && prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash) {
3049
- return false;
3050
- }
3315
+ const tokenType = item.tokenType ?? prev?.tokenType;
3316
+ if (prev && unchanged(prev, item, tokenType)) return false;
3051
3317
  this.mirror.set(item.tokenId, {
3052
3318
  stateHash: item.stateHash,
3053
3319
  seq: item.seq,
3054
3320
  status: item.status,
3055
- assets: item.assets ?? prev?.assets ?? [],
3321
+ // ONLY a tombstone inherits: it omits assets for an unrelated reason and
3322
+ // recoverRemoved must know the amount it restores. An ACTIVE row that omits
3323
+ // them is stating coinlessness, and inheriting there would keep stale assets
3324
+ // in tokens() while `coinless` is true — the row in BOTH reads (§16).
3325
+ assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
3326
+ coinless: isCoinless(item, prev),
3327
+ ...tokenType !== void 0 ? { tokenType } : {},
3056
3328
  createdAt: prev?.createdAt ?? now,
3057
3329
  updatedAt: now
3058
3330
  });
@@ -3143,6 +3415,73 @@ var InventoryView = class {
3143
3415
  }
3144
3416
  };
3145
3417
 
3418
+ // token-engine/sdk.ts
3419
+ import { StateTransitionClient } from "@unicitylabs/state-transition-sdk/lib/StateTransitionClient.js";
3420
+ import { AggregatorClient } from "@unicitylabs/state-transition-sdk/lib/api/AggregatorClient.js";
3421
+ import { NetworkId } from "@unicitylabs/state-transition-sdk/lib/api/NetworkId.js";
3422
+ import { CertificationData } from "@unicitylabs/state-transition-sdk/lib/api/CertificationData.js";
3423
+ import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-transition-sdk/lib/api/CertificationResponse.js";
3424
+ import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
3425
+ import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
3426
+ import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
3427
+ import { JsonRpcNetworkError } from "@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js";
3428
+ import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
3429
+ import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
3430
+ import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
3431
+ import { Token } from "@unicitylabs/state-transition-sdk/lib/transaction/Token.js";
3432
+ import { MintTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransaction.js";
3433
+ import { TransferTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction.js";
3434
+ import { CertifiedMintTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/CertifiedMintTransaction.js";
3435
+ import { CertifiedTransferTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/CertifiedTransferTransaction.js";
3436
+ import { TokenId } from "@unicitylabs/state-transition-sdk/lib/transaction/TokenId.js";
3437
+ import { TokenType } from "@unicitylabs/state-transition-sdk/lib/transaction/TokenType.js";
3438
+ import { TokenSalt } from "@unicitylabs/state-transition-sdk/lib/transaction/TokenSalt.js";
3439
+ import { StateMask } from "@unicitylabs/state-transition-sdk/lib/transaction/StateMask.js";
3440
+ import { MintJustificationVerifierService } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/MintJustificationVerifierService.js";
3441
+ import { TokenIssuanceVerifierService } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/TokenIssuanceVerifierService.js";
3442
+ import { VerificationContext } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/VerificationContext.js";
3443
+ import { UnicityCertificateVerifier } from "@unicitylabs/state-transition-sdk/lib/api/bft/verification/UnicityCertificateVerifier.js";
3444
+ import { UnicitySealQuorumSignaturesVerificationRule } from "@unicitylabs/state-transition-sdk/lib/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.js";
3445
+ import { VerifiedSealCache } from "@unicitylabs/state-transition-sdk/lib/api/bft/verification/VerifiedSealCache.js";
3446
+ import { Secp256k1SignatureVerifier } from "@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/Secp256k1SignatureVerifier.js";
3447
+ import { WorkerTokenVerifier } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/worker/WorkerTokenVerifier.js";
3448
+ import { EncodedPredicate } from "@unicitylabs/state-transition-sdk/lib/predicate/EncodedPredicate.js";
3449
+ import { SignaturePredicate } from "@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicate.js";
3450
+ import { SignaturePredicateUnlockScript } from "@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicateUnlockScript.js";
3451
+ import { BurnPredicate } from "@unicitylabs/state-transition-sdk/lib/predicate/builtin/BurnPredicate.js";
3452
+ import { PredicateVerifierService } from "@unicitylabs/state-transition-sdk/lib/predicate/verification/PredicateVerifierService.js";
3453
+ import { SigningService } from "@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/SigningService.js";
3454
+ import { Signature } from "@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/Signature.js";
3455
+ import { MintSigningService } from "@unicitylabs/state-transition-sdk/lib/crypto/MintSigningService.js";
3456
+ import { HashAlgorithm } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/HashAlgorithm.js";
3457
+ import { DataHash } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHash.js";
3458
+ import { DataHasher } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasher.js";
3459
+ import { DataHasherFactory } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasherFactory.js";
3460
+ import { CborSerializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js";
3461
+ import { CborDeserializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js";
3462
+ import { CborError } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js";
3463
+ import { CborReader } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js";
3464
+ import { MajorType } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js";
3465
+ import { Asset } from "@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js";
3466
+ import { AssetId } from "@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js";
3467
+ import { PaymentAssetCollection } from "@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js";
3468
+ import { TokenSplit } from "@unicitylabs/state-transition-sdk/lib/payment/TokenSplit.js";
3469
+ import { SplitTokenRequest } from "@unicitylabs/state-transition-sdk/lib/payment/SplitTokenRequest.js";
3470
+ import { SplitToken } from "@unicitylabs/state-transition-sdk/lib/payment/SplitToken.js";
3471
+ import { SplitAllocationProof } from "@unicitylabs/state-transition-sdk/lib/payment/SplitAllocationProof.js";
3472
+ import { SplitMintJustification } from "@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustification.js";
3473
+ import { SplitMintJustificationVerifier } from "@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustificationVerifier.js";
3474
+ import { VerificationStatus } from "@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js";
3475
+ import { VerificationResult } from "@unicitylabs/state-transition-sdk/lib/verification/VerificationResult.js";
3476
+ import { HexConverter } from "@unicitylabs/state-transition-sdk/lib/util/HexConverter.js";
3477
+ import { BigintConverter } from "@unicitylabs/state-transition-sdk/lib/util/BigintConverter.js";
3478
+ import { BitString } from "@unicitylabs/state-transition-sdk/lib/util/BitString.js";
3479
+
3480
+ // token-engine/value-envelope.ts
3481
+ function isCoinlessEnvelope(envelope) {
3482
+ return envelope.startsWith("none_");
3483
+ }
3484
+
3146
3485
  // modules/payments-v2/receive/Receive.ts
3147
3486
  var ACK_BATCH_SIZE = 200;
3148
3487
  var REFRESH_INTERVAL_MS = 2500;
@@ -3283,7 +3622,12 @@ async function screen(deps, engine, entry) {
3283
3622
  }
3284
3623
  return {
3285
3624
  kind: "accept",
3286
- record: { tokenId: keys.tokenId, stateHash: keys.stateHash, assets: toAssetAmounts(token) }
3625
+ record: {
3626
+ tokenId: keys.tokenId,
3627
+ stateHash: keys.stateHash,
3628
+ assets: toAssetAmounts(token),
3629
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
3630
+ }
3287
3631
  };
3288
3632
  }
3289
3633
  async function announce(deps, entry, record) {
@@ -3294,6 +3638,7 @@ async function announce(deps, entry, record) {
3294
3638
  tokenId: record.tokenId,
3295
3639
  stateHash: record.stateHash,
3296
3640
  assets: record.assets,
3641
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
3297
3642
  ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
3298
3643
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
3299
3644
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
@@ -3307,6 +3652,19 @@ async function announce(deps, entry, record) {
3307
3652
  senderPubkey: entry.senderPubkey ?? "",
3308
3653
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
3309
3654
  tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
3655
+ // #777: named here rather than mapped from assets, which announced an EMPTY list.
3656
+ ...record.tokenType !== void 0 ? {
3657
+ coinless: [
3658
+ {
3659
+ tokenId: record.tokenId,
3660
+ tokenType: record.tokenType,
3661
+ stateHash: record.stateHash,
3662
+ transferring: false,
3663
+ createdAt: receivedAt,
3664
+ updatedAt: receivedAt
3665
+ }
3666
+ ]
3667
+ } : {},
3310
3668
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
3311
3669
  receivedAt
3312
3670
  };
@@ -3972,6 +4330,34 @@ var SpendQueue = class {
3972
4330
  }
3973
4331
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
3974
4332
  }
4333
+ /**
4334
+ * #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
4335
+ * for. A coin plan waits because some OTHER combination may free up; a named
4336
+ * token is either free now or held by a reservation that only its own transfer
4337
+ * can release, so queueing would block until a timeout on a spend that cannot
4338
+ * become possible.
4339
+ */
4340
+ planWhole(reservationId, tokenId) {
4341
+ if (this.destroyed) {
4342
+ throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
4343
+ }
4344
+ const unproven = this.deps.ledger.unprovenReason();
4345
+ if (unproven !== null) {
4346
+ throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
4347
+ }
4348
+ if (this.deps.spendableToken?.(tokenId) !== true) {
4349
+ throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
4350
+ }
4351
+ const holder = this.deps.ledger.holderOf(tokenId);
4352
+ if (holder !== void 0) {
4353
+ throw new SphereError(
4354
+ `Token ${tokenId} is already reserved by transfer ${holder}`,
4355
+ "VALIDATION_ERROR"
4356
+ );
4357
+ }
4358
+ this.deps.ledger.reserve(reservationId, [{ tokenId }]);
4359
+ return { reservationId, plan: { direct: [tokenId] } };
4360
+ }
3975
4361
  /**
3976
4362
  * The sources `plan()` WOULD pick right now, reserving nothing and queueing
3977
4363
  * nothing — a read-only probe used to warm blobs while the user is still
@@ -4161,6 +4547,7 @@ function composeFacadeParts(deps, hooks) {
4161
4547
  const queue = new SpendQueue({
4162
4548
  ledger,
4163
4549
  getPool: (coinId) => view.pool(coinId),
4550
+ spendableToken: (tokenId) => view.spendableToken(tokenId),
4164
4551
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
4165
4552
  });
4166
4553
  const historyStore = new History({
@@ -4241,13 +4628,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
4241
4628
  ...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
4242
4629
  emit: deps.emit,
4243
4630
  now: deps.now ?? Date.now,
4244
- recordHistory: async ({ transferId, payload, committedAmount }) => {
4631
+ recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
4245
4632
  await historyStore.recordSent({
4246
4633
  transferId,
4247
- coinId: payload.coinId,
4248
- // §5.9: the SETTLED amount (machine-computed from the certified
4249
- // recipient blobs), never payload.amount — the plan.
4250
- amount: committedAmount,
4634
+ // §5.9: the SETTLED amount, never payload.amount — the plan. A token
4635
+ // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
4636
+ ...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
4251
4637
  recipientPubkey: payload.recipient,
4252
4638
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
4253
4639
  });
@@ -4282,12 +4668,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
4282
4668
  });
4283
4669
  }
4284
4670
  async function recordReceived(historyStore, record) {
4285
- const first = record.assets[0];
4286
4671
  await historyStore.recordReceived({
4287
4672
  tokenId: record.tokenId,
4288
4673
  stateHash: record.stateHash,
4289
- coinId: first?.coinId ?? "",
4290
- amount: first?.amount ?? "0",
4674
+ assets: record.assets,
4291
4675
  ...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
4292
4676
  ...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
4293
4677
  ...record.memo !== void 0 ? { memo: record.memo } : {},
@@ -4435,6 +4819,13 @@ var PaymentsFacade = class {
4435
4819
  tokens(filter) {
4436
4820
  return this.view.tokens(this.deps.registry, filter);
4437
4821
  }
4822
+ coinless() {
4823
+ return this.view.coinless(this.deps.registry);
4824
+ }
4825
+ tokenData(tokenId) {
4826
+ const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
4827
+ return readTokenData(deps, tokenId);
4828
+ }
4438
4829
  history(page) {
4439
4830
  return this.historyStore.page(page ?? {});
4440
4831
  }
@@ -4457,7 +4848,14 @@ var PaymentsFacade = class {
4457
4848
  return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
4458
4849
  }
4459
4850
  send(request) {
4460
- return this.track(this.sendOutcome(request));
4851
+ return this.track(this.runJob({ kind: "coin", request }, request.amount));
4852
+ }
4853
+ sendWholeToken(request) {
4854
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
4855
+ }
4856
+ /** NFT-scoped twin: refuses a valued source. */
4857
+ sendCoinless(request) {
4858
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
4461
4859
  }
4462
4860
  async receive() {
4463
4861
  const transfers = await this.track(this.receiveLoop.drainOnce());
@@ -4489,12 +4887,25 @@ var PaymentsFacade = class {
4489
4887
  nowMs() {
4490
4888
  return (this.deps.now ?? Date.now)();
4491
4889
  }
4890
+ mintDeps() {
4891
+ return {
4892
+ engine: this.engine(),
4893
+ mintJournal: this.machineStores.mintJournal,
4894
+ storagePort: this.deps.storagePort,
4895
+ recordMint: (input) => this.historyStore.recordMint(input),
4896
+ armHeartbeat: () => this.heartbeat.arm(),
4897
+ noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
4898
+ refreshView: () => this.trackTail(this.view.delta()),
4899
+ ownPubkeyBytes: this.ownPubkeyBytes,
4900
+ now: () => this.nowMs()
4901
+ };
4902
+ }
4492
4903
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
4493
4904
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
4494
4905
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
4495
- async sendOutcome(request) {
4906
+ async runJob(job, amount) {
4496
4907
  const run = {
4497
- amount: request.amount,
4908
+ amount,
4498
4909
  delivered: [],
4499
4910
  sentTokens: [],
4500
4911
  tokenTransfers: [],
@@ -4504,7 +4915,7 @@ var PaymentsFacade = class {
4504
4915
  lastTransferId: ""
4505
4916
  };
4506
4917
  try {
4507
- return await this.sendWithPolicy(request, run);
4918
+ return await this.sendWithPolicy(job, run);
4508
4919
  } catch (err) {
4509
4920
  this.emitCleanFailure(err, run.lastTransferId);
4510
4921
  throw err;
@@ -4524,19 +4935,19 @@ var PaymentsFacade = class {
4524
4935
  };
4525
4936
  this.deps.emit("transfer:updated", failed);
4526
4937
  }
4527
- async sendWithPolicy(request, run) {
4528
- const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
4938
+ async sendWithPolicy(job, run) {
4939
+ const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
4529
4940
  for (let attempt = 0; ; attempt++) {
4530
4941
  let ctx;
4531
4942
  try {
4532
- ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
4943
+ ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
4533
4944
  } catch (err) {
4534
- throw this.partialize(err, run);
4945
+ throw partialize(err, run);
4535
4946
  }
4536
4947
  const disposition = await this.runAttempt(ctx);
4537
- if (disposition.kind === "rethrow") throw this.partialize(disposition.error, run);
4948
+ if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
4538
4949
  if (disposition.kind === "retry-full") {
4539
- if (attempt >= MAX_RESELECT) throw this.partialize(disposition.error, run);
4950
+ if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
4540
4951
  continue;
4541
4952
  }
4542
4953
  if (disposition.kind === "success") {
@@ -4592,7 +5003,7 @@ var PaymentsFacade = class {
4592
5003
  async disposeFailedAttempt(ctx, err) {
4593
5004
  if (isPossiblyCommittedSendOutcome(err)) {
4594
5005
  this.settleKeepOpen(ctx);
4595
- this.stampTransferId(err, ctx.transferId);
5006
+ stampTransferId(err, ctx.transferId);
4596
5007
  return { kind: "rethrow", error: err };
4597
5008
  }
4598
5009
  const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
@@ -4626,7 +5037,7 @@ var PaymentsFacade = class {
4626
5037
  async disposeConvergeFailure(ctx, err) {
4627
5038
  if (isPossiblyCommittedSendOutcome(err)) {
4628
5039
  this.settleKeepOpen(ctx);
4629
- this.stampTransferId(err, ctx.transferId);
5040
+ stampTransferId(err, ctx.transferId);
4630
5041
  return { kind: "rethrow", error: err };
4631
5042
  }
4632
5043
  if (classifyError(err) === "conflict") {
@@ -4637,9 +5048,29 @@ var PaymentsFacade = class {
4637
5048
  }
4638
5049
  return { kind: "rethrow", error: err };
4639
5050
  }
4640
- async planAndMaterialize(recipientPubkey, request, run) {
5051
+ async planAndMaterialize(recipientPubkey, job, run) {
4641
5052
  const transferId = this.newId();
4642
5053
  run.lastTransferId = transferId;
5054
+ if (job.kind === "whole") {
5055
+ const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
5056
+ const sourceIds2 = this.markPlanned(transferId, "", spend2);
5057
+ try {
5058
+ return await materializeWholeSpend(
5059
+ { engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
5060
+ {
5061
+ transferId,
5062
+ recipientPubkey,
5063
+ request: job.request,
5064
+ sourceIds: sourceIds2,
5065
+ requireCoinless: job.requireCoinless
5066
+ }
5067
+ );
5068
+ } catch (err) {
5069
+ this.settleFailure(transferId, "", sourceIds2);
5070
+ throw err;
5071
+ }
5072
+ }
5073
+ const request = { ...job.request, amount: run.amount };
4643
5074
  const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
4644
5075
  const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
4645
5076
  const sourceIds = this.markPlanned(transferId, request.coinId, spend);
@@ -4773,22 +5204,6 @@ var PaymentsFacade = class {
4773
5204
  return result;
4774
5205
  }
4775
5206
  /** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
4776
- partialize(err, run) {
4777
- if (run.delivered.length === 0) return err;
4778
- return new PartialSendConflictError(
4779
- "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.",
4780
- run.firstPartialId ?? "",
4781
- run.delivered,
4782
- run.amount,
4783
- err
4784
- );
4785
- }
4786
- /** #441: possibly-committed errors must carry the transferId for the settling journal. */
4787
- stampTransferId(err, transferId) {
4788
- if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
4789
- err.transferId = transferId;
4790
- }
4791
- }
4792
5207
  async softAbort(transferId) {
4793
5208
  try {
4794
5209
  await this.deps.client.abortIntent(transferId);
@@ -4804,57 +5219,11 @@ var PaymentsFacade = class {
4804
5219
  const mintId = this.newId();
4805
5220
  this.activeMoneyOps.add(mintId);
4806
5221
  try {
4807
- return await this.mintUnderJournal(mintId, coinId, amount);
5222
+ return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
4808
5223
  } finally {
4809
5224
  this.activeMoneyOps.delete(mintId);
4810
5225
  }
4811
5226
  }
4812
- async mintUnderJournal(mintId, coinId, amount) {
4813
- const engine = this.engine();
4814
- const entry = {
4815
- mintId,
4816
- coinId,
4817
- amount: amount.toString(),
4818
- tokenId: "",
4819
- createdAt: this.nowMs()
4820
- };
4821
- await this.machineStores.mintJournal.upsert(entry);
4822
- let token;
4823
- try {
4824
- token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
4825
- } catch (err) {
4826
- this.heartbeat.arm();
4827
- return { success: false, error: messageOf(err) };
4828
- }
4829
- if (token.blob.tokenId !== entry.tokenId) {
4830
- await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
4831
- }
4832
- try {
4833
- await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
4834
- } catch (err) {
4835
- this.heartbeat.arm();
4836
- return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
4837
- }
4838
- return { success: true, tokenId: token.blob.tokenId };
4839
- }
4840
- async finalizeMint(engine, mintId, token, coinId, amount) {
4841
- const bytes = token.blob.token;
4842
- const digest = bytesToHex(sha256(bytes));
4843
- const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
4844
- const key = keys.get(digest);
4845
- if (key === void 0) {
4846
- throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
4847
- }
4848
- await this.deps.storagePort.applyDelta({
4849
- transferId: mintId,
4850
- spent: [],
4851
- added: [{ tokenId: token.blob.tokenId, key }]
4852
- });
4853
- await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
4854
- await this.machineStores.mintJournal.removeByKey(mintId);
4855
- this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
4856
- this.trackTail(this.view.delta());
4857
- }
4858
5227
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
4859
5228
  async replayMints() {
4860
5229
  let resolved = 0;
@@ -4886,7 +5255,7 @@ var PaymentsFacade = class {
4886
5255
  if (token.blob.tokenId !== entry.tokenId) {
4887
5256
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
4888
5257
  }
4889
- await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
5258
+ await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
4890
5259
  return true;
4891
5260
  }
4892
5261
  async tokenInServerInventory(tokenId) {