@unicitylabs/sphere-sdk 0.16.0 → 0.17.0

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 +566 -171
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +67 -0
  10. package/dist/core/index.d.ts +67 -0
  11. package/dist/core/index.js +566 -171
  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 +566 -171
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +61 -1
  38. package/dist/index.d.ts +61 -1
  39. package/dist/index.js +566 -171
  40. package/dist/index.js.map +1 -1
  41. package/dist/modules/payments-v2/index.cjs +609 -269
  42. package/dist/modules/payments-v2/index.cjs.map +1 -1
  43. package/dist/modules/payments-v2/index.d.cts +88 -13
  44. package/dist/modules/payments-v2/index.d.ts +88 -13
  45. package/dist/modules/payments-v2/index.js +609 -269
  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,73 @@ function createMachineStores(kv) {
834
832
  };
835
833
  }
836
834
 
835
+ // token-engine/sdk.ts
836
+ var import_StateTransitionClient = require("@unicitylabs/state-transition-sdk/lib/StateTransitionClient.js");
837
+ var import_AggregatorClient = require("@unicitylabs/state-transition-sdk/lib/api/AggregatorClient.js");
838
+ var import_NetworkId = require("@unicitylabs/state-transition-sdk/lib/api/NetworkId.js");
839
+ var import_CertificationData = require("@unicitylabs/state-transition-sdk/lib/api/CertificationData.js");
840
+ var import_CertificationResponse = require("@unicitylabs/state-transition-sdk/lib/api/CertificationResponse.js");
841
+ var import_StateId = require("@unicitylabs/state-transition-sdk/lib/api/StateId.js");
842
+ var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js");
843
+ var import_InclusionProofResponse = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js");
844
+ var import_JsonRpcNetworkError = require("@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js");
845
+ var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js");
846
+ var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js");
847
+ var import_InclusionProofVerificationRule = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js");
848
+ var import_Token = require("@unicitylabs/state-transition-sdk/lib/transaction/Token.js");
849
+ var import_MintTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransaction.js");
850
+ var import_TransferTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction.js");
851
+ var import_CertifiedMintTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/CertifiedMintTransaction.js");
852
+ var import_CertifiedTransferTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/CertifiedTransferTransaction.js");
853
+ var import_TokenId = require("@unicitylabs/state-transition-sdk/lib/transaction/TokenId.js");
854
+ var import_TokenType = require("@unicitylabs/state-transition-sdk/lib/transaction/TokenType.js");
855
+ var import_TokenSalt = require("@unicitylabs/state-transition-sdk/lib/transaction/TokenSalt.js");
856
+ var import_StateMask = require("@unicitylabs/state-transition-sdk/lib/transaction/StateMask.js");
857
+ var import_MintJustificationVerifierService = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/MintJustificationVerifierService.js");
858
+ var import_TokenIssuanceVerifierService = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/TokenIssuanceVerifierService.js");
859
+ var import_VerificationContext = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/VerificationContext.js");
860
+ var import_UnicityCertificateVerifier = require("@unicitylabs/state-transition-sdk/lib/api/bft/verification/UnicityCertificateVerifier.js");
861
+ var import_UnicitySealQuorumSignaturesVerificationRule = require("@unicitylabs/state-transition-sdk/lib/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.js");
862
+ var import_VerifiedSealCache = require("@unicitylabs/state-transition-sdk/lib/api/bft/verification/VerifiedSealCache.js");
863
+ var import_Secp256k1SignatureVerifier = require("@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/Secp256k1SignatureVerifier.js");
864
+ var import_WorkerTokenVerifier = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/worker/WorkerTokenVerifier.js");
865
+ var import_EncodedPredicate = require("@unicitylabs/state-transition-sdk/lib/predicate/EncodedPredicate.js");
866
+ var import_SignaturePredicate = require("@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicate.js");
867
+ var import_SignaturePredicateUnlockScript = require("@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicateUnlockScript.js");
868
+ var import_BurnPredicate = require("@unicitylabs/state-transition-sdk/lib/predicate/builtin/BurnPredicate.js");
869
+ var import_PredicateVerifierService = require("@unicitylabs/state-transition-sdk/lib/predicate/verification/PredicateVerifierService.js");
870
+ var import_SigningService = require("@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/SigningService.js");
871
+ var import_Signature = require("@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/Signature.js");
872
+ var import_MintSigningService = require("@unicitylabs/state-transition-sdk/lib/crypto/MintSigningService.js");
873
+ var import_HashAlgorithm = require("@unicitylabs/state-transition-sdk/lib/crypto/hash/HashAlgorithm.js");
874
+ var import_DataHash = require("@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHash.js");
875
+ var import_DataHasher = require("@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasher.js");
876
+ var import_DataHasherFactory = require("@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasherFactory.js");
877
+ var import_CborSerializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js");
878
+ var import_CborDeserializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js");
879
+ var import_CborError = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js");
880
+ var import_CborReader = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js");
881
+ var import_MajorType = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js");
882
+ var import_Asset = require("@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js");
883
+ var import_AssetId = require("@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js");
884
+ var import_PaymentAssetCollection = require("@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js");
885
+ var import_TokenSplit = require("@unicitylabs/state-transition-sdk/lib/payment/TokenSplit.js");
886
+ var import_SplitTokenRequest = require("@unicitylabs/state-transition-sdk/lib/payment/SplitTokenRequest.js");
887
+ var import_SplitToken = require("@unicitylabs/state-transition-sdk/lib/payment/SplitToken.js");
888
+ var import_SplitAllocationProof = require("@unicitylabs/state-transition-sdk/lib/payment/SplitAllocationProof.js");
889
+ var import_SplitMintJustification = require("@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustification.js");
890
+ var import_SplitMintJustificationVerifier = require("@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustificationVerifier.js");
891
+ var import_VerificationStatus = require("@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js");
892
+ var import_VerificationResult = require("@unicitylabs/state-transition-sdk/lib/verification/VerificationResult.js");
893
+ var import_HexConverter = require("@unicitylabs/state-transition-sdk/lib/util/HexConverter.js");
894
+ var import_BigintConverter = require("@unicitylabs/state-transition-sdk/lib/util/BigintConverter.js");
895
+ var import_BitString = require("@unicitylabs/state-transition-sdk/lib/util/BitString.js");
896
+
897
+ // token-engine/value-envelope.ts
898
+ function isCoinlessEnvelope(envelope) {
899
+ return envelope.startsWith("none_");
900
+ }
901
+
837
902
  // token-engine/errors.ts
