@unicitylabs/sphere-sdk 0.16.0-dev.2 → 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 (61) 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 +568 -172
  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 +568 -172
  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/browser/index.cjs +2 -2
  20. package/dist/impl/browser/index.cjs.map +1 -1
  21. package/dist/impl/browser/index.js +2 -2
  22. package/dist/impl/browser/index.js.map +1 -1
  23. package/dist/impl/nodejs/connect/index.cjs +10 -4
  24. package/dist/impl/nodejs/connect/index.cjs.map +1 -1
  25. package/dist/impl/nodejs/connect/index.d.cts +1 -1
  26. package/dist/impl/nodejs/connect/index.d.ts +1 -1
  27. package/dist/impl/nodejs/connect/index.js +10 -4
  28. package/dist/impl/nodejs/connect/index.js.map +1 -1
  29. package/dist/impl/nodejs/index.cjs +19 -3
  30. package/dist/impl/nodejs/index.cjs.map +1 -1
  31. package/dist/impl/nodejs/index.d.cts +11 -0
  32. package/dist/impl/nodejs/index.d.ts +11 -0
  33. package/dist/impl/nodejs/index.js +19 -3
  34. package/dist/impl/nodejs/index.js.map +1 -1
  35. package/dist/impl/shared/wallet-api/index.d.cts +11 -0
  36. package/dist/impl/shared/wallet-api/index.d.ts +11 -0
  37. package/dist/impl/wallet-api-v2/index.cjs +2 -0
  38. package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
  39. package/dist/impl/wallet-api-v2/index.d.cts +13 -0
  40. package/dist/impl/wallet-api-v2/index.d.ts +13 -0
  41. package/dist/impl/wallet-api-v2/index.js +2 -0
  42. package/dist/impl/wallet-api-v2/index.js.map +1 -1
  43. package/dist/index.cjs +568 -172
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +61 -1
  46. package/dist/index.d.ts +61 -1
  47. package/dist/index.js +568 -172
  48. package/dist/index.js.map +1 -1
  49. package/dist/modules/payments-v2/index.cjs +609 -269
  50. package/dist/modules/payments-v2/index.cjs.map +1 -1
  51. package/dist/modules/payments-v2/index.d.cts +88 -13
  52. package/dist/modules/payments-v2/index.d.ts +88 -13
  53. package/dist/modules/payments-v2/index.js +609 -269
  54. package/dist/modules/payments-v2/index.js.map +1 -1
  55. package/dist/token-engine/index.cjs +132 -34
  56. package/dist/token-engine/index.cjs.map +1 -1
  57. package/dist/token-engine/index.d.cts +19 -0
  58. package/dist/token-engine/index.d.ts +19 -0
  59. package/dist/token-engine/index.js +132 -34
  60. package/dist/token-engine/index.js.map +1 -1
  61. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -8273,6 +8273,8 @@ var FETCH_TIMEOUT_MS = 1e4;
