@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
@@ -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,73 @@ function createMachineStores(kv) {
787
785
  };
788
786
  }
789
787
 
788
+ // token-engine/sdk.ts
789
+ import { StateTransitionClient } from "@unicitylabs/state-transition-sdk/lib/StateTransitionClient.js";
790
+ import { AggregatorClient } from "@unicitylabs/state-transition-sdk/lib/api/AggregatorClient.js";
791
+ import { NetworkId } from "@unicitylabs/state-transition-sdk/lib/api/NetworkId.js";
792
+ import { CertificationData } from "@unicitylabs/state-transition-sdk/lib/api/CertificationData.js";
793
+ import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-transition-sdk/lib/api/CertificationResponse.js";
794
+ import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
795
+ import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
796
+ import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
797
+ import { JsonRpcNetworkError } from "@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js";
798
+ import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
799
+ import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
800
+ import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
801
+ import { Token } from "@unicitylabs/state-transition-sdk/lib/transaction/Token.js";
802
+ import { MintTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransaction.js";
803
+ import { TransferTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction.js";
804
+ import { CertifiedMintTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/CertifiedMintTransaction.js";
805
+ import { CertifiedTransferTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/CertifiedTransferTransaction.js";
806
+ import { TokenId } from "@unicitylabs/state-transition-sdk/lib/transaction/TokenId.js";
807
+ import { TokenType } from "@unicitylabs/state-transition-sdk/lib/transaction/TokenType.js";
808
+ import { TokenSalt } from "@unicitylabs/state-transition-sdk/lib/transaction/TokenSalt.js";
809
+ import { StateMask } from "@unicitylabs/state-transition-sdk/lib/transaction/StateMask.js";
810
+ import { MintJustificationVerifierService } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/MintJustificationVerifierService.js";
811
+ import { TokenIssuanceVerifierService } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/TokenIssuanceVerifierService.js";
812
+ import { VerificationContext } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/VerificationContext.js";
813
+ import { UnicityCertificateVerifier } from "@unicitylabs/state-transition-sdk/lib/api/bft/verification/UnicityCertificateVerifier.js";
814
+ import { UnicitySealQuorumSignaturesVerificationRule } from "@unicitylabs/state-transition-sdk/lib/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.js";
815
+ import { VerifiedSealCache } from "@unicitylabs/state-transition-sdk/lib/api/bft/verification/VerifiedSealCache.js";
816
+ import { Secp256k1SignatureVerifier } from "@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/Secp256k1SignatureVerifier.js";
817
+ import { WorkerTokenVerifier } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/worker/WorkerTokenVerifier.js";
818
+ import { EncodedPredicate } from "@unicitylabs/state-transition-sdk/lib/predicate/EncodedPredicate.js";
819
+ import { SignaturePredicate } from "@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicate.js";
820
+ import { SignaturePredicateUnlockScript } from "@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicateUnlockScript.js";
821
+ import { BurnPredicate } from "@unicitylabs/state-transition-sdk/lib/predicate/builtin/BurnPredicate.js";
822
+ import { PredicateVerifierService } from "@unicitylabs/state-transition-sdk/lib/predicate/verification/PredicateVerifierService.js";
823
+ import { SigningService } from "@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/SigningService.js";
824
+ import { Signature } from "@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/Signature.js";
825
+ import { MintSigningService } from "@unicitylabs/state-transition-sdk/lib/crypto/MintSigningService.js";
826
+ import { HashAlgorithm } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/HashAlgorithm.js";
827
+ import { DataHash } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHash.js";
828
+ import { DataHasher } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasher.js";
829
+ import { DataHasherFactory } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasherFactory.js";
830
+ import { CborSerializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js";
831
+ import { CborDeserializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js";
832
+ import { CborError } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js";
833
+ import { CborReader } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js";
834
+ import { MajorType } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js";
835
+ import { Asset } from "@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js";
836
+ import { AssetId } from "@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js";
837
+ import { PaymentAssetCollection } from "@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js";
838
+ import { TokenSplit } from "@unicitylabs/state-transition-sdk/lib/payment/TokenSplit.js";
839
+ import { SplitTokenRequest } from "@unicitylabs/state-transition-sdk/lib/payment/SplitTokenRequest.js";
840
+ import { SplitToken } from "@unicitylabs/state-transition-sdk/lib/payment/SplitToken.js";
841
+ import { SplitAllocationProof } from "@unicitylabs/state-transition-sdk/lib/payment/SplitAllocationProof.js";
842
+ import { SplitMintJustification } from "@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustification.js";
843
+ import { SplitMintJustificationVerifier } from "@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustificationVerifier.js";
844
+ import { VerificationStatus } from "@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js";
845
+ import { VerificationResult } from "@unicitylabs/state-transition-sdk/lib/verification/VerificationResult.js";
846
+ import { HexConverter } from "@unicitylabs/state-transition-sdk/lib/util/HexConverter.js";
847
+ import { BigintConverter } from "@unicitylabs/state-transition-sdk/lib/util/BigintConverter.js";
848
+ import { BitString } from "@unicitylabs/state-transition-sdk/lib/util/BitString.js";
849
+
850
+ // token-engine/value-envelope.ts
851
+ function isCoinlessEnvelope(envelope) {
852
+ return envelope.startsWith("none_");
853
+ }
854
+
790
855
  // token-engine/errors.ts
791
856
  var TransferConflictError = class extends SphereError {
792
857
  /**
@@ -826,6 +891,14 @@ var CheckpointTrustbaseMismatchError = class extends SphereError {
826
891
  }
827
892
  };
828
893
 
894
+ // modules/payments-v2/machine/payload-view.ts
895
+ function splitOf(p) {
896
+ return p.kind === "coin" ? p.split : void 0;
897
+ }
898
+ function coinIdOf(p) {
899
+ return p.kind === "coin" ? p.coinId : void 0;
900
+ }
901
+
829
902
  // modules/payments-v2/machine/TransferMachine.ts
830
903
  var CERTIFY_WIDTH = 20;
831
904
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
@@ -1028,13 +1101,14 @@ var TransferMachine = class {
1028
1101
  const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
1029
1102
  return { recipientBlob: finished.blob.token };
1030
1103
  }
1031
- const split = ctx.payload.split;
1104
+ const split = splitOf(ctx.payload);
1105
+ const splitCoinId = coinIdOf(ctx.payload);
1032
1106
  const { outputs } = await engine.split(
1033
1107
  {
1034
1108
  token: source,
1035
1109
  outputs: [
1036
- { recipientPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.splitAmount) },
1037
- { recipientPubkey: this.deps.ownPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.remainderAmount) }
1110
+ { recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
1111
+ { recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
1038
1112
  ]
1039
1113
  },
1040
1114
  opts
@@ -1156,7 +1230,9 @@ var TransferMachine = class {
1156
1230
  const entry = {
1157
1231
  transferId: r.transferId,
1158
1232
  remainingAmount: undelivered.toString(),
1159
- coinId: r.payload.coinId,
1233
+ // '' only on a token-addressed intent, which is one leg: it either lands or
1234
+ // conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
1235
+ coinId: coinIdOf(r.payload) ?? "",
1160
1236
  recipient: r.payload.recipient,
1161
1237
  committedTokenIds: committed.map((o) => o.op.sourceTokenId),
1162
1238
  createdAt: this.deps.now()
@@ -1182,7 +1258,8 @@ var TransferMachine = class {
1182
1258
  }
1183
1259
  async finishBookkeeping(engine, transferId, payload, phase, committed) {
1184
1260
  try {
1185
- const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
1261
+ const coinId = coinIdOf(payload);
1262
+ const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
1186
1263
  await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
1187
1264
  } catch {
1188
1265
  }
@@ -1225,8 +1302,11 @@ var TransferMachine = class {
1225
1302
  }
1226
1303
  };
1227
1304
  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;
1305
+ const split = splitOf(payload);
1306
+ if (op.kind === "split") return BigInt(split.splitAmount);
1307
+ const coinId = coinIdOf(payload);
1308
+ if (coinId === void 0) return 0n;
1309
+ return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
1230
1310
  }
1231
1311
 
1232
1312
  // modules/payments-v2/machine/resume.ts
@@ -1333,6 +1413,14 @@ async function runOne(ctx, job, report) {
1333
1413
  report.failed.push(job.transferId);
1334
1414
  return;
1335
1415
  }
1416
+ if (job.payload.kind === "coinless" && !isCoinlessEnvelope(token.valueEnvelope)) {
1417
+ logger.warn(
1418
+ "PaymentsV2",
1419
+ `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
1420
+ );
1421
+ report.failed.push(job.transferId);
1422
+ return;
1423
+ }
1336
1424
  mine.set(op.sourceTokenId, token);
1337
1425
  }
1338
1426
  try {
@@ -1363,6 +1451,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
1363
1451
  }
1364
1452
  report.failed.push(transferId);
1365
1453
  }
1454
+ function validateCoinPayload(c) {
1455
+ if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
1456
+ throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
1457
+ }
1458
+ const s = c.split;
1459
+ if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
1460
+ throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
1461
+ }
1462
+ return { ...c, kind: "coin" };
1463
+ }
1464
+ function validateCoinlessPayload(p) {
1465
+ if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
1466
+ throw new SphereError(
1467
+ "token intent payload must name exactly one source token",
1468
+ "VALIDATION_ERROR"
1469
+ );
1470
+ }
1471
+ if (p.split !== void 0) {
1472
+ throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
1473
+ }
1474
+ return { ...p, kind: "coinless", direct: [p.direct[0]] };
1475
+ }
1366
1476
  function validatePayload(raw) {
1367
1477
  const p = raw !== null && typeof raw === "object" ? raw : null;
1368
1478
  if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
@@ -1371,14 +1481,17 @@ function validatePayload(raw) {
1371
1481
  "VALIDATION_ERROR"
1372
1482
  );
1373
1483
  }
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");
1484
+ if (typeof p.recipient !== "string") {
1485
+ throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
1376
1486
  }
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");
1487
+ const kind = p.kind ?? "coin";
1488
+ if (kind !== "coin" && kind !== "coinless") {
1489
+ throw new SphereError(
1490
+ `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
1491
+ "VALIDATION_ERROR"
1492
+ );
1380
1493
  }
1381
- return p;
1494
+ return kind === "coinless" ? validateCoinlessPayload(p) : validateCoinPayload(p);
1382
1495
  }
1383
1496
 
1384
1497
  // modules/payments-v2/convergence.ts
@@ -1584,13 +1697,23 @@ async function openRow(entry, journal, decryptPayload) {
1584
1697
  transferId: entry.transferId,
1585
1698
  kind: "open",
1586
1699
  recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
1587
- coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
1588
- amount: typeof payload?.amount === "string" ? payload.amount : "",
1700
+ ...subject(payload),
1589
1701
  legs: { certified, total: Math.max(total, certified) },
1590
1702
  deliveryPending: journal.length > 0,
1591
1703
  createdAt: entry.createdAt
1592
1704
  };
1593
1705
  }
1706
+ function subject(payload) {
1707
+ if (payload?.kind === "coinless") {
1708
+ const tokenId = payload.direct?.[0];
1709
+ return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
1710
+ }
1711
+ const coin = payload;
1712
+ return {
1713
+ coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
1714
+ amount: typeof coin?.amount === "string" ? coin.amount : ""
1715
+ };
1716
+ }
1594
1717
  function journalOnlyRow(transferId, entries) {
1595
1718
  const certified = distinctOpCount(entries);
1596
1719
  return {
@@ -1620,6 +1743,160 @@ function distinctOpCount(entries) {
1620
1743
  return new Set(entries.map((entry) => entry.opIndex)).size;
1621
1744
  }
1622
1745
 
1746
+ // modules/payments-v2/inventory/token-data.ts
1747
+ async function readTokenData(deps, tokenId) {
1748
+ if (deps.view.stateHashOf(tokenId) === void 0) {
1749
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
1750
+ }
1751
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
1752
+ if (bytes === void 0) {
1753
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
1754
+ }
1755
+ return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
1756
+ }
1757
+
1758
+ // modules/payments-v2/machine/payload.ts
1759
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
1760
+ return {
1761
+ v: 2,
1762
+ kind: "coin",
1763
+ recipient: recipientPubkey,
1764
+ coinId: request.coinId,
1765
+ amount: request.amount,
1766
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
1767
+ direct: [...spend.plan.direct],
1768
+ ...spend.plan.split !== void 0 ? {
1769
+ split: {
1770
+ tokenId: spend.plan.split.tokenId,
1771
+ splitAmount: spend.plan.split.splitAmount.toString(),
1772
+ remainderAmount: spend.plan.split.remainderAmount.toString()
1773
+ }
1774
+ } : {},
1775
+ spentStates
1776
+ };
1777
+ }
1778
+ function messageOf(err) {
1779
+ return err instanceof Error ? err.message : String(err);
1780
+ }
1781
+ function buildCoinlessPayload(recipientPubkey, request, spentStates) {
1782
+ return {
1783
+ v: 2,
1784
+ kind: "coinless",
1785
+ recipient: recipientPubkey,
1786
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
1787
+ direct: [request.tokenId],
1788
+ spentStates
1789
+ };
1790
+ }
1791
+
1792
+ // modules/payments-v2/mint-params.ts
1793
+ function mintParams(recipientPubkey, coinId, amount) {
1794
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1795
+ }
1796
+
1797
+ // modules/payments-v2/mint.ts
1798
+ function runMintUnderJournal(deps, input) {
1799
+ return mintUnderJournal(deps, input);
1800
+ }
1801
+ async function mintUnderJournal(deps, input) {
1802
+ const { mintId, coinId, amount } = input;
1803
+ const engine = deps.engine;
1804
+ const entry = {
1805
+ mintId,
1806
+ coinId,
1807
+ amount: amount.toString(),
1808
+ tokenId: "",
1809
+ createdAt: deps.now()
1810
+ };
1811
+ await deps.mintJournal.upsert(entry);
1812
+ let token;
1813
+ try {
1814
+ token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
1815
+ } catch (err) {
1816
+ deps.armHeartbeat();
1817
+ return { success: false, error: messageOf(err) };
1818
+ }
1819
+ if (token.blob.tokenId !== entry.tokenId) {
1820
+ await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
1821
+ }
1822
+ try {
1823
+ await finalizeMint(deps, mintId, token, coinId, amount.toString());
1824
+ } catch (err) {
1825
+ deps.armHeartbeat();
1826
+ return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
1827
+ }
1828
+ return { success: true, tokenId: token.blob.tokenId };
1829
+ }
1830
+ async function finalizeMint(deps, mintId, token, coinId, amount) {
1831
+ const engine = deps.engine;
1832
+ const bytes = token.blob.token;
1833
+ const digest = bytesToHex(sha256(bytes));
1834
+ const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
1835
+ const key = keys.get(digest);
1836
+ if (key === void 0) {
1837
+ throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
1838
+ }
1839
+ await deps.storagePort.applyDelta({
1840
+ transferId: mintId,
1841
+ spent: [],
1842
+ added: [{ tokenId: token.blob.tokenId, key }]
1843
+ });
1844
+ await deps.recordMint({
1845
+ tokenId: token.blob.tokenId,
1846
+ assets: [{ coinId, amount }]
1847
+ });
1848
+ await deps.mintJournal.removeByKey(mintId);
1849
+ deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
1850
+ deps.refreshView();
1851
+ }
1852
+
1853
+ // modules/payments-v2/send-coinless.ts
1854
+ async function materializeCoinlessSpend(deps, input) {
1855
+ const { engine } = deps;
1856
+ const tokenId = input.request.tokenId;
1857
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
1858
+ if (bytes === void 0) {
1859
+ throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
1860
+ }
1861
+ const token = await engine.decodeToken({ tokenId, token: bytes });
1862
+ if (!isCoinlessEnvelope(token.valueEnvelope)) {
1863
+ throw new SphereError(
1864
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use send()`,
1865
+ "VALIDATION_ERROR"
1866
+ );
1867
+ }
1868
+ const keys = await engine.deliveryKeys(bytes);
1869
+ const payload = buildCoinlessPayload(input.recipientPubkey, input.request, {
1870
+ [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
1871
+ });
1872
+ const plan = {
1873
+ transferId: input.transferId,
1874
+ recipientPubkey: input.recipientPubkey,
1875
+ payload,
1876
+ ops: buildOps(payload),
1877
+ sources: /* @__PURE__ */ new Map([[tokenId, token]]),
1878
+ ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
1879
+ };
1880
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens: [] };
1881
+ }
1882
+
1883
+ // modules/payments-v2/send-errors.ts
1884
+ function partialize(err, run) {
1885
+ if (run.delivered.length === 0) return err;
1886
+ return new PartialSendConflictError(
1887
+ "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.",
1888
+ run.firstPartialId ?? "",
1889
+ [...run.delivered],
1890
+ run.amount,
1891
+ err
1892
+ );
1893
+ }
1894
+ function stampTransferId(err, transferId) {
1895
+ if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
1896
+ err.transferId = transferId;
1897
+ }
1898
+ }
1899
+
1623
1900
  // modules/payments-v2/recipient.ts
1624
1901
  var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
1625
1902
  async function requireSameNetworkRecipient(deps, identifier) {
@@ -1668,11 +1945,6 @@ async function splitOpIndex(deps, envelope) {
1668
1945
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
1669
1946
  }
1670
1947
 
1671
- // modules/payments-v2/mint-params.ts
1672
- function mintParams(recipientPubkey, coinId, amount) {
1673
- return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
1674
- }
1675
-
1676
1948
  // modules/payments-v2/prewarm-cache.ts
1677
1949
  var PrewarmCache = class _PrewarmCache {
1678
1950
  entries = /* @__PURE__ */ new Map();
@@ -1846,29 +2118,6 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
1846
2118
  });
1847
2119
  }
1848
2120
 
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
2121
  // node_modules/@noble/ciphers/utils.js
1873
2122
  function isBytes2(a) {
1874
2123
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
@@ -2720,6 +2969,14 @@ function decryptField(key, envelope) {
2720
2969
  }
2721
2970
 
2722
2971
  // modules/payments-v2/history/History.ts
2972
+ function wireAssets(assets) {
2973
+ return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
2974
+ }
2975
+ function isPermanentReject(err) {
2976
+ const status = err?.status;
2977
+ if (typeof status !== "number") return false;
2978
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
2979
+ }
2723
2980
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
2724
2981
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
2725
2982
  var History = class {
@@ -2744,7 +3001,7 @@ var History = class {
2744
3001
  dedupKey: input.transferId,
2745
3002
  id: (this.deps.newId ?? randomUUID)(),
2746
3003
  type: "SENT",
2747
- assets: [{ coinId: input.coinId, amount: input.amount }],
3004
+ assets: wireAssets(input.assets),
2748
3005
  ts: this.ts(input.timestamp),
2749
3006
  transferId: input.transferId,
2750
3007
  ...this.wireTokenId(input.tokenId),
@@ -2757,7 +3014,7 @@ var History = class {
2757
3014
  dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
2758
3015
  id: (this.deps.newId ?? randomUUID)(),
2759
3016
  type: "RECEIVED",
2760
- assets: [{ coinId: input.coinId, amount: input.amount }],
3017
+ assets: wireAssets(input.assets),
2761
3018
  ts: this.ts(input.timestamp),
2762
3019
  ...input.transferId !== void 0 ? { transferId: input.transferId } : {},
2763
3020
  ...this.wireTokenId(input.tokenId),
@@ -2770,7 +3027,7 @@ var History = class {
2770
3027
  dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
2771
3028
  id: (this.deps.newId ?? randomUUID)(),
2772
3029
  type: "MINT",
2773
- assets: [{ coinId: input.coinId, amount: input.amount }],
3030
+ assets: wireAssets(input.assets),
2774
3031
  ts: this.ts(input.timestamp),
2775
3032
  ...this.wireTokenId(input.tokenId)
2776
3033
  }));
@@ -2781,7 +3038,10 @@ var History = class {
2781
3038
  try {
2782
3039
  await this.deps.client.postHistory([record]);
2783
3040
  } catch (err) {
2784
- this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
3041
+ this.deps.log?.(
3042
+ isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
3043
+ err
3044
+ );
2785
3045
  return;
2786
3046
  }
2787
3047
  this.deps.emit("history:updated", this.toEntry(record));
@@ -2848,6 +3108,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
2848
3108
  function stateKey(tokenId, stateHash) {
2849
3109
  return `${tokenId}:${stateHash}`;
2850
3110
  }
3111
+ function unchanged(prev, item, tokenType) {
3112
+ return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
3113
+ }
3114
+ function isCoinless(item, prev) {
3115
+ if (item.status !== "active") return prev?.coinless ?? false;
3116
+ return (item.assets?.length ?? 0) === 0;
3117
+ }
2851
3118
  var InventoryView = class {
2852
3119
  constructor(deps) {
2853
3120
  this.deps = deps;
@@ -2941,14 +3208,26 @@ var InventoryView = class {
2941
3208
  pool(coinId) {
2942
3209
  const out = [];
2943
3210
  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;
3211
+ if (!this.isSpendable(tokenId, entry)) continue;
2947
3212
  const asset = entry.assets.find((a) => a.coinId === coinId);
2948
3213
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
2949
3214
  }
2950
3215
  return out;
2951
3216
  }
3217
+ /**
3218
+ * The eligibility gates every spend shares, coin or token — ONE definition, so a
3219
+ * single probe covers both verbs and neither can drift from the other.
3220
+ */
3221
+ isSpendable(tokenId, entry) {
3222
+ if (entry.status !== "active") return false;
3223
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
3224
+ return !this.suspected.has(stateKey(tokenId, entry.stateHash));
3225
+ }
3226
+ /** #777: is this NAMED token a spendable COINLESS holding? Never a coin source. */
3227
+ spendableCoinless(tokenId) {
3228
+ const entry = this.mirror.get(tokenId);
3229
+ return entry !== void 0 && entry.coinless && this.isSpendable(tokenId, entry);
3230
+ }
2952
3231
  /**
2953
3232
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
2954
3233
  * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
@@ -2974,6 +3253,29 @@ var InventoryView = class {
2974
3253
  }
2975
3254
  return out;
2976
3255
  }
3256
+ /**
3257
+ * Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
3258
+ * exactly when it is not, so no mirror row can appear in both reads.
3259
+ */
3260
+ coinless(registry) {
3261
+ const out = [];
3262
+ for (const [tokenId, entry] of this.mirror) {
3263
+ if (entry.status !== "active" || !entry.coinless) continue;
3264
+ const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
3265
+ out.push({
3266
+ tokenId,
3267
+ ...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
3268
+ ...meta ? { name: meta.name } : {},
3269
+ ...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
3270
+ stateHash: entry.stateHash,
3271
+ transferring: this.held(tokenId),
3272
+ ...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
3273
+ createdAt: entry.createdAt,
3274
+ updatedAt: entry.updatedAt
3275
+ });
3276
+ }
3277
+ return out;
3278
+ }
2977
3279
  async assets(registry, price) {
2978
3280
  const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
2979
3281
  if (!price || raw.length === 0) return raw;
@@ -3045,14 +3347,19 @@ var InventoryView = class {
3045
3347
  applyOne(item, now) {
3046
3348
  const prev = this.mirror.get(item.tokenId);
3047
3349
  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
- }
3350
+ const tokenType = item.tokenType ?? prev?.tokenType;
3351
+ if (prev && unchanged(prev, item, tokenType)) return false;
3051
3352
  this.mirror.set(item.tokenId, {
3052
3353
  stateHash: item.stateHash,
3053
3354
  seq: item.seq,
3054
3355
  status: item.status,
3055
- assets: item.assets ?? prev?.assets ?? [],
3356
+ // ONLY a tombstone inherits: it omits assets for an unrelated reason and
3357
+ // recoverRemoved must know the amount it restores. An ACTIVE row that omits
3358
+ // them is stating coinlessness, and inheriting there would keep stale assets
3359
+ // in tokens() while `coinless` is true — the row in BOTH reads (§16).
3360
+ assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
3361
+ coinless: isCoinless(item, prev),
3362
+ ...tokenType !== void 0 ? { tokenType } : {},
3056
3363
  createdAt: prev?.createdAt ?? now,
3057
3364
  updatedAt: now
3058
3365
  });
@@ -3283,7 +3590,12 @@ async function screen(deps, engine, entry) {
3283
3590
  }
3284
3591
  return {
3285
3592
  kind: "accept",
3286
- record: { tokenId: keys.tokenId, stateHash: keys.stateHash, assets: toAssetAmounts(token) }
3593
+ record: {
3594
+ tokenId: keys.tokenId,
3595
+ stateHash: keys.stateHash,
3596
+ assets: toAssetAmounts(token),
3597
+ ...isCoinlessEnvelope2(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
3598
+ }
3287
3599
  };
3288
3600
  }
3289
3601
  async function announce(deps, entry, record) {
@@ -3294,6 +3606,7 @@ async function announce(deps, entry, record) {
3294
3606
  tokenId: record.tokenId,
3295
3607
  stateHash: record.stateHash,
3296
3608
  assets: record.assets,
3609
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
3297
3610
  ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
3298
3611
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
3299
3612
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
@@ -3307,6 +3620,19 @@ async function announce(deps, entry, record) {
3307
3620
  senderPubkey: entry.senderPubkey ?? "",
3308
3621
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
3309
3622
  tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
3623
+ // #777: named here rather than mapped from assets, which announced an EMPTY list.
3624
+ ...record.tokenType !== void 0 ? {
3625
+ coinless: [
3626
+ {
3627
+ tokenId: record.tokenId,
3628
+ tokenType: record.tokenType,
3629
+ stateHash: record.stateHash,
3630
+ transferring: false,
3631
+ createdAt: receivedAt,
3632
+ updatedAt: receivedAt
3633
+ }
3634
+ ]
3635
+ } : {},
3310
3636
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
3311
3637
  receivedAt
3312
3638
  };
@@ -3405,6 +3731,9 @@ function claimAck(entry) {
3405
3731
  function rejectAck(entry, reason) {
3406
3732
  return { deliveryId: entry.deliveryId, disposition: "rejected", reason, cursor: entry.cursor };
3407
3733
  }
3734
+ function isCoinlessEnvelope2(envelope) {
3735
+ return envelope.startsWith("none_");
3736
+ }
3408
3737
  function toAssetAmounts(token) {
3409
3738
  return (token.value?.assets ?? []).map((asset) => ({
3410
3739
  coinId: asset.coinId,
@@ -3972,6 +4301,37 @@ var SpendQueue = class {
3972
4301
  }
3973
4302
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
3974
4303
  }
4304
+ /**
4305
+ * #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
4306
+ * for. A coin plan waits because some OTHER combination may free up; a named
4307
+ * token is either free now or held by a reservation that only its own transfer
4308
+ * can release, so queueing would block until a timeout on a spend that cannot
4309
+ * become possible.
4310
+ */
4311
+ planCoinless(reservationId, tokenId) {
4312
+ if (this.destroyed) {
4313
+ throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
4314
+ }
4315
+ const unproven = this.deps.ledger.unprovenReason();
4316
+ if (unproven !== null) {
4317
+ throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
4318
+ }
4319
+ if (this.deps.spendableCoinless?.(tokenId) !== true) {
4320
+ throw new SphereError(
4321
+ `Token ${tokenId} is not a spendable coinless holding`,
4322
+ "VALIDATION_ERROR"
4323
+ );
4324
+ }
4325
+ const holder = this.deps.ledger.holderOf(tokenId);
4326
+ if (holder !== void 0) {
4327
+ throw new SphereError(
4328
+ `Token ${tokenId} is already reserved by transfer ${holder}`,
4329
+ "VALIDATION_ERROR"
4330
+ );
4331
+ }
4332
+ this.deps.ledger.reserve(reservationId, [{ tokenId }]);
4333
+ return { reservationId, plan: { direct: [tokenId] } };
4334
+ }
3975
4335
  /**
3976
4336
  * The sources `plan()` WOULD pick right now, reserving nothing and queueing
3977
4337
  * nothing — a read-only probe used to warm blobs while the user is still
@@ -4161,6 +4521,7 @@ function composeFacadeParts(deps, hooks) {
4161
4521
  const queue = new SpendQueue({
4162
4522
  ledger,
4163
4523
  getPool: (coinId) => view.pool(coinId),
4524
+ spendableCoinless: (tokenId) => view.spendableCoinless(tokenId),
4164
4525
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
4165
4526
  });
4166
4527
  const historyStore = new History({
@@ -4244,10 +4605,9 @@ function buildMachineDeps(deps, hooks, historyStore) {
4244
4605
  recordHistory: async ({ transferId, payload, committedAmount }) => {
4245
4606
  await historyStore.recordSent({
4246
4607
  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,
4608
+ // §5.9: the SETTLED amount, never payload.amount — the plan. A token
4609
+ // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
4610
+ ...payload.kind === "coinless" ? { assets: [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
4251
4611
  recipientPubkey: payload.recipient,
4252
4612
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
4253
4613
  });
@@ -4282,12 +4642,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
4282
4642
  });
4283
4643
  }
4284
4644
  async function recordReceived(historyStore, record) {
4285
- const first = record.assets[0];
4286
4645
  await historyStore.recordReceived({
4287
4646
  tokenId: record.tokenId,
4288
4647
  stateHash: record.stateHash,
4289
- coinId: first?.coinId ?? "",
4290
- amount: first?.amount ?? "0",
4648
+ assets: record.assets,
4291
4649
  ...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
4292
4650
  ...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
4293
4651
  ...record.memo !== void 0 ? { memo: record.memo } : {},
@@ -4435,6 +4793,13 @@ var PaymentsFacade = class {
4435
4793
  tokens(filter) {
4436
4794
  return this.view.tokens(this.deps.registry, filter);
4437
4795
  }
4796
+ coinless() {
4797
+ return this.view.coinless(this.deps.registry);
4798
+ }
4799
+ tokenData(tokenId) {
4800
+ const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
4801
+ return readTokenData(deps, tokenId);
4802
+ }
4438
4803
  history(page) {
4439
4804
  return this.historyStore.page(page ?? {});
4440
4805
  }
@@ -4459,6 +4824,9 @@ var PaymentsFacade = class {
4459
4824
  send(request) {
4460
4825
  return this.track(this.sendOutcome(request));
4461
4826
  }
4827
+ sendCoinless(request) {
4828
+ return this.track(this.sendCoinlessOutcome(request));
4829
+ }
4462
4830
  async receive() {
4463
4831
  const transfers = await this.track(this.receiveLoop.drainOnce());
4464
4832
  return { transfers };
@@ -4489,12 +4857,32 @@ var PaymentsFacade = class {
4489
4857
  nowMs() {
4490
4858
  return (this.deps.now ?? Date.now)();
4491
4859
  }
4860
+ mintDeps() {
4861
+ return {
4862
+ engine: this.engine(),
4863
+ mintJournal: this.machineStores.mintJournal,
4864
+ storagePort: this.deps.storagePort,
4865
+ recordMint: (input) => this.historyStore.recordMint(input),
4866
+ armHeartbeat: () => this.heartbeat.arm(),
4867
+ noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
4868
+ refreshView: () => this.trackTail(this.view.delta()),
4869
+ ownPubkeyBytes: this.ownPubkeyBytes,
4870
+ now: () => this.nowMs()
4871
+ };
4872
+ }
4492
4873
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
4493
4874
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
4494
4875
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
4495
- async sendOutcome(request) {
4876
+ sendOutcome(request) {
4877
+ return this.runJob({ kind: "coin", request }, request.amount);
4878
+ }
4879
+ /** A token spend has no amount; '0' keeps the shortfall arithmetic total-free. */
4880
+ sendCoinlessOutcome(request) {
4881
+ return this.runJob({ kind: "coinless", request }, "0");
4882
+ }
4883
+ async runJob(job, amount) {
4496
4884
  const run = {
4497
- amount: request.amount,
4885
+ amount,
4498
4886
  delivered: [],
4499
4887
  sentTokens: [],
4500
4888
  tokenTransfers: [],
@@ -4504,7 +4892,7 @@ var PaymentsFacade = class {
4504
4892
  lastTransferId: ""
4505
4893
  };
4506
4894
  try {
4507
- return await this.sendWithPolicy(request, run);
4895
+ return await this.sendWithPolicy(job, run);
4508
4896
  } catch (err) {
4509
4897
  this.emitCleanFailure(err, run.lastTransferId);
4510
4898
  throw err;
@@ -4524,19 +4912,19 @@ var PaymentsFacade = class {
4524
4912
  };
4525
4913
  this.deps.emit("transfer:updated", failed);
4526
4914
  }
4527
- async sendWithPolicy(request, run) {
4528
- const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
4915
+ async sendWithPolicy(job, run) {
4916
+ const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
4529
4917
  for (let attempt = 0; ; attempt++) {
4530
4918
  let ctx;
4531
4919
  try {
4532
- ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
4920
+ ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
4533
4921
  } catch (err) {
4534
- throw this.partialize(err, run);
4922
+ throw partialize(err, run);
4535
4923
  }
4536
4924
  const disposition = await this.runAttempt(ctx);
4537
- if (disposition.kind === "rethrow") throw this.partialize(disposition.error, run);
4925
+ if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
4538
4926
  if (disposition.kind === "retry-full") {
4539
- if (attempt >= MAX_RESELECT) throw this.partialize(disposition.error, run);
4927
+ if (job.kind === "coinless" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
4540
4928
  continue;
4541
4929
  }
4542
4930
  if (disposition.kind === "success") {
@@ -4592,7 +4980,7 @@ var PaymentsFacade = class {
4592
4980
  async disposeFailedAttempt(ctx, err) {
4593
4981
  if (isPossiblyCommittedSendOutcome(err)) {
4594
4982
  this.settleKeepOpen(ctx);
4595
- this.stampTransferId(err, ctx.transferId);
4983
+ stampTransferId(err, ctx.transferId);
4596
4984
  return { kind: "rethrow", error: err };
4597
4985
  }
4598
4986
  const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
@@ -4626,7 +5014,7 @@ var PaymentsFacade = class {
4626
5014
  async disposeConvergeFailure(ctx, err) {
4627
5015
  if (isPossiblyCommittedSendOutcome(err)) {
4628
5016
  this.settleKeepOpen(ctx);
4629
- this.stampTransferId(err, ctx.transferId);
5017
+ stampTransferId(err, ctx.transferId);
4630
5018
  return { kind: "rethrow", error: err };
4631
5019
  }
4632
5020
  if (classifyError(err) === "conflict") {
@@ -4637,9 +5025,23 @@ var PaymentsFacade = class {
4637
5025
  }
4638
5026
  return { kind: "rethrow", error: err };
4639
5027
  }
4640
- async planAndMaterialize(recipientPubkey, request, run) {
5028
+ async planAndMaterialize(recipientPubkey, job, run) {
4641
5029
  const transferId = this.newId();
4642
5030
  run.lastTransferId = transferId;
5031
+ if (job.kind === "coinless") {
5032
+ const spend2 = this.queue.planCoinless(transferId, job.request.tokenId);
5033
+ const sourceIds2 = this.markPlanned(transferId, "", spend2);
5034
+ try {
5035
+ return await materializeCoinlessSpend(
5036
+ { engine: this.engine(), storagePort: this.deps.storagePort },
5037
+ { transferId, recipientPubkey, request: job.request, sourceIds: sourceIds2 }
5038
+ );
5039
+ } catch (err) {
5040
+ this.settleFailure(transferId, "", sourceIds2);
5041
+ throw err;
5042
+ }
5043
+ }
5044
+ const request = { ...job.request, amount: run.amount };
4643
5045
  const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
4644
5046
  const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
4645
5047
  const sourceIds = this.markPlanned(transferId, request.coinId, spend);
@@ -4773,22 +5175,6 @@ var PaymentsFacade = class {
4773
5175
  return result;
4774
5176
  }
4775
5177
  /** 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
5178
  async softAbort(transferId) {
4793
5179
  try {
4794
5180
  await this.deps.client.abortIntent(transferId);
@@ -4804,57 +5190,11 @@ var PaymentsFacade = class {
4804
5190
  const mintId = this.newId();
4805
5191
  this.activeMoneyOps.add(mintId);
4806
5192
  try {
4807
- return await this.mintUnderJournal(mintId, coinId, amount);
5193
+ return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
4808
5194
  } finally {
4809
5195
  this.activeMoneyOps.delete(mintId);
4810
5196
  }
4811
5197
  }
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
5198
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
4859
5199
  async replayMints() {
4860
5200
  let resolved = 0;
@@ -4886,7 +5226,7 @@ var PaymentsFacade = class {
4886
5226
  if (token.blob.tokenId !== entry.tokenId) {
4887
5227
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
4888
5228
  }
4889
- await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
5229
+ await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
4890
5230
  return true;
4891
5231
  }
4892
5232
  async tokenInServerInventory(tokenId) {