838
903
  var TransferConflictError = class extends SphereError {
839
904
  /**
@@ -873,6 +938,14 @@ var CheckpointTrustbaseMismatchError = class extends SphereError {
873
938
  }
874
939
  };
875
940
 
941
+ // modules/payments-v2/machine/payload-view.ts
942
+ function splitOf(p) {
943
+ return p.kind === "coin" ? p.split : void 0;
944
+ }
945
+ function coinIdOf(p) {
946
+ return p.kind === "coin" ? p.coinId : void 0;
947
+ }
948
+
876
949
  // modules/payments-v2/machine/TransferMachine.ts
877
950
  var CERTIFY_WIDTH = 20;
878
951
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
@@ -1075,13 +1148,14 @@ var TransferMachine = class {
1075
1148
  const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
1076
1149
  return { recipientBlob: finished.blob.token };
1077
1150
  }
1078
- const split = ctx.payload.split;
1151
+ const split = splitOf(ctx.payload);
1152
+ const splitCoinId = coinIdOf(ctx.payload);
1079
1153
  const { outputs } = await engine.split(
1080
1154
  {
1081
1155
  token: source,
1082
1156
  outputs: [
1083
- { recipientPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.splitAmount) },
1084
- { recipientPubkey: this.deps.ownPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.remainderAmount) }
1157
+ { recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
1158
+ { recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
1085
1159
  ]
1086
1160
  },
1087
1161
  opts
@@ -1203,7 +1277,9 @@ var TransferMachine = class {
1203
1277
  const entry = {
1204
1278
  transferId: r.transferId,
1205
1279
  remainingAmount: undelivered.toString(),
1206
- coinId: r.payload.coinId,
1280
+ // '' only on a token-addressed intent, which is one leg: it either lands or
1281
+ // conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
1282
+ coinId: coinIdOf(r.payload) ?? "",
1207
1283
  recipient: r.payload.recipient,
1208
1284
  committedTokenIds: committed.map((o) => o.op.sourceTokenId),
1209
1285
  createdAt: this.deps.now()
@@ -1229,7 +1305,8 @@ var TransferMachine = class {
1229
1305
  }
1230
1306
  async finishBookkeeping(engine, transferId, payload, phase, committed) {
1231
1307
  try {
1232
- const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
1308
+ const coinId = coinIdOf(payload);
1309
+ const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
1233
1310
  await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
1234
1311
  } catch {
1235
1312
  }
@@ -1272,8 +1349,11 @@ var TransferMachine = class {
1272
1349
  }
1273
1350
  };
1274
1351
  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;
1352
+ const split = splitOf(payload);
1353
+ if (op.kind === "split") return BigInt(split.splitAmount);
1354
+ const coinId = coinIdOf(payload);
1355
+ if (coinId === void 0) return 0n;
1356
+ return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
1277
1357
  }
1278
1358
 
1279
1359
  // modules/payments-v2/machine/resume.ts
@@ -1380,6 +1460,14 @@ async function runOne(ctx, job, report) {
1380
1460
  report.failed.push(job.transferId);
1381
1461
  return;
1382
1462
  }
1463
+ if (job.payload.kind === "coinless" && !isCoinlessEnvelope(token.valueEnvelope)) {
1464
+ logger.warn(
1465
+ "PaymentsV2",
1466
+ `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
1467
+ );
1468
+ report.failed.push(job.transferId);
1469
+ return;
1470
+ }
1383
1471
  mine.set(op.sourceTokenId, token);
1384
1472
  }
1385
1473
  try {
@@ -1410,6 +1498,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
1410
1498
  }
1411
1499
  report.failed.push(transferId);
1412
1500
  }
1501
+ function validateCoinPayload(c) {
1502
+ if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
1503
+ throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
1504
+ }
1505
+ const s = c.split;
1506
+ if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
1507
+ throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
1508
+ }
1509
+ return { ...c, kind: "coin" };
1510
+ }
1511
+ function validateCoinlessPayload(p) {
1512
+ if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
1513
+ throw new SphereError(
1514
+ "token intent payload must name exactly one source token",
1515
+ "VALIDATION_ERROR"
1516
+ );
1517
+ }
1518
+ if (p.split !== void 0) {
1519
+ throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
1520
+ }
1521
+ return { ...p, kind: "coinless", direct: [p.direct[0]] };
1522
+ }
1413
1523
  function validatePayload(raw) {
1414
1524
  const p = raw !== null && typeof raw === "object" ? raw : null;
1415
1525
  if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
@@ -1418,14 +1528,17 @@ function validatePayload(raw) {
1418
1528
  "VALIDATION_ERROR"
1419
1529
  );
1420
1530
  }
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");
1531
+ if (typeof p.recipient !== "string") {
1532
+ throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
1423
1533
  }
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");
1534
+ const kind = p.kind ?? "coin";
1535
+ if (kind !== "coin" && kind !== "coinless") {
1536
+ throw new SphereError(
1537
+ `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
1538
+ "VALIDATION_ERROR"
1539
+ );
1427
1540
  }
1428
- return p;
1541
+ return kind === "coinless" ? validateCoinlessPayload(p) : validateCoinPayload(p);
1429
1542
  }
1430
1543
 
1431
1544
  // modules/payments-v2/convergence.ts
@@ -1631,13 +1744,23 @@ async function openRow(entry, journal, decryptPayload) {
1631
1744
  transferId: entry.transferId,
1632
1745
  kind: "open",
1633
1746
  recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
1634
- coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
1635
- amount: typeof payload?.amount === "string" ? payload.amount : "",
1747
+ ...subject(payload),
1636
1748
  legs: { certified, total: Math.max(total, certified) },
1637
1749
  deliveryPending: journal.length > 0,
1638
1750
  createdAt: entry.createdAt
1639
1751
  };
1640
1752
  }
1753
+ function subject(payload) {
1754
+ if (payload?.kind === "coinless") {
1755
+ const tokenId = payload.direct?.[0];
1756
+ return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
1757
+ }
1758
+ const coin = payload;
1759
+ return {
1760
+ coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
1761
+ amount: typeof coin?.amount === "string" ? coin.amount : ""
1762
+ };
1763
+ }
1641
1764
  function journalOnlyRow(transferId, entries) {
1642
1765
  const certified = distinctOpCount(entries);
1643
1766
  return {
@@ -1667,6 +1790,160 @@ function distinctOpCount(entries) {
1667
1790
  return new Set(entries.map((entry) => entry.opIndex)).size;
1668
1791
  }
1669
1792
 
1793
+ // modules/payments-v2/inventory/token-data.ts
1794
+ async function readTokenData(deps, tokenId) {
1795
+ if (deps.view.stateHashOf(tokenId) === void 0) {
1796
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
1797
+ }
1798
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
1799
+ if (bytes === void 0) {
1800
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
1801
+ }
1802
+ return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
1803
+ }
1804
+
1805
+ // modules/payments-v2/machine/payload.ts
1806
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
1807
+ return {
1808
+ v: 2,
1809
+ kind: "coin",
1810
+ recipient: recipientPubkey,
1811
+ coinId: request.coinId,
1812
+ amount: request.amount,
1813
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
1814
+ direct: [...spend.plan.direct],
1815
+ ...spend.plan.split !== void 0 ? {
1816
+ split: {
1817
+ tokenId: spend.plan.split.tokenId,
1818
+ splitAmount: spend.plan.split.splitAmount.toString(),
1819
+ remainderAmount: spend.plan.split.remainderAmount.toString()
1820
+ }
1821
+ } : {},
1822
+ spentStates
1823
+ };
1824
+ }
1825
+ function messageOf(err) {
1826
+ return err instanceof Error ? err.message : String(err);
1827
+ }
1828
+ function buildCoinlessPayload(recipientPubkey, request, spentStates) {
1829
+ return {
1830
+ v: 2,
1831
+ kind: "coinless",
1832
+ recipient: recipientPubkey,
1833
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
1834
+ direct: [request.tokenId],
1835
+ spentStates
1836
+ };
1837
+ }
1838
+
1839
+ // modules/payments-v2/mint-params.ts
1840
+ function mintParams(recipientPubkey, coinId, amount) {
1841
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1842
+ }
1843
+
1844
+ // modules/payments-v2/mint.ts
1845
+ function runMintUnderJournal(deps, input) {
1846
+ return mintUnderJournal(deps, input);
1847
+ }
1848
+ async function mintUnderJournal(deps, input) {
1849
+ const { mintId, coinId, amount } = input;
1850
+ const engine = deps.engine;
1851
+ const entry = {
1852
+ mintId,
1853
+ coinId,
1854
+ amount: amount.toString(),
1855
+ tokenId: "",
1856
+ createdAt: deps.now()
1857
+ };
1858
+ await deps.mintJournal.upsert(entry);
1859
+ let token;
1860
+ try {
1861
+ token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
1862
+ } catch (err) {
1863
+ deps.armHeartbeat();
1864
+ return { success: false, error: messageOf(err) };
1865
+ }
1866
+ if (token.blob.tokenId !== entry.tokenId) {
1867
+ await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
1868
+ }
1869
+ try {
1870
+ await finalizeMint(deps, mintId, token, coinId, amount.toString());
1871
+ } catch (err) {
1872
+ deps.armHeartbeat();
1873
+ return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
1874
+ }
1875
+ return { success: true, tokenId: token.blob.tokenId };
1876
+ }
1877
+ async function finalizeMint(deps, mintId, token, coinId, amount) {
1878
+ const engine = deps.engine;
1879
+ const bytes = token.blob.token;
1880
+ const digest = bytesToHex(sha256(bytes));
1881
+ const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
1882
+ const key = keys.get(digest);
1883
+ if (key === void 0) {
1884
+ throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
1885
+ }
1886
+ await deps.storagePort.applyDelta({
1887
+ transferId: mintId,
1888
+ spent: [],
1889
+ added: [{ tokenId: token.blob.tokenId, key }]
1890
+ });
1891
+ await deps.recordMint({
1892
+ tokenId: token.blob.tokenId,
1893
+ assets: [{ coinId, amount }]
1894
+ });
1895
+ await deps.mintJournal.removeByKey(mintId);
1896
+ deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
1897
+ deps.refreshView();
1898
+ }
1899
+
1900
+ // modules/payments-v2/send-coinless.ts
1901
+ async function materializeCoinlessSpend(deps, input) {
1902
+ const { engine } = deps;
1903
+ const tokenId = input.request.tokenId;
1904
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
1905
+ if (bytes === void 0) {
1906
+ throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
1907
+ }
1908
+ const token = await engine.decodeToken({ tokenId, token: bytes });
1909
+ if (!isCoinlessEnvelope(token.valueEnvelope)) {
1910
+ throw new SphereError(
1911
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use send()`,
1912
+ "VALIDATION_ERROR"
1913
+ );
1914
+ }
1915
+ const keys = await engine.deliveryKeys(bytes);
1916
+ const payload = buildCoinlessPayload(input.recipientPubkey, input.request, {
1917
+ [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
1918
+ });
1919
+ const plan = {
1920
+ transferId: input.transferId,
1921
+ recipientPubkey: input.recipientPubkey,
1922
+ payload,
1923
+ ops: buildOps(payload),
1924
+ sources: /* @__PURE__ */ new Map([[tokenId, token]]),
1925
+ ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
1926
+ };
1927
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens: [] };
1928
+ }
1929
+
1930
+ // modules/payments-v2/send-errors.ts
1931
+ function partialize(err, run) {
1932
+ if (run.delivered.length === 0) return err;
1933
+ return new PartialSendConflictError(
1934
+ "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.",
1935
+ run.firstPartialId ?? "",
1936
+ [...run.delivered],
1937
+ run.amount,
1938
+ err
1939
+ );
1940
+ }
1941
+ function stampTransferId(err, transferId) {
1942
+ if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
1943
+ err.transferId = transferId;
1944
+ }
1945
+ }
1946
+
1670
1947
  // modules/payments-v2/recipient.ts