8273
8273
  var TokenRegistry = class _TokenRegistry {
8274
8274
  static instance = null;
8275
8275
  definitionsById;
8276
+ /** Non-fungible definitions keyed by TOKEN TYPE — a separate namespace (#147). */
8277
+ definitionsByType;
8276
8278
  definitionsBySymbol;
8277
8279
  definitionsByName;
8278
8280
  // Remote refresh state
@@ -8291,6 +8293,7 @@ var TokenRegistry = class _TokenRegistry {
8291
8293
  inFlight = null;
8292
8294
  constructor() {
8293
8295
  this.definitionsById = /* @__PURE__ */ new Map();
8296
+ this.definitionsByType = /* @__PURE__ */ new Map();
8294
8297
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
8295
8298
  this.definitionsByName = /* @__PURE__ */ new Map();
8296
8299
  }
@@ -8518,9 +8521,11 @@ var TokenRegistry = class _TokenRegistry {
8518
8521
  this.definitionsById.clear();
8519
8522
  this.definitionsBySymbol.clear();
8520
8523
  this.definitionsByName.clear();
8524
+ this.definitionsByType.clear();
8521
8525
  for (const def of definitions) {
8522
8526
  const idLower = def.id.toLowerCase();
8523
8527
  this.definitionsById.set(idLower, def);
8528
+ if (def.assetKind === "non-fungible") this.definitionsByType.set(idLower, def);
8524
8529
  if (def.symbol) {
8525
8530
  this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
8526
8531
  }
@@ -8734,6 +8739,20 @@ var TokenRegistry = class _TokenRegistry {
8734
8739
  getAllDefinitions() {
8735
8740
  return Array.from(this.definitionsById.values());
8736
8741
  }
8742
+ /**
8743
+ * Definition for a coinless token's genesis TOKEN TYPE (#777). Never falls back
8744
+ * to `getDefinition`: a type and a coin id live in different namespaces, so a
8745
+ * type that collided with a coin id would otherwise render as that coin.
8746
+ */
8747
+ getTypeMeta(tokenType) {
8748
+ const def = this.getTypeDefinition(tokenType);
8749
+ if (!def) return null;
8750
+ return { name: def.name, iconUrl: def.icons?.[0]?.url ?? null };
8751
+ }
8752
+ getTypeDefinition(tokenType) {
8753
+ if (!tokenType) return void 0;
8754
+ return this.definitionsByType.get(tokenType.toLowerCase());
8755
+ }
8737
8756
  /**
8738
8757
  * Get all fungible token definitions
8739
8758
  * @returns Array of fungible token definitions
@@ -9568,6 +9587,8 @@ var import_DataHasherFactory = require("@unicitylabs/state-transition-sdk/lib/cr
9568
9587
  var import_CborSerializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js");
9569
9588
  var import_CborDeserializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js");
9570
9589
  var import_CborError = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js");
9590
+ var import_CborReader = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js");
9591
+ var import_MajorType = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js");
9571
9592
  var import_Asset = require("@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js");
9572
9593
  var import_AssetId = require("@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js");
9573
9594
  var import_PaymentAssetCollection = require("@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js");
@@ -9980,6 +10001,126 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
9980
10001
  return burntToken;
9981
10002
  }
9982
10003
 
10004
+ // token-engine/value-envelope.ts
10005
+ function isCoinlessEnvelope(envelope) {
10006
+ return envelope.startsWith("none_");
10007
+ }
10008
+ var SELF_DESCRIBED_CBOR_TAG = 55799n;
10009
+ var CBOR_MAJOR_TYPE_MASK = 224;
10010
+ var describe = (error) => error instanceof Error ? error.message : String(error);
10011
+ var invalid = (message) => new SphereError(message, "VALIDATION_ERROR");
10012
+ function majorTypeOf(bytes) {
10013
+ const first = bytes.at(0);
10014
+ return first === void 0 ? null : first & CBOR_MAJOR_TYPE_MASK;
10015
+ }
10016
+ function headTagNumber(bytes) {
10017
+ try {
10018
+ return new import_CborReader.CborReader(bytes).readLength(import_MajorType.MajorType.TAG);
10019
+ } catch {
10020
+ return null;
10021
+ }
10022
+ }
10023
+ function isAssetShaped(item) {
10024
+ try {
10025
+ return import_CborDeserializer.CborDeserializer.decodeArray(item, 2).every(
10026
+ (field) => majorTypeOf(field) === import_MajorType.MajorType.BYTE_STRING
10027
+ );
10028
+ } catch {
10029
+ return false;
10030
+ }
10031
+ }
10032
+ function arrayItemsOrNull(bytes) {
10033
+ try {
10034
+ return import_CborDeserializer.CborDeserializer.decodeArray(bytes);
10035
+ } catch {
10036
+ return null;
10037
+ }
10038
+ }
10039
+ function decodeSphere(genesisData) {
10040
+ try {
10041
+ return SpherePaymentData.fromCBOR(genesisData).toValue();
10042
+ } catch (error) {
10043
+ throw invalid(`Failed to decode token payment data: ${describe(error)}`);
10044
+ }
10045
+ }
10046
+ function taggedItemBody(genesisData) {
10047
+ try {
10048
+ return import_CborDeserializer.CborDeserializer.decodeTag(genesisData).data;
10049
+ } catch (error) {
10050
+ throw invalid(
10051
+ `Token value payload is a CBOR tag whose item is malformed or carries trailing bytes: ${describe(error)}`
10052
+ );
10053
+ }
10054
+ }
10055
+ function selfDescribedBodyCarriesValue(body) {
10056
+ const major = majorTypeOf(body);
10057
+ if (major === import_MajorType.MajorType.TAG) {
10058
+ const inner = headTagNumber(body);
10059
+ return inner === SpherePaymentData.CBOR_TAG || inner === SELF_DESCRIBED_CBOR_TAG;
10060
+ }
10061
+ if (major === import_MajorType.MajorType.ARRAY) {
10062
+ const items = arrayItemsOrNull(body);
10063
+ return items !== null && items.some(isAssetShaped);
10064
+ }
10065
+ return false;
10066
+ }
10067
+ function classifyTagged(genesisData) {
10068
+ const tag = headTagNumber(genesisData);
10069
+ if (tag === null) {
10070
+ throw invalid(
10071
+ "Token value payload has an unreadable or non-canonically encoded CBOR tag head \u2014 it could be a malformed SpherePaymentData envelope"
10072
+ );
10073
+ }
10074
+ if (tag === SpherePaymentData.CBOR_TAG) {
10075
+ return { envelope: "sphere", value: decodeSphere(genesisData) };
10076
+ }
10077
+ const body = taggedItemBody(genesisData);
10078
+ if (tag === SELF_DESCRIBED_CBOR_TAG && selfDescribedBodyCarriesValue(body)) {
10079
+ throw invalid(
10080
+ "Token value payload is a value envelope wrapped in the self-described CBOR tag 55799 (RFC 8949 \xA73.4.6), which no codec here unwraps \u2014 its coins would be invisible. Emit the envelope without the self-describe prefix"
10081
+ );
10082
+ }
10083
+ return { envelope: "none_tag", value: null };
10084
+ }
10085
+ function classifyArray(genesisData) {
10086
+ const items = arrayItemsOrNull(genesisData);
10087
+ if (items === null) {
10088
+ throw invalid("Token value payload is not PaymentAssetCollection: malformed CBOR array");
10089
+ }
10090
+ return items.some(isAssetShaped) ? { envelope: "bare_collection", value: null } : { envelope: "none_other", value: null };
10091
+ }
10092
+ function classifyValueEnvelope(genesisData) {
10093
+ if (genesisData === null) return { envelope: "none_absent", value: null };
10094
+ const major = majorTypeOf(genesisData);
10095
+ if (major === null) return { envelope: "none_absent", value: null };
10096
+ if (major === import_MajorType.MajorType.TAG) return classifyTagged(genesisData);
10097
+ if (major === import_MajorType.MajorType.ARRAY) return classifyArray(genesisData);
10098
+ return { envelope: "none_other", value: null };
10099
+ }
10100
+ function wrapToken(sdkToken) {
10101
+ const { envelope, value } = classifyValueEnvelope(sdkToken.genesis.data);
10102
+ const blob = {
10103
+ tokenId: import_HexConverter.HexConverter.encode(sdkToken.id.bytes),
10104
+ token: sdkToken.toCBOR()
10105
+ };
10106
+ return {
10107
+ sdkToken,
10108
+ blob,
10109
+ value,
10110
+ valueEnvelope: envelope,
10111
+ tokenType: import_HexConverter.HexConverter.encode(sdkToken.type.bytes)
10112
+ };
10113
+ }
10114
+ function assertMintableData(data) {
10115
+ try {
10116
+ classifyValueEnvelope(data);
10117
+ } catch (error) {
10118
+ throw invalid(
10119
+ `Cannot mint a data token with this payload: ${describe(error)}. Raw bytes starting in the CBOR array (0x80-0x9f) or tag (0xc0-0xdf) range must be well-formed canonical CBOR; wrap them in a CBOR byte string, map, text string, or a tag other than 39050/55799.`
10120
+ );
10121
+ }
10122
+ }
10123
+
9983
10124
  // token-engine/SphereTokenEngine.ts
9984
10125
  var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
9985
10126
  var MAX_MINT_CONCURRENCY = 8;
@@ -10036,7 +10177,7 @@ var SphereTokenEngine = class {
10036
10177
  return sdkToken.latestTransaction.data;
10037
10178
  }
10038
10179
  const data = sdkToken.genesis.data;
10039
- if (data && this.isSpherePaymentData(data)) {
10180
+ if (data && classifyValueEnvelope(data).envelope === "sphere") {
10040
10181
  return SpherePaymentData.fromCBOR(data).memo;
10041
10182
  }
10042
10183
  return null;
@@ -10066,9 +10207,10 @@ var SphereTokenEngine = class {
10066
10207
  );
10067
10208
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10068
10209
  const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
10069
- return this.wrapToken(token);
10210
+ return wrapToken(token);
10070
10211
  }
10071
10212
  async mintDataToken(params, options) {
10213
+ assertMintableData(params.data);
10072
10214
  const recipient = import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey);
10073
10215
  const tokenType = params.tokenType ? new import_TokenType.TokenType(params.tokenType) : import_TokenType.TokenType.generate();
10074
10216
  const salt = params.salt ? import_TokenSalt.TokenSalt.fromBytes(params.salt) : import_TokenSalt.TokenSalt.generate();
@@ -10083,7 +10225,7 @@ var SphereTokenEngine = class {
10083
10225
  );
10084
10226
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10085
10227
  const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
10086
- return this.wrapToken(token);
10228
+ return wrapToken(token);
10087
10229
  }
10088
10230
  async transfer(params, options) {
10089
10231
  this.assertOwned(params.token);
@@ -10106,13 +10248,19 @@ var SphereTokenEngine = class {
10106
10248
  );
10107
10249
  const certified = await transferTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10108
10250
  const transferred = await params.token.sdkToken.transfer(certified, this.deps.verificationContext);
10109
- return this.wrapToken(transferred);
10251
+ return wrapToken(transferred);
10110
10252
  }
10111
10253
  async split(params, options) {
10112
10254
  this.assertOwned(params.token);
10113
10255
  if (params.outputs.length === 0) {
10114
10256
  throw new SphereError("Split requires at least one output", "VALIDATION_ERROR");
10115
10257
  }
10258
+ if (params.token.value === null) {
10259
+ throw new SphereError(
10260
+ `Cannot split token ${params.token.blob.tokenId}: its genesis payload carries no value this SDK can read (envelope: ${params.token.valueEnvelope}). A coinless token can only be transferred whole.`,
10261
+ "VALIDATION_ERROR"
10262
+ );
10263
+ }
10116
10264
  const transferId = this.resolveTransferId(options);
10117
10265
  const requests = params.outputs.map(
10118
10266
  (o, i) => import_SplitTokenRequest.SplitTokenRequest.create(
@@ -10234,7 +10382,7 @@ var SphereTokenEngine = class {
10234
10382
  const proof = await this.submitSplitMintLeg(certData, mintTx, options);
10235
10383
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10236
10384
  const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
10237
- return this.wrapToken(token);
10385
+ return wrapToken(token);
10238
10386
  }
10239
10387
  /**
10240
10388
  * Submit one split-mint leg. A `TRANSACTION_HASH_MISMATCH` on a mint leg is NOT a foreign spend
@@ -10292,7 +10440,7 @@ var SphereTokenEngine = class {
10292
10440
  "VALIDATION_ERROR"
10293
10441
  );
10294
10442
  }
10295
- return this.wrapToken(sdkToken);
10443
+ return wrapToken(sdkToken);
10296
10444
  }
10297
10445
  // ── internals ────────────────────────────────────────────────────────────────
10298
10446
  /**
@@ -10404,34 +10552,6 @@ var SphereTokenEngine = class {
10404
10552
  throw new SphereError("Cannot transfer a token not owned by this engine identity", "VALIDATION_ERROR");
10405
10553
  }
10406
10554
  }
10407
- /** Wrap an SDK token into a SphereToken: cache its blob (incl. stable tokenId) + decoded value. */
10408
- wrapToken(sdkToken) {
10409
- const data = sdkToken.genesis.data;
10410
- let value = null;
10411
- if (data && this.isSpherePaymentData(data)) {
10412
- try {
10413
- value = SpherePaymentData.fromCBOR(data).toValue();
10414
- } catch (err) {
10415
- throw new SphereError(
10416
- `Failed to decode token payment data: ${err instanceof Error ? err.message : String(err)}`,
10417
- "VALIDATION_ERROR"
10418
- );
10419
- }
10420
- }
10421
- const blob = {
10422
- tokenId: import_HexConverter.HexConverter.encode(sdkToken.id.bytes),
10423
- token: sdkToken.toCBOR()
10424
- };
10425
- return { sdkToken, blob, value };
10426
- }
10427
- /** True if the bytes are a SpherePaymentData envelope (value token) vs a raw data token. */
10428
- isSpherePaymentData(data) {
10429
- try {
10430
- return import_CborDeserializer.CborDeserializer.decodeTag(data).tag === SpherePaymentData.CBOR_TAG;
10431
- } catch {
10432
- return false;
10433
- }
10434
- }
10435
10555
  /**
10436
10556
  * Terminate the verification worker pool, if this engine was configured with
10437
10557
  * one. Idempotent — the pool's dispose only touches workers it spawned.
@@ -11827,6 +11947,14 @@ function createMachineStores(kv) {
11827
11947
  };
11828
11948
  }
11829
11949
 
11950
+ // modules/payments-v2/machine/payload-view.ts
11951
+ function splitOf(p) {
11952
+ return p.kind === "coin" ? p.split : void 0;
11953
+ }
11954
+ function coinIdOf(p) {
11955
+ return p.kind === "coin" ? p.coinId : void 0;
11956
+ }
11957
+
11830
11958
  // modules/payments-v2/machine/TransferMachine.ts
11831
11959
  var CERTIFY_WIDTH = 20;
11832
11960
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
@@ -12029,13 +12157,14 @@ var TransferMachine = class {
12029
12157
  const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
12030
12158
  return { recipientBlob: finished.blob.token };
12031
12159
  }
12032
- const split = ctx.payload.split;
12160
+ const split = splitOf(ctx.payload);
12161
+ const splitCoinId = coinIdOf(ctx.payload);
12033
12162
  const { outputs } = await engine.split(
12034
12163
  {
12035
12164
  token: source,
12036
12165
  outputs: [
12037
- { recipientPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.splitAmount) },
12038
- { recipientPubkey: this.deps.ownPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.remainderAmount) }
12166
+ { recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
12167
+ { recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
12039
12168
  ]
12040
12169
  },
12041
12170
  opts
@@ -12157,7 +12286,9 @@ var TransferMachine = class {
12157
12286
  const entry = {
12158
12287
  transferId: r.transferId,
12159
12288
  remainingAmount: undelivered.toString(),
12160
- coinId: r.payload.coinId,
12289
+ // '' only on a token-addressed intent, which is one leg: it either lands or
12290
+ // conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
12291
+ coinId: coinIdOf(r.payload) ?? "",
12161
12292
  recipient: r.payload.recipient,
12162
12293
  committedTokenIds: committed.map((o) => o.op.sourceTokenId),
12163
12294
  createdAt: this.deps.now()
@@ -12183,7 +12314,8 @@ var TransferMachine = class {
12183
12314
  }
12184
12315
  async finishBookkeeping(engine, transferId, payload, phase, committed) {
12185
12316
  try {
12186
- const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
12317
+ const coinId = coinIdOf(payload);
12318
+ const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
12187
12319
  await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
12188
12320
  } catch {
12189
12321
  }
@@ -12226,8 +12358,11 @@ var TransferMachine = class {
12226
12358
  }
12227
12359
  };
12228
12360
  function conflictAmount(engine, payload, op, source) {
12229
- if (op.kind === "split") return BigInt(payload.split.splitAmount);
12230
- return source !== void 0 ? engine.balanceOf(source, payload.coinId) : 0n;
12361
+ const split = splitOf(payload);
12362
+ if (op.kind === "split") return BigInt(split.splitAmount);
12363
+ const coinId = coinIdOf(payload);
12364
+ if (coinId === void 0) return 0n;
12365
+ return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
12231
12366
  }
12232
12367
 
12233
12368
  // modules/payments-v2/machine/resume.ts
@@ -12334,6 +12469,14 @@ async function runOne(ctx, job, report) {
12334
12469
  report.failed.push(job.transferId);
12335
12470
  return;
12336
12471
  }
12472
+ if (job.payload.kind === "coinless" && !isCoinlessEnvelope(token.valueEnvelope)) {
12473
+ logger.warn(
12474
+ "PaymentsV2",
12475
+ `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
12476
+ );
12477
+ report.failed.push(job.transferId);
12478
+ return;
12479
+ }
12337
12480
  mine.set(op.sourceTokenId, token);
12338
12481
  }
12339
12482
  try {
@@ -12364,6 +12507,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
12364
12507
  }
12365
12508
  report.failed.push(transferId);
12366
12509
  }
12510
+ function validateCoinPayload(c) {
12511
+ if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
12512
+ throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
12513
+ }
12514
+ const s = c.split;
12515
+ if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
12516
+ throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
12517
+ }
12518
+ return { ...c, kind: "coin" };
12519
+ }
12520
+ function validateCoinlessPayload(p) {
12521
+ if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
12522
+ throw new SphereError(
12523
+ "token intent payload must name exactly one source token",
12524
+ "VALIDATION_ERROR"
12525
+ );
12526
+ }
12527
+ if (p.split !== void 0) {
12528
+ throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
12529
+ }
12530
+ return { ...p, kind: "coinless", direct: [p.direct[0]] };
12531
+ }
12367
12532
  function validatePayload(raw) {
12368
12533
  const p = raw !== null && typeof raw === "object" ? raw : null;
12369
12534
  if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
@@ -12372,14 +12537,17 @@ function validatePayload(raw) {
12372
12537
  "VALIDATION_ERROR"
12373
12538
  );
12374
12539
  }
12375
- if (typeof p.recipient !== "string" || typeof p.coinId !== "string" || typeof p.amount !== "string") {
12376
- throw new SphereError("intent payload is missing recipient/coinId/amount", "VALIDATION_ERROR");
12540
+ if (typeof p.recipient !== "string") {
12541
+ throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
12377
12542
  }
12378
- const s = p.split;
12379
- if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
12380
- throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
12543
+ const kind = p.kind ?? "coin";
12544
+ if (kind !== "coin" && kind !== "coinless") {
12545
+ throw new SphereError(
12546
+ `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
12547
+ "VALIDATION_ERROR"
12548
+ );
12381
12549
  }
12382
- return p;
12550
+ return kind === "coinless" ? validateCoinlessPayload(p) : validateCoinPayload(p);
12383
12551
  }
12384
12552
 
12385
12553
  // modules/payments-v2/convergence.ts
@@ -12585,13 +12753,23 @@ async function openRow(entry, journal, decryptPayload) {
12585
12753
  transferId: entry.transferId,
12586
12754
  kind: "open",
12587
12755
  recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
12588
- coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
12589
- amount: typeof payload?.amount === "string" ? payload.amount : "",
12756
+ ...subject(payload),
12590
12757
  legs: { certified, total: Math.max(total, certified) },
12591
12758
  deliveryPending: journal.length > 0,
12592
12759
  createdAt: entry.createdAt
12593
12760
  };
12594
12761
  }
12762
+ function subject(payload) {
12763
+ if (payload?.kind === "coinless") {
12764
+ const tokenId = payload.direct?.[0];
12765
+ return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
12766
+ }
12767
+ const coin = payload;
12768
+ return {
12769
+ coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
12770
+ amount: typeof coin?.amount === "string" ? coin.amount : ""
12771
+ };
12772
+ }
12595
12773
  function journalOnlyRow(transferId, entries) {
12596
12774
  const certified = distinctOpCount(entries);
12597
12775
  return {
@@ -12621,6 +12799,160 @@ function distinctOpCount(entries) {
12621
12799
  return new Set(entries.map((entry) => entry.opIndex)).size;
12622
12800
  }
12623
12801
 
12802
+ // modules/payments-v2/inventory/token-data.ts
12803
+ async function readTokenData(deps, tokenId) {
12804
+ if (deps.view.stateHashOf(tokenId) === void 0) {
12805
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
12806
+ }
12807
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
12808
+ if (bytes === void 0) {
12809
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
12810
+ }
12811
+ return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
12812
+ }
12813
+
12814
+ // modules/payments-v2/machine/payload.ts
12815
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
12816
+ return {
12817
+ v: 2,
12818
+ kind: "coin",
12819
+ recipient: recipientPubkey,
12820
+ coinId: request.coinId,
12821
+ amount: request.amount,
12822
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
12823
+ direct: [...spend.plan.direct],
12824
+ ...spend.plan.split !== void 0 ? {
12825
+ split: {
12826
+ tokenId: spend.plan.split.tokenId,
12827
+ splitAmount: spend.plan.split.splitAmount.toString(),
12828
+ remainderAmount: spend.plan.split.remainderAmount.toString()
12829
+ }
12830
+ } : {},
12831
+ spentStates
12832
+ };
12833
+ }
12834
+ function messageOf(err) {
12835
+ return err instanceof Error ? err.message : String(err);
12836
+ }
12837
+ function buildCoinlessPayload(recipientPubkey, request, spentStates) {
12838
+ return {
12839
+ v: 2,
12840
+ kind: "coinless",
12841
+ recipient: recipientPubkey,
12842
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
12843
+ direct: [request.tokenId],
12844
+ spentStates
12845
+ };
12846
+ }
12847
+
12848
+ // modules/payments-v2/mint-params.ts
12849
+ function mintParams(recipientPubkey, coinId, amount) {
12850
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12851
+ }
12852
+
12853
+ // modules/payments-v2/mint.ts
12854
+ function runMintUnderJournal(deps, input) {
12855
+ return mintUnderJournal(deps, input);
12856
+ }
12857
+ async function mintUnderJournal(deps, input) {
12858
+ const { mintId, coinId, amount } = input;
12859
+ const engine = deps.engine;
12860
+ const entry = {
12861
+ mintId,
12862
+ coinId,
12863
+ amount: amount.toString(),
12864
+ tokenId: "",
12865
+ createdAt: deps.now()
12866
+ };
12867
+ await deps.mintJournal.upsert(entry);
12868
+ let token;
12869
+ try {
12870
+ token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
12871
+ } catch (err) {
12872
+ deps.armHeartbeat();
12873
+ return { success: false, error: messageOf(err) };
12874
+ }
12875
+ if (token.blob.tokenId !== entry.tokenId) {
12876
+ await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
12877
+ }
12878
+ try {
12879
+ await finalizeMint(deps, mintId, token, coinId, amount.toString());
12880
+ } catch (err) {
12881
+ deps.armHeartbeat();
12882
+ return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
12883
+ }
12884
+ return { success: true, tokenId: token.blob.tokenId };
12885
+ }
12886
+ async function finalizeMint(deps, mintId, token, coinId, amount) {
12887
+ const engine = deps.engine;
12888
+ const bytes = token.blob.token;
12889
+ const digest = bytesToHex3(sha256(bytes));
12890
+ const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
12891
+ const key = keys.get(digest);
12892
+ if (key === void 0) {
12893
+ throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
12894
+ }
12895
+ await deps.storagePort.applyDelta({
12896
+ transferId: mintId,
12897
+ spent: [],
12898
+ added: [{ tokenId: token.blob.tokenId, key }]
12899
+ });
12900
+ await deps.recordMint({
12901
+ tokenId: token.blob.tokenId,
12902
+ assets: [{ coinId, amount }]
12903
+ });
12904
+ await deps.mintJournal.removeByKey(mintId);
12905
+ deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
12906
+ deps.refreshView();
12907
+ }
12908
+
12909
+ // modules/payments-v2/send-coinless.ts
12910
+ async function materializeCoinlessSpend(deps, input) {
12911
+ const { engine } = deps;
12912
+ const tokenId = input.request.tokenId;
12913
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
12914
+ if (bytes === void 0) {
12915
+ throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
12916
+ }
12917
+ const token = await engine.decodeToken({ tokenId, token: bytes });
12918
+ if (!isCoinlessEnvelope(token.valueEnvelope)) {
12919
+ throw new SphereError(
12920
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use send()`,
12921
+ "VALIDATION_ERROR"
12922
+ );
12923
+ }
12924
+ const keys = await engine.deliveryKeys(bytes);
12925
+ const payload = buildCoinlessPayload(input.recipientPubkey, input.request, {
12926
+ [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
12927
+ });
12928
+ const plan = {
12929
+ transferId: input.transferId,
12930
+ recipientPubkey: input.recipientPubkey,
12931
+ payload,
12932
+ ops: buildOps(payload),
12933
+ sources: /* @__PURE__ */ new Map([[tokenId, token]]),
12934
+ ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
12935
+ };
12936
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens: [] };
12937
+ }
12938
+
12939
+ // modules/payments-v2/send-errors.ts
12940
+ function partialize(err, run) {
12941
+ if (run.delivered.length === 0) return err;
12942
+ return new PartialSendConflictError(
12943
+ "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.",
12944
+ run.firstPartialId ?? "",
12945
+ [...run.delivered],
12946
+ run.amount,
12947
+ err
12948
+ );
12949
+ }
12950
+ function stampTransferId(err, transferId) {
12951
+ if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
12952
+ err.transferId = transferId;
12953
+ }
12954
+ }
12955
+
12624
12956
  // modules/payments-v2/recipient.ts
12625
12957
  var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
12626
12958
  async function requireSameNetworkRecipient(deps, identifier) {
@@ -12669,11 +13001,6 @@ async function splitOpIndex(deps, envelope) {
12669
13001
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
12670
13002
  }
12671
13003
 
12672
- // modules/payments-v2/mint-params.ts
12673
- function mintParams(recipientPubkey, coinId, amount) {
12674
- return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12675
- }
12676
-
12677
13004
  // modules/payments-v2/prewarm-cache.ts
12678
13005
  var PrewarmCache = class _PrewarmCache {
12679
13006
  entries = /* @__PURE__ */ new Map();
@@ -12847,30 +13174,15 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
12847
13174
  });
12848
13175
  }
12849
13176
 
12850
- // modules/payments-v2/machine/payload.ts
12851
- function buildPayload(recipientPubkey, request, spend, spentStates) {
12852
- return {
12853
- v: 2,
12854
- recipient: recipientPubkey,
12855
- coinId: request.coinId,
12856
- amount: request.amount,
12857
- ...request.memo !== void 0 ? { memo: request.memo } : {},
12858
- direct: [...spend.plan.direct],
12859
- ...spend.plan.split !== void 0 ? {
12860
- split: {
12861
- tokenId: spend.plan.split.tokenId,
12862
- splitAmount: spend.plan.split.splitAmount.toString(),
12863
- remainderAmount: spend.plan.split.remainderAmount.toString()
12864
- }
12865
- } : {},
12866
- spentStates
12867
- };
13177
+ // modules/payments-v2/history/History.ts
13178
+ function wireAssets(assets) {
13179
+ return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
12868
13180
  }
12869
- function messageOf(err) {
12870
- return err instanceof Error ? err.message : String(err);
13181
+ function isPermanentReject(err) {
13182
+ const status = err?.status;
13183
+ if (typeof status !== "number") return false;
13184
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
12871
13185
  }
12872
-
12873
- // modules/payments-v2/history/History.ts
12874
13186
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
12875
13187
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
12876
13188
  var History = class {
@@ -12895,7 +13207,7 @@ var History = class {
12895
13207
  dedupKey: input.transferId,
12896
13208
  id: (this.deps.newId ?? randomUUID)(),
12897
13209
  type: "SENT",
12898
- assets: [{ coinId: input.coinId, amount: input.amount }],
13210
+ assets: wireAssets(input.assets),
12899
13211
  ts: this.ts(input.timestamp),
12900
13212
  transferId: input.transferId,
12901
13213
  ...this.wireTokenId(input.tokenId),
@@ -12908,7 +13220,7 @@ var History = class {
12908
13220
  dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
12909
13221
  id: (this.deps.newId ?? randomUUID)(),
12910
13222
  type: "RECEIVED",
12911
- assets: [{ coinId: input.coinId, amount: input.amount }],
13223
+ assets: wireAssets(input.assets),
12912
13224
  ts: this.ts(input.timestamp),
12913
13225
  ...input.transferId !== void 0 ? { transferId: input.transferId } : {},
12914
13226
  ...this.wireTokenId(input.tokenId),
@@ -12921,7 +13233,7 @@ var History = class {
12921
13233
  dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
12922
13234
  id: (this.deps.newId ?? randomUUID)(),
12923
13235
  type: "MINT",
12924
- assets: [{ coinId: input.coinId, amount: input.amount }],
13236
+ assets: wireAssets(input.assets),
12925
13237
  ts: this.ts(input.timestamp),
12926
13238
  ...this.wireTokenId(input.tokenId)
12927
13239
  }));
@@ -12932,7 +13244,10 @@ var History = class {
12932
13244
  try {
12933
13245
  await this.deps.client.postHistory([record]);
12934
13246
  } catch (err) {
12935
- this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
13247
+ this.deps.log?.(
13248
+ isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
13249
+ err
13250
+ );
12936
13251
  return;
12937
13252
  }
12938
13253
  this.deps.emit("history:updated", this.toEntry(record));
@@ -12999,6 +13314,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
12999
13314
  function stateKey(tokenId, stateHash) {
13000
13315
  return `${tokenId}:${stateHash}`;
13001
13316
  }
13317
+ function unchanged(prev, item, tokenType) {
13318
+ return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
13319
+ }
13320
+ function isCoinless(item, prev) {
13321
+ if (item.status !== "active") return prev?.coinless ?? false;
13322
+ return (item.assets?.length ?? 0) === 0;
13323
+ }
13002
13324
  var InventoryView = class {
13003
13325
  constructor(deps) {
13004
13326
  this.deps = deps;
@@ -13092,14 +13414,26 @@ var InventoryView = class {
13092
13414
  pool(coinId) {
13093
13415
  const out = [];
13094
13416
  for (const [tokenId, entry] of this.mirror) {
13095
- if (entry.status !== "active") continue;
13096
- if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
13097
- if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
13417
+ if (!this.isSpendable(tokenId, entry)) continue;
13098
13418
  const asset = entry.assets.find((a) => a.coinId === coinId);
13099
13419
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
13100
13420
  }
13101
13421
  return out;
13102
13422
  }
13423
+ /**
13424
+ * The eligibility gates every spend shares, coin or token — ONE definition, so a
13425
+ * single probe covers both verbs and neither can drift from the other.
13426
+ */
13427
+ isSpendable(tokenId, entry) {
13428
+ if (entry.status !== "active") return false;
13429
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13430
+ return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13431
+ }
13432
+ /** #777: is this NAMED token a spendable COINLESS holding? Never a coin source. */
13433
+ spendableCoinless(tokenId) {
13434
+ const entry = this.mirror.get(tokenId);
13435
+ return entry !== void 0 && entry.coinless && this.isSpendable(tokenId, entry);
13436
+ }
13103
13437
  /**
13104
13438
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
13105
13439
  * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
@@ -13125,6 +13459,29 @@ var InventoryView = class {
13125
13459
  }
13126
13460
  return out;
13127
13461
  }
13462
+ /**
13463
+ * Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
13464
+ * exactly when it is not, so no mirror row can appear in both reads.
13465
+ */
13466
+ coinless(registry) {
13467
+ const out = [];
13468
+ for (const [tokenId, entry] of this.mirror) {
13469
+ if (entry.status !== "active" || !entry.coinless) continue;
13470
+ const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
13471
+ out.push({
13472
+ tokenId,
13473
+ ...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
13474
+ ...meta ? { name: meta.name } : {},
13475
+ ...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
13476
+ stateHash: entry.stateHash,
13477
+ transferring: this.held(tokenId),
13478
+ ...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
13479
+ createdAt: entry.createdAt,
13480
+ updatedAt: entry.updatedAt
13481
+ });
13482
+ }
13483
+ return out;
13484
+ }
13128
13485
  async assets(registry, price) {
13129
13486
  const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
13130
13487
  if (!price || raw.length === 0) return raw;
@@ -13196,14 +13553,19 @@ var InventoryView = class {
13196
13553
  applyOne(item, now) {
13197
13554
  const prev = this.mirror.get(item.tokenId);
13198
13555
  if (prev && item.seq < prev.seq) return false;
13199
- if (prev && prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash) {
13200
- return false;
13201
- }
13556
+ const tokenType = item.tokenType ?? prev?.tokenType;
13557
+ if (prev && unchanged(prev, item, tokenType)) return false;
13202
13558
  this.mirror.set(item.tokenId, {
13203
13559
  stateHash: item.stateHash,
13204
13560
  seq: item.seq,
13205
13561
  status: item.status,
13206
- assets: item.assets ?? prev?.assets ?? [],
13562
+ // ONLY a tombstone inherits: it omits assets for an unrelated reason and
13563
+ // recoverRemoved must know the amount it restores. An ACTIVE row that omits
13564
+ // them is stating coinlessness, and inheriting there would keep stale assets
13565
+ // in tokens() while `coinless` is true — the row in BOTH reads (§16).
13566
+ assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
13567
+ coinless: isCoinless(item, prev),
13568
+ ...tokenType !== void 0 ? { tokenType } : {},
13207
13569
  createdAt: prev?.createdAt ?? now,
13208
13570
  updatedAt: now
13209
13571
  });
@@ -13439,7 +13801,12 @@ async function screen(deps, engine, entry) {
13439
13801
  }
13440
13802
  return {
13441
13803
  kind: "accept",
13442
- record: { tokenId: keys.tokenId, stateHash: keys.stateHash, assets: toAssetAmounts(token) }
13804
+ record: {
13805
+ tokenId: keys.tokenId,
13806
+ stateHash: keys.stateHash,
13807
+ assets: toAssetAmounts(token),
13808
+ ...isCoinlessEnvelope2(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
13809
+ }
13443
13810
  };
13444
13811
  }
13445
13812
  async function announce(deps, entry, record) {
@@ -13450,6 +13817,7 @@ async function announce(deps, entry, record) {
13450
13817
  tokenId: record.tokenId,
13451
13818
  stateHash: record.stateHash,
13452
13819
  assets: record.assets,
13820
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
13453
13821
  ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
13454
13822
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13455
13823
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
@@ -13463,6 +13831,19 @@ async function announce(deps, entry, record) {
13463
13831
  senderPubkey: entry.senderPubkey ?? "",
13464
13832
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13465
13833
  tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
13834
+ // #777: named here rather than mapped from assets, which announced an EMPTY list.
13835
+ ...record.tokenType !== void 0 ? {
13836
+ coinless: [
13837
+ {
13838
+ tokenId: record.tokenId,
13839
+ tokenType: record.tokenType,
13840
+ stateHash: record.stateHash,
13841
+ transferring: false,
13842
+ createdAt: receivedAt,
13843
+ updatedAt: receivedAt
13844
+ }
13845
+ ]
13846
+ } : {},
13466
13847
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
13467
13848
  receivedAt
13468
13849
  };
@@ -13561,6 +13942,9 @@ function claimAck(entry) {
13561
13942
  function rejectAck(entry, reason) {
13562
13943
  return { deliveryId: entry.deliveryId, disposition: "rejected", reason, cursor: entry.cursor };
13563
13944
  }
13945
+ function isCoinlessEnvelope2(envelope) {
13946
+ return envelope.startsWith("none_");
13947
+ }
13564
13948
  function toAssetAmounts(token) {
13565
13949
  return (token.value?.assets ?? []).map((asset) => ({
13566
13950
  coinId: asset.coinId,
@@ -14128,6 +14512,37 @@ var SpendQueue = class {
14128
14512
  }
14129
14513
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
14130
14514
  }
14515
+ /**
14516
+ * #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
14517
+ * for. A coin plan waits because some OTHER combination may free up; a named
14518
+ * token is either free now or held by a reservation that only its own transfer
14519
+ * can release, so queueing would block until a timeout on a spend that cannot
14520
+ * become possible.
14521
+ */
14522
+ planCoinless(reservationId, tokenId) {
14523
+ if (this.destroyed) {
14524
+ throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
14525
+ }
14526
+ const unproven = this.deps.ledger.unprovenReason();
14527
+ if (unproven !== null) {
14528
+ throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
14529
+ }
14530
+ if (this.deps.spendableCoinless?.(tokenId) !== true) {
14531
+ throw new SphereError(
14532
+ `Token ${tokenId} is not a spendable coinless holding`,
14533
+ "VALIDATION_ERROR"
14534
+ );
14535
+ }
14536
+ const holder = this.deps.ledger.holderOf(tokenId);
14537
+ if (holder !== void 0) {
14538
+ throw new SphereError(
14539
+ `Token ${tokenId} is already reserved by transfer ${holder}`,
14540
+ "VALIDATION_ERROR"
14541
+ );
14542
+ }
14543
+ this.deps.ledger.reserve(reservationId, [{ tokenId }]);
14544
+ return { reservationId, plan: { direct: [tokenId] } };
14545
+ }
14131
14546
  /**
14132
14547
  * The sources `plan()` WOULD pick right now, reserving nothing and queueing
14133
14548
  * nothing — a read-only probe used to warm blobs while the user is still
@@ -14317,6 +14732,7 @@ function composeFacadeParts(deps, hooks) {
14317
14732
  const queue = new SpendQueue({
14318
14733
  ledger,
14319
14734
  getPool: (coinId) => view.pool(coinId),
14735
+ spendableCoinless: (tokenId) => view.spendableCoinless(tokenId),
14320
14736
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
14321
14737
  });
14322
14738
  const historyStore = new History({
@@ -14400,10 +14816,9 @@ function buildMachineDeps(deps, hooks, historyStore) {
14400
14816
  recordHistory: async ({ transferId, payload, committedAmount }) => {
14401
14817
  await historyStore.recordSent({
14402
14818
  transferId,
14403
- coinId: payload.coinId,
14404
- // §5.9: the SETTLED amount (machine-computed from the certified
14405
- // recipient blobs), never payload.amount — the plan.
14406
- amount: committedAmount,
14819
+ // §5.9: the SETTLED amount, never payload.amount — the plan. A token
14820
+ // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
14821
+ ...payload.kind === "coinless" ? { assets: [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
14407
14822
  recipientPubkey: payload.recipient,
14408
14823
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
14409
14824
  });
@@ -14438,12 +14853,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
14438
14853
  });
14439
14854
  }
14440
14855
  async function recordReceived(historyStore, record) {
14441
- const first = record.assets[0];
14442
14856
  await historyStore.recordReceived({
14443
14857
  tokenId: record.tokenId,
14444
14858
  stateHash: record.stateHash,
14445
- coinId: first?.coinId ?? "",
14446
- amount: first?.amount ?? "0",
14859
+ assets: record.assets,
14447
14860
  ...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
14448
14861
  ...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
14449
14862
  ...record.memo !== void 0 ? { memo: record.memo } : {},
@@ -14591,6 +15004,13 @@ var PaymentsFacade = class {
14591
15004
  tokens(filter) {
14592
15005
  return this.view.tokens(this.deps.registry, filter);
14593
15006
  }
15007
+ coinless() {
15008
+ return this.view.coinless(this.deps.registry);
15009
+ }
15010
+ tokenData(tokenId) {
15011
+ const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
15012
+ return readTokenData(deps, tokenId);
15013
+ }
14594
15014
  history(page) {
14595
15015
  return this.historyStore.page(page ?? {});
14596
15016
  }
@@ -14615,6 +15035,9 @@ var PaymentsFacade = class {
14615
15035
  send(request) {
14616
15036
  return this.track(this.sendOutcome(request));
14617
15037
  }
15038
+ sendCoinless(request) {
15039
+ return this.track(this.sendCoinlessOutcome(request));
15040
+ }
14618
15041
  async receive() {
14619
15042
  const transfers = await this.track(this.receiveLoop.drainOnce());
14620
15043
  return { transfers };
@@ -14645,12 +15068,32 @@ var PaymentsFacade = class {
14645
15068
  nowMs() {
14646
15069
  return (this.deps.now ?? Date.now)();
14647
15070
  }
15071
+ mintDeps() {
15072
+ return {
15073
+ engine: this.engine(),
15074
+ mintJournal: this.machineStores.mintJournal,
15075
+ storagePort: this.deps.storagePort,
15076
+ recordMint: (input) => this.historyStore.recordMint(input),
15077
+ armHeartbeat: () => this.heartbeat.arm(),
15078
+ noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
15079
+ refreshView: () => this.trackTail(this.view.delta()),
15080
+ ownPubkeyBytes: this.ownPubkeyBytes,
15081
+ now: () => this.nowMs()
15082
+ };
15083
+ }
14648
15084
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
14649
15085
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
14650
15086
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
14651
- async sendOutcome(request) {
15087
+ sendOutcome(request) {
15088
+ return this.runJob({ kind: "coin", request }, request.amount);
15089
+ }
15090
+ /** A token spend has no amount; '0' keeps the shortfall arithmetic total-free. */
15091
+ sendCoinlessOutcome(request) {
15092
+ return this.runJob({ kind: "coinless", request }, "0");
15093
+ }
15094
+ async runJob(job, amount) {
14652
15095
  const run = {
14653
- amount: request.amount,
15096
+ amount,
14654
15097
  delivered: [],
14655
15098
  sentTokens: [],
14656
15099
  tokenTransfers: [],
@@ -14660,7 +15103,7 @@ var PaymentsFacade = class {
14660
15103
  lastTransferId: ""
14661
15104
  };
14662
15105
  try {
14663
- return await this.sendWithPolicy(request, run);
15106
+ return await this.sendWithPolicy(job, run);
14664
15107
  } catch (err) {
14665
15108
  this.emitCleanFailure(err, run.lastTransferId);
14666
15109
  throw err;
@@ -14680,19 +15123,19 @@ var PaymentsFacade = class {
14680
15123
  };
14681
15124
  this.deps.emit("transfer:updated", failed);
14682
15125
  }
14683
- async sendWithPolicy(request, run) {
14684
- const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
15126
+ async sendWithPolicy(job, run) {
15127
+ const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
14685
15128
  for (let attempt = 0; ; attempt++) {
14686
15129
  let ctx;
14687
15130
  try {
14688
- ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
15131
+ ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
14689
15132
  } catch (err) {
14690
- throw this.partialize(err, run);
15133
+ throw partialize(err, run);
14691
15134
  }
14692
15135
  const disposition = await this.runAttempt(ctx);
14693
- if (disposition.kind === "rethrow") throw this.partialize(disposition.error, run);
15136
+ if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
14694
15137
  if (disposition.kind === "retry-full") {
14695
- if (attempt >= MAX_RESELECT) throw this.partialize(disposition.error, run);
15138
+ if (job.kind === "coinless" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
14696
15139
  continue;
14697
15140
  }
14698
15141
  if (disposition.kind === "success") {
@@ -14748,7 +15191,7 @@ var PaymentsFacade = class {
14748
15191
  async disposeFailedAttempt(ctx, err) {
14749
15192
  if (isPossiblyCommittedSendOutcome(err)) {
14750
15193
  this.settleKeepOpen(ctx);
14751
- this.stampTransferId(err, ctx.transferId);
15194
+ stampTransferId(err, ctx.transferId);
14752
15195
  return { kind: "rethrow", error: err };
14753
15196
  }
14754
15197
  const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
@@ -14782,7 +15225,7 @@ var PaymentsFacade = class {
14782
15225
  async disposeConvergeFailure(ctx, err) {
14783
15226
  if (isPossiblyCommittedSendOutcome(err)) {
14784
15227
  this.settleKeepOpen(ctx);
14785
- this.stampTransferId(err, ctx.transferId);
15228
+ stampTransferId(err, ctx.transferId);
14786
15229
  return { kind: "rethrow", error: err };
14787
15230
  }
14788
15231
  if (classifyError(err) === "conflict") {
@@ -14793,9 +15236,23 @@ var PaymentsFacade = class {
14793
15236
  }
14794
15237
  return { kind: "rethrow", error: err };
14795
15238
  }
14796
- async planAndMaterialize(recipientPubkey, request, run) {
15239
+ async planAndMaterialize(recipientPubkey, job, run) {
14797
15240
  const transferId = this.newId();
14798
15241
  run.lastTransferId = transferId;
15242
+ if (job.kind === "coinless") {
15243
+ const spend2 = this.queue.planCoinless(transferId, job.request.tokenId);
15244
+ const sourceIds2 = this.markPlanned(transferId, "", spend2);
15245
+ try {
15246
+ return await materializeCoinlessSpend(
15247
+ { engine: this.engine(), storagePort: this.deps.storagePort },
15248
+ { transferId, recipientPubkey, request: job.request, sourceIds: sourceIds2 }
15249
+ );
15250
+ } catch (err) {
15251
+ this.settleFailure(transferId, "", sourceIds2);
15252
+ throw err;
15253
+ }
15254
+ }
15255
+ const request = { ...job.request, amount: run.amount };
14799
15256
  const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
14800
15257
  const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
14801
15258
  const sourceIds = this.markPlanned(transferId, request.coinId, spend);
@@ -14929,22 +15386,6 @@ var PaymentsFacade = class {
14929
15386
  return result;
14930
15387
  }
14931
15388
  /** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
14932
- partialize(err, run) {
14933
- if (run.delivered.length === 0) return err;
14934
- return new PartialSendConflictError(
14935
- "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.",
14936
- run.firstPartialId ?? "",
14937
- run.delivered,
14938
- run.amount,
14939
- err
14940
- );
14941
- }
14942
- /** #441: possibly-committed errors must carry the transferId for the settling journal. */
14943
- stampTransferId(err, transferId) {
14944
- if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
14945
- err.transferId = transferId;
14946
- }
14947
- }
14948
15389
  async softAbort(transferId) {
14949
15390
  try {
14950
15391
  await this.deps.client.abortIntent(transferId);
@@ -14960,57 +15401,11 @@ var PaymentsFacade = class {
14960
15401
  const mintId = this.newId();
14961
15402
  this.activeMoneyOps.add(mintId);
14962
15403
  try {
14963
- return await this.mintUnderJournal(mintId, coinId, amount);
15404
+ return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
14964
15405
  } finally {
14965
15406
  this.activeMoneyOps.delete(mintId);
14966
15407
  }
14967
15408
  }
14968
- async mintUnderJournal(mintId, coinId, amount) {
14969
- const engine = this.engine();
14970
- const entry = {
14971
- mintId,
14972
- coinId,
14973
- amount: amount.toString(),
14974
- tokenId: "",
14975
- createdAt: this.nowMs()
14976
- };
14977
- await this.machineStores.mintJournal.upsert(entry);
14978
- let token;
14979
- try {
14980
- token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
14981
- } catch (err) {
14982
- this.heartbeat.arm();
14983
- return { success: false, error: messageOf(err) };
14984
- }
14985
- if (token.blob.tokenId !== entry.tokenId) {
14986
- await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
14987
- }
14988
- try {
14989
- await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
14990
- } catch (err) {
14991
- this.heartbeat.arm();
14992
- return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
14993
- }
14994
- return { success: true, tokenId: token.blob.tokenId };
14995
- }
14996
- async finalizeMint(engine, mintId, token, coinId, amount) {
14997
- const bytes = token.blob.token;
14998
- const digest = bytesToHex3(sha256(bytes));
14999
- const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
15000
- const key = keys.get(digest);
15001
- if (key === void 0) {
15002
- throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
15003
- }
15004
- await this.deps.storagePort.applyDelta({
15005
- transferId: mintId,
15006
- spent: [],
15007
- added: [{ tokenId: token.blob.tokenId, key }]
15008
- });
15009
- await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
15010
- await this.machineStores.mintJournal.removeByKey(mintId);
15011
- this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
15012
- this.trackTail(this.view.delta());
15013
- }
15014
15409
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
15015
15410
  async replayMints() {
15016
15411
  let resolved = 0;
@@ -15042,7 +15437,7 @@ var PaymentsFacade = class {
15042
15437
  if (token.blob.tokenId !== entry.tokenId) {
15043
15438
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
15044
15439
  }
15045
- await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
15440
+ await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
15046
15441
  return true;
15047
15442
  }
15048
15443
  async tokenInServerInventory(tokenId) {
@@ -16817,7 +17212,8 @@ var Sphere = class _Sphere {
16817
17212
  }
16818
17213
  const progress = options.onProgress;
16819
17214
  logger.debug("Sphere", "Starting import...");
16820
- const needsClear = _Sphere.liveOn(options.storage).length > 0 || await _Sphere.exists(options.storage);
17215
+ const liveHere = _Sphere.liveOn(options.storage).some((s) => s._storage === options.storage);
17216
+ const needsClear = liveHere || await _Sphere.exists(options.storage);
16821
17217
  if (needsClear) {
16822
17218
  progress?.({ step: "clearing", message: "Clearing previous wallet data..." });
16823
17219
  logger.debug("Sphere", "Clearing existing wallet data...");