1671
1948
  var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
1672
1949
  async function requireSameNetworkRecipient(deps, identifier) {
@@ -1715,11 +1992,6 @@ async function splitOpIndex(deps, envelope) {
1715
1992
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
1716
1993
  }
1717
1994
 
1718
- // modules/payments-v2/mint-params.ts
1719
- function mintParams(recipientPubkey, coinId, amount) {
1720
- return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1721
- }
1722
-
1723
1995
  // modules/payments-v2/prewarm-cache.ts
1724
1996
  var PrewarmCache = class _PrewarmCache {
1725
1997
  entries = /* @__PURE__ */ new Map();
@@ -1893,29 +2165,6 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
1893
2165
  });
1894
2166
  }
1895
2167
 
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
2168
  // node_modules/@noble/ciphers/utils.js
1920
2169
  function isBytes2(a) {
1921
2170
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
@@ -2767,6 +3016,14 @@ function decryptField(key, envelope) {
2767
3016
  }
2768
3017
 
2769
3018
  // modules/payments-v2/history/History.ts
3019
+ function wireAssets(assets) {
3020
+ return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
3021
+ }
3022
+ function isPermanentReject(err) {
3023
+ const status = err?.status;
3024
+ if (typeof status !== "number") return false;
3025
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
3026
+ }
2770
3027
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
2771
3028
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
2772
3029
  var History = class {
@@ -2791,7 +3048,7 @@ var History = class {
2791
3048
  dedupKey: input.transferId,
2792
3049
  id: (this.deps.newId ?? randomUUID)(),
2793
3050
  type: "SENT",
2794
- assets: [{ coinId: input.coinId, amount: input.amount }],
3051
+ assets: wireAssets(input.assets),
2795
3052
  ts: this.ts(input.timestamp),
2796
3053
  transferId: input.transferId,
2797
3054
  ...this.wireTokenId(input.tokenId),
@@ -2804,7 +3061,7 @@ var History = class {
2804
3061
  dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
2805
3062
  id: (this.deps.newId ?? randomUUID)(),
2806
3063
  type: "RECEIVED",
2807
- assets: [{ coinId: input.coinId, amount: input.amount }],
3064
+ assets: wireAssets(input.assets),
2808
3065
  ts: this.ts(input.timestamp),
2809
3066
  ...input.transferId !== void 0 ? { transferId: input.transferId } : {},
2810
3067
  ...this.wireTokenId(input.tokenId),
@@ -2817,7 +3074,7 @@ var History = class {
2817
3074
  dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
2818
3075
  id: (this.deps.newId ?? randomUUID)(),
2819
3076
  type: "MINT",
2820
- assets: [{ coinId: input.coinId, amount: input.amount }],
3077
+ assets: wireAssets(input.assets),
2821
3078
  ts: this.ts(input.timestamp),
2822
3079
  ...this.wireTokenId(input.tokenId)
2823
3080
  }));
@@ -2828,7 +3085,10 @@ var History = class {
2828
3085
  try {
2829
3086
  await this.deps.client.postHistory([record]);
2830
3087
  } catch (err) {
2831
- this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
3088
+ this.deps.log?.(
3089
+ isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
3090
+ err
3091
+ );
2832
3092
  return;
2833
3093
  }
2834
3094
  this.deps.emit("history:updated", this.toEntry(record));
@@ -2895,6 +3155,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
2895
3155
  function stateKey(tokenId, stateHash) {
2896
3156
  return `${tokenId}:${stateHash}`;
2897
3157
  }
3158
+ function unchanged(prev, item, tokenType) {
3159
+ return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
3160
+ }
3161
+ function isCoinless(item, prev) {
3162
+ if (item.status !== "active") return prev?.coinless ?? false;
3163
+ return (item.assets?.length ?? 0) === 0;
3164
+ }
2898
3165
  var InventoryView = class {
2899
3166
  constructor(deps) {
2900
3167
  this.deps = deps;
@@ -2988,14 +3255,26 @@ var InventoryView = class {
2988
3255
  pool(coinId) {
2989
3256
  const out = [];
2990
3257
  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;
3258
+ if (!this.isSpendable(tokenId, entry)) continue;
2994
3259
  const asset = entry.assets.find((a) => a.coinId === coinId);
2995
3260
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
2996
3261
  }
2997
3262
  return out;
2998
3263
  }
3264
+ /**
3265
+ * The eligibility gates every spend shares, coin or token — ONE definition, so a
3266
+ * single probe covers both verbs and neither can drift from the other.
3267
+ */
3268
+ isSpendable(tokenId, entry) {
3269
+ if (entry.status !== "active") return false;
3270
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
3271
+ return !this.suspected.has(stateKey(tokenId, entry.stateHash));
3272
+ }
3273
+ /** #777: is this NAMED token a spendable COINLESS holding? Never a coin source. */
3274
+ spendableCoinless(tokenId) {
3275
+ const entry = this.mirror.get(tokenId);
3276
+ return entry !== void 0 && entry.coinless && this.isSpendable(tokenId, entry);
3277
+ }
2999
3278
  /**
3000
3279
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
3001
3280
  * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
@@ -3021,6 +3300,29 @@ var InventoryView = class {
3021
3300
  }
3022
3301
  return out;
3023
3302
  }
3303
+ /**
3304
+ * Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
3305
+ * exactly when it is not, so no mirror row can appear in both reads.
3306
+ */
3307
+ coinless(registry) {
3308
+ const out = [];
3309
+ for (const [tokenId, entry] of this.mirror) {
3310
+ if (entry.status !== "active" || !entry.coinless) continue;
3311
+ const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
3312
+ out.push({
3313
+ tokenId,
3314
+ ...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
3315
+ ...meta ? { name: meta.name } : {},
3316
+ ...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
3317
+ stateHash: entry.stateHash,
3318
+ transferring: this.held(tokenId),
3319
+ ...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
3320
+ createdAt: entry.createdAt,
3321
+ updatedAt: entry.updatedAt
3322
+ });
3323
+ }
3324
+ return out;
3325
+ }
3024
3326
  async assets(registry, price) {
3025
3327
  const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
3026
3328
  if (!price || raw.length === 0) return raw;
@@ -3092,14 +3394,19 @@ var InventoryView = class {
3092
3394
  applyOne(item, now) {
3093
3395
  const prev = this.mirror.get(item.tokenId);
3094
3396
  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
- }
3397
+ const tokenType = item.tokenType ?? prev?.tokenType;
3398
+ if (prev && unchanged(prev, item, tokenType)) return false;
3098
3399
  this.mirror.set(item.tokenId, {
3099
3400
  stateHash: item.stateHash,
3100
3401
  seq: item.seq,
3101
3402
  status: item.status,
3102
- assets: item.assets ?? prev?.assets ?? [],
3403
+ // ONLY a tombstone inherits: it omits assets for an unrelated reason and
3404
+ // recoverRemoved must know the amount it restores. An ACTIVE row that omits
3405
+ // them is stating coinlessness, and inheriting there would keep stale assets
3406
+ // in tokens() while `coinless` is true — the row in BOTH reads (§16).
3407
+ assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
3408
+ coinless: isCoinless(item, prev),
3409
+ ...tokenType !== void 0 ? { tokenType } : {},
3103
3410
  createdAt: prev?.createdAt ?? now,
3104
3411
  updatedAt: now
3105
3412
  });
@@ -3330,7 +3637,12 @@ async function screen(deps, engine, entry) {
3330
3637
  }
3331
3638
  return {
3332
3639
  kind: "accept",
3333
- record: { tokenId: keys.tokenId, stateHash: keys.stateHash, assets: toAssetAmounts(token) }
3640
+ record: {
3641
+ tokenId: keys.tokenId,
3642
+ stateHash: keys.stateHash,
3643
+ assets: toAssetAmounts(token),
3644
+ ...isCoinlessEnvelope2(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
3645
+ }
3334
3646
  };
3335
3647
  }
3336
3648
  async function announce(deps, entry, record) {
@@ -3341,6 +3653,7 @@ async function announce(deps, entry, record) {
3341
3653
  tokenId: record.tokenId,
3342
3654
  stateHash: record.stateHash,
3343
3655
  assets: record.assets,
3656
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
3344
3657
  ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
3345
3658
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
3346
3659
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
@@ -3354,6 +3667,19 @@ async function announce(deps, entry, record) {
3354
3667
  senderPubkey: entry.senderPubkey ?? "",
3355
3668
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
3356
3669
  tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
3670
+ // #777: named here rather than mapped from assets, which announced an EMPTY list.
3671
+ ...record.tokenType !== void 0 ? {
3672
+ coinless: [
3673
+ {
3674
+ tokenId: record.tokenId,
3675
+ tokenType: record.tokenType,
3676
+ stateHash: record.stateHash,
3677
+ transferring: false,
3678
+ createdAt: receivedAt,
3679
+ updatedAt: receivedAt
3680
+ }
3681
+ ]
3682
+ } : {},
3357
3683
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
3358
3684
  receivedAt
3359
3685
  };
@@ -3452,6 +3778,9 @@ function claimAck(entry) {
3452
3778
  function rejectAck(entry, reason) {
3453
3779
  return { deliveryId: entry.deliveryId, disposition: "rejected", reason, cursor: entry.cursor };
3454
3780
  }
3781
+ function isCoinlessEnvelope2(envelope) {
3782
+ return envelope.startsWith("none_");
3783
+ }
3455
3784
  function toAssetAmounts(token) {
3456
3785
  return (token.value?.assets ?? []).map((asset) => ({
3457
3786
  coinId: asset.coinId,
@@ -4019,6 +4348,37 @@ var SpendQueue = class {
4019
4348
  }
4020
4349
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
4021
4350
  }
4351
+ /**
4352
+ * #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
4353
+ * for. A coin plan waits because some OTHER combination may free up; a named
4354
+ * token is either free now or held by a reservation that only its own transfer
4355
+ * can release, so queueing would block until a timeout on a spend that cannot
4356
+ * become possible.
4357
+ */
4358
+ planCoinless(reservationId, tokenId) {
4359
+ if (this.destroyed) {
4360
+ throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
4361
+ }
4362
+ const unproven = this.deps.ledger.unprovenReason();
4363
+ if (unproven !== null) {
4364
+ throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
4365
+ }
4366
+ if (this.deps.spendableCoinless?.(tokenId) !== true) {
4367
+ throw new SphereError(
4368
+ `Token ${tokenId} is not a spendable coinless holding`,
4369
+ "VALIDATION_ERROR"
4370
+ );
4371
+ }
4372
+ const holder = this.deps.ledger.holderOf(tokenId);
4373
+ if (holder !== void 0) {
4374
+ throw new SphereError(
4375
+ `Token ${tokenId} is already reserved by transfer ${holder}`,
4376
+ "VALIDATION_ERROR"
4377
+ );
4378
+ }
4379
+ this.deps.ledger.reserve(reservationId, [{ tokenId }]);
4380
+ return { reservationId, plan: { direct: [tokenId] } };
4381
+ }
4022
4382
  /**
4023
4383
  * The sources `plan()` WOULD pick right now, reserving nothing and queueing
4024
4384
  * nothing — a read-only probe used to warm blobs while the user is still
@@ -4208,6 +4568,7 @@ function composeFacadeParts(deps, hooks) {
4208
4568
  const queue = new SpendQueue({
4209
4569
  ledger,
4210
4570
  getPool: (coinId) => view.pool(coinId),
4571
+ spendableCoinless: (tokenId) => view.spendableCoinless(tokenId),
4211
4572
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
4212
4573
  });
4213
4574
  const historyStore = new History({
@@ -4291,10 +4652,9 @@ function buildMachineDeps(deps, hooks, historyStore) {
4291
4652
  recordHistory: async ({ transferId, payload, committedAmount }) => {
4292
4653
  await historyStore.recordSent({
4293
4654
  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,
4655
+ // §5.9: the SETTLED amount, never payload.amount — the plan. A token
4656
+ // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
4657
+ ...payload.kind === "coinless" ? { assets: [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
4298
4658
  recipientPubkey: payload.recipient,
4299
4659
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
4300
4660
  });
@@ -4329,12 +4689,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
4329
4689
  });
4330
4690
  }
4331
4691
  async function recordReceived(historyStore, record) {
4332
- const first = record.assets[0];
4333
4692
  await historyStore.recordReceived({
4334
4693
  tokenId: record.tokenId,
4335
4694
  stateHash: record.stateHash,
4336
- coinId: first?.coinId ?? "",
4337
- amount: first?.amount ?? "0",
4695
+ assets: record.assets,
4338
4696
  ...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
4339
4697
  ...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
4340
4698
  ...record.memo !== void 0 ? { memo: record.memo } : {},
@@ -4482,6 +4840,13 @@ var PaymentsFacade = class {
4482
4840
  tokens(filter) {
4483
4841
  return this.view.tokens(this.deps.registry, filter);
4484
4842
  }
4843
+ coinless() {
4844
+ return this.view.coinless(this.deps.registry);
4845
+ }
4846
+ tokenData(tokenId) {
4847
+ const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
4848
+ return readTokenData(deps, tokenId);
4849
+ }
4485
4850
  history(page) {
4486
4851
  return this.historyStore.page(page ?? {});
4487
4852
  }
@@ -4506,6 +4871,9 @@ var PaymentsFacade = class {
4506
4871
  send(request) {
4507
4872
  return this.track(this.sendOutcome(request));
4508
4873
  }
4874
+ sendCoinless(request) {
4875
+ return this.track(this.sendCoinlessOutcome(request));
4876
+ }
4509
4877
  async receive() {
4510
4878
  const transfers = await this.track(this.receiveLoop.drainOnce());
4511
4879
  return { transfers };
@@ -4536,12 +4904,32 @@ var PaymentsFacade = class {
4536
4904
  nowMs() {
4537
4905
  return (this.deps.now ?? Date.now)();
4538
4906
  }
4907
+ mintDeps() {
4908
+ return {
4909
+ engine: this.engine(),
4910
+ mintJournal: this.machineStores.mintJournal,
4911
+ storagePort: this.deps.storagePort,
4912
+ recordMint: (input) => this.historyStore.recordMint(input),
4913
+ armHeartbeat: () => this.heartbeat.arm(),
4914
+ noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
4915
+ refreshView: () => this.trackTail(this.view.delta()),
4916
+ ownPubkeyBytes: this.ownPubkeyBytes,
4917
+ now: () => this.nowMs()
4918
+ };
4919
+ }
4539
4920
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
4540
4921
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
4541
4922
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
4542
- async sendOutcome(request) {
4923
+ sendOutcome(request) {
4924
+ return this.runJob({ kind: "coin", request }, request.amount);
4925
+ }
4926
+ /** A token spend has no amount; '0' keeps the shortfall arithmetic total-free. */
4927
+ sendCoinlessOutcome(request) {
4928
+ return this.runJob({ kind: "coinless", request }, "0");
4929
+ }
4930
+ async runJob(job, amount) {
4543
4931
  const run = {
4544
- amount: request.amount,
4932
+ amount,
4545
4933
  delivered: [],
4546
4934
  sentTokens: [],
4547
4935
  tokenTransfers: [],
@@ -4551,7 +4939,7 @@ var PaymentsFacade = class {
4551
4939
  lastTransferId: ""
4552
4940
  };
4553
4941
  try {
4554
- return await this.sendWithPolicy(request, run);
4942
+ return await this.sendWithPolicy(job, run);
4555
4943
  } catch (err) {
4556
4944
  this.emitCleanFailure(err, run.lastTransferId);
4557
4945
  throw err;
@@ -4571,19 +4959,19 @@ var PaymentsFacade = class {
4571
4959
  };
4572
4960
  this.deps.emit("transfer:updated", failed);
4573
4961
  }
4574
- async sendWithPolicy(request, run) {
4575
- const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
4962
+ async sendWithPolicy(job, run) {
4963
+ const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
4576
4964
  for (let attempt = 0; ; attempt++) {
4577
4965
  let ctx;
4578
4966
  try {
4579
- ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
4967
+ ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
4580
4968
  } catch (err) {
4581
- throw this.partialize(err, run);
4969
+ throw partialize(err, run);
4582
4970
  }
4583
4971
  const disposition = await this.runAttempt(ctx);
4584
- if (disposition.kind === "rethrow") throw this.partialize(disposition.error, run);
4972
+ if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
4585
4973
  if (disposition.kind === "retry-full") {
4586
- if (attempt >= MAX_RESELECT) throw this.partialize(disposition.error, run);
4974
+ if (job.kind === "coinless" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
4587
4975
  continue;
4588
4976
  }
4589
4977
  if (disposition.kind === "success") {
@@ -4639,7 +5027,7 @@ var PaymentsFacade = class {
4639
5027
  async disposeFailedAttempt(ctx, err) {
4640
5028
  if (isPossiblyCommittedSendOutcome(err)) {
4641
5029
  this.settleKeepOpen(ctx);
4642
- this.stampTransferId(err, ctx.transferId);
5030
+ stampTransferId(err, ctx.transferId);
4643
5031
  return { kind: "rethrow", error: err };
4644
5032
  }
4645
5033
  const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
@@ -4673,7 +5061,7 @@ var PaymentsFacade = class {
4673
5061
  async disposeConvergeFailure(ctx, err) {
4674
5062
  if (isPossiblyCommittedSendOutcome(err)) {
4675
5063
  this.settleKeepOpen(ctx);
4676
- this.stampTransferId(err, ctx.transferId);
5064
+ stampTransferId(err, ctx.transferId);
4677
5065
  return { kind: "rethrow", error: err };
4678
5066
  }
4679
5067
  if (classifyError(err) === "conflict") {
@@ -4684,9 +5072,23 @@ var PaymentsFacade = class {
4684
5072
  }
4685
5073
  return { kind: "rethrow", error: err };
4686
5074
  }
4687
- async planAndMaterialize(recipientPubkey, request, run) {
5075
+ async planAndMaterialize(recipientPubkey, job, run) {
4688
5076
  const transferId = this.newId();
4689
5077
  run.lastTransferId = transferId;
5078
+ if (job.kind === "coinless") {
5079
+ const spend2 = this.queue.planCoinless(transferId, job.request.tokenId);
5080
+ const sourceIds2 = this.markPlanned(transferId, "", spend2);
5081
+ try {
5082
+ return await materializeCoinlessSpend(
5083
+ { engine: this.engine(), storagePort: this.deps.storagePort },
5084
+ { transferId, recipientPubkey, request: job.request, sourceIds: sourceIds2 }
5085
+ );
5086
+ } catch (err) {
5087
+ this.settleFailure(transferId, "", sourceIds2);
5088
+ throw err;
5089
+ }
5090
+ }
5091
+ const request = { ...job.request, amount: run.amount };
4690
5092
  const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
4691
5093
  const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
4692
5094
  const sourceIds = this.markPlanned(transferId, request.coinId, spend);
@@ -4820,22 +5222,6 @@ var PaymentsFacade = class {
4820
5222
  return result;
4821
5223
  }
4822
5224
  /** 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
5225
  async softAbort(transferId) {
4840
5226
  try {
4841
5227
  await this.deps.client.abortIntent(transferId);
@@ -4851,57 +5237,11 @@ var PaymentsFacade = class {
4851
5237
  const mintId = this.newId();
4852
5238
  this.activeMoneyOps.add(mintId);
4853
5239
  try {
4854
- return await this.mintUnderJournal(mintId, coinId, amount);
5240
+ return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
4855
5241
  } finally {
4856
5242
  this.activeMoneyOps.delete(mintId);
4857
5243
  }
4858
5244
  }
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
5245
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
4906
5246
  async replayMints() {
4907
5247
  let resolved = 0;
@@ -4933,7 +5273,7 @@ var PaymentsFacade = class {
4933
5273
  if (token.blob.tokenId !== entry.tokenId) {
4934
5274
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
4935
5275
  }
4936
- await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
5276
+ await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
4937
5277
  return true;
4938
5278
  }
4939
5279
  async tokenInServerInventory(tokenId) {