@unicitylabs/sphere-sdk 0.16.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/connect/index.cjs +11 -5
  2. package/dist/connect/index.cjs.map +1 -1
  3. package/dist/connect/index.d.cts +4 -1
  4. package/dist/connect/index.d.ts +4 -1
  5. package/dist/connect/index.js +11 -5
  6. package/dist/connect/index.js.map +1 -1
  7. package/dist/core/index.cjs +692 -268
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +69 -0
  10. package/dist/core/index.d.ts +69 -0
  11. package/dist/core/index.js +692 -268
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/impl/browser/connect/index.cjs +11 -5
  14. package/dist/impl/browser/connect/index.cjs.map +1 -1
  15. package/dist/impl/browser/connect/index.d.cts +3 -1
  16. package/dist/impl/browser/connect/index.d.ts +3 -1
  17. package/dist/impl/browser/connect/index.js +11 -5
  18. package/dist/impl/browser/connect/index.js.map +1 -1
  19. package/dist/impl/nodejs/connect/index.cjs +10 -4
  20. package/dist/impl/nodejs/connect/index.cjs.map +1 -1
  21. package/dist/impl/nodejs/connect/index.d.cts +1 -1
  22. package/dist/impl/nodejs/connect/index.d.ts +1 -1
  23. package/dist/impl/nodejs/connect/index.js +10 -4
  24. package/dist/impl/nodejs/connect/index.js.map +1 -1
  25. package/dist/impl/nodejs/index.d.cts +11 -0
  26. package/dist/impl/nodejs/index.d.ts +11 -0
  27. package/dist/impl/shared/wallet-api/index.d.cts +11 -0
  28. package/dist/impl/shared/wallet-api/index.d.ts +11 -0
  29. package/dist/impl/wallet-api-v2/index.cjs +2 -0
  30. package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
  31. package/dist/impl/wallet-api-v2/index.d.cts +13 -0
  32. package/dist/impl/wallet-api-v2/index.d.ts +13 -0
  33. package/dist/impl/wallet-api-v2/index.js +2 -0
  34. package/dist/impl/wallet-api-v2/index.js.map +1 -1
  35. package/dist/index.cjs +692 -268
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +63 -1
  38. package/dist/index.d.ts +63 -1
  39. package/dist/index.js +692 -268
  40. package/dist/index.js.map +1 -1
  41. package/dist/modules/payments-v2/index.cjs +767 -398
  42. package/dist/modules/payments-v2/index.cjs.map +1 -1
  43. package/dist/modules/payments-v2/index.d.cts +91 -14
  44. package/dist/modules/payments-v2/index.d.ts +91 -14
  45. package/dist/modules/payments-v2/index.js +767 -398
  46. package/dist/modules/payments-v2/index.js.map +1 -1
  47. package/dist/token-engine/index.cjs +132 -34
  48. package/dist/token-engine/index.cjs.map +1 -1
  49. package/dist/token-engine/index.d.cts +19 -0
  50. package/dist/token-engine/index.d.ts +19 -0
  51. package/dist/token-engine/index.js +132 -34
  52. package/dist/token-engine/index.js.map +1 -1
  53. package/package.json +1 -1
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,17 @@ function createMachineStores(kv) {
11827
11947
  };
11828
11948
  }
11829
11949
 
11950
+ // modules/payments-v2/machine/payload-view.ts
11951
+ function isWholeIntent(p) {
11952
+ return p.kind === "whole" || p.kind === "coinless";
11953
+ }
11954
+ function splitOf(p) {
11955
+ return p.kind === "coin" ? p.split : void 0;
11956
+ }
11957
+ function coinIdOf(p) {
11958
+ return p.kind === "coin" ? p.coinId : void 0;
11959
+ }
11960
+
11830
11961
  // modules/payments-v2/machine/TransferMachine.ts
11831
11962
  var CERTIFY_WIDTH = 20;
11832
11963
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
@@ -12029,13 +12160,14 @@ var TransferMachine = class {
12029
12160
  const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
12030
12161
  return { recipientBlob: finished.blob.token };
12031
12162
  }
12032
- const split = ctx.payload.split;
12163
+ const split = splitOf(ctx.payload);
12164
+ const splitCoinId = coinIdOf(ctx.payload);
12033
12165
  const { outputs } = await engine.split(
12034
12166
  {
12035
12167
  token: source,
12036
12168
  outputs: [
12037
- { recipientPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.splitAmount) },
12038
- { recipientPubkey: this.deps.ownPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.remainderAmount) }
12169
+ { recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
12170
+ { recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
12039
12171
  ]
12040
12172
  },
12041
12173
  opts
@@ -12157,7 +12289,9 @@ var TransferMachine = class {
12157
12289
  const entry = {
12158
12290
  transferId: r.transferId,
12159
12291
  remainingAmount: undelivered.toString(),
12160
- coinId: r.payload.coinId,
12292
+ // '' only on a token-addressed intent, which is one leg: it either lands or
12293
+ // conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
12294
+ coinId: coinIdOf(r.payload) ?? "",
12161
12295
  recipient: r.payload.recipient,
12162
12296
  committedTokenIds: committed.map((o) => o.op.sourceTokenId),
12163
12297
  createdAt: this.deps.now()
@@ -12183,8 +12317,16 @@ var TransferMachine = class {
12183
12317
  }
12184
12318
  async finishBookkeeping(engine, transferId, payload, phase, committed) {
12185
12319
  try {
12186
- const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
12187
- await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
12320
+ const coinId = coinIdOf(payload);
12321
+ const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
12322
+ const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
12323
+ await this.deps.recordHistory?.({
12324
+ transferId,
12325
+ payload,
12326
+ phase,
12327
+ committedAmount,
12328
+ ...committedAssets !== void 0 ? { committedAssets } : {}
12329
+ });
12188
12330
  } catch {
12189
12331
  }
12190
12332
  try {
@@ -12198,6 +12340,18 @@ var TransferMachine = class {
12198
12340
  * a journal-rehydrated resume leg has no source token, but every committed
12199
12341
  * leg carries its recipient blob, so the outcome-derived rule is uniform).
12200
12342
  */
12343
+ /** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
12344
+ async settledAssets(engine, committed) {
12345
+ const total = /* @__PURE__ */ new Map();
12346
+ for (const outcome of committed) {
12347
+ if (outcome.recipientBlob === void 0) continue;
12348
+ const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
12349
+ for (const asset of engine.readValue(token)?.assets ?? []) {
12350
+ total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
12351
+ }
12352
+ }
12353
+ return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
12354
+ }
12201
12355
  async settledAmount(engine, coinId, committed) {
12202
12356
  let total = 0n;
12203
12357
  for (const outcome of committed) {
@@ -12226,8 +12380,11 @@ var TransferMachine = class {
12226
12380
  }
12227
12381
  };
12228
12382
  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;
12383
+ const split = splitOf(payload);
12384
+ if (op.kind === "split") return BigInt(split.splitAmount);
12385
+ const coinId = coinIdOf(payload);
12386
+ if (coinId === void 0) return 0n;
12387
+ return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
12231
12388
  }
12232
12389
 
12233
12390
  // modules/payments-v2/machine/resume.ts
@@ -12334,6 +12491,14 @@ async function runOne(ctx, job, report) {
12334
12491
  report.failed.push(job.transferId);
12335
12492
  return;
12336
12493
  }
12494
+ if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
12495
+ logger.warn(
12496
+ "PaymentsV2",
12497
+ `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
12498
+ );
12499
+ report.failed.push(job.transferId);
12500
+ return;
12501
+ }
12337
12502
  mine.set(op.sourceTokenId, token);
12338
12503
  }
12339
12504
  try {
@@ -12364,6 +12529,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
12364
12529
  }
12365
12530
  report.failed.push(transferId);
12366
12531
  }
12532
+ function validateCoinPayload(c) {
12533
+ if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
12534
+ throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
12535
+ }
12536
+ const s = c.split;
12537
+ if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
12538
+ throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
12539
+ }
12540
+ return { ...c, kind: "coin" };
12541
+ }
12542
+ function validateWholePayload(p) {
12543
+ if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
12544
+ throw new SphereError(
12545
+ "token intent payload must name exactly one source token",
12546
+ "VALIDATION_ERROR"
12547
+ );
12548
+ }
12549
+ if (p.split !== void 0) {
12550
+ throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
12551
+ }
12552
+ return { ...p, kind: "whole", direct: [p.direct[0]] };
12553
+ }
12367
12554
  function validatePayload(raw) {
12368
12555
  const p = raw !== null && typeof raw === "object" ? raw : null;
12369
12556
  if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
@@ -12372,14 +12559,17 @@ function validatePayload(raw) {
12372
12559
  "VALIDATION_ERROR"
12373
12560
  );
12374
12561
  }
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");
12562
+ if (typeof p.recipient !== "string") {
12563
+ throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
12377
12564
  }
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");
12565
+ const kind = p.kind ?? "coin";
12566
+ if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
12567
+ throw new SphereError(
12568
+ `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
12569
+ "VALIDATION_ERROR"
12570
+ );
12381
12571
  }
12382
- return p;
12572
+ return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
12383
12573
  }
12384
12574
 
12385
12575
  // modules/payments-v2/convergence.ts
@@ -12585,13 +12775,24 @@ async function openRow(entry, journal, decryptPayload) {
12585
12775
  transferId: entry.transferId,
12586
12776
  kind: "open",
12587
12777
  recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
12588
- coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
12589
- amount: typeof payload?.amount === "string" ? payload.amount : "",
12778
+ ...subject(payload),
12590
12779
  legs: { certified, total: Math.max(total, certified) },
12591
12780
  deliveryPending: journal.length > 0,
12592
12781
  createdAt: entry.createdAt
12593
12782
  };
12594
12783
  }
12784
+ function subject(payload) {
12785
+ const rawKind = payload?.kind;
12786
+ if (rawKind === "whole" || rawKind === "coinless") {
12787
+ const tokenId = payload?.direct?.[0];
12788
+ return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
12789
+ }
12790
+ const coin = payload;
12791
+ return {
12792
+ coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
12793
+ amount: typeof coin?.amount === "string" ? coin.amount : ""
12794
+ };
12795
+ }
12595
12796
  function journalOnlyRow(transferId, entries) {
12596
12797
  const certified = distinctOpCount(entries);
12597
12798
  return {
@@ -12621,6 +12822,263 @@ function distinctOpCount(entries) {
12621
12822
  return new Set(entries.map((entry) => entry.opIndex)).size;
12622
12823
  }
12623
12824
 
12825
+ // modules/payments-v2/inventory/token-data.ts
12826
+ async function readTokenData(deps, tokenId) {
12827
+ if (deps.view.stateHashOf(tokenId) === void 0) {
12828
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
12829
+ }
12830
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
12831
+ if (bytes === void 0) {
12832
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
12833
+ }
12834
+ return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
12835
+ }
12836
+
12837
+ // modules/payments-v2/machine/payload.ts
12838
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
12839
+ return {
12840
+ v: 2,
12841
+ kind: "coin",
12842
+ recipient: recipientPubkey,
12843
+ coinId: request.coinId,
12844
+ amount: request.amount,
12845
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
12846
+ direct: [...spend.plan.direct],
12847
+ ...spend.plan.split !== void 0 ? {
12848
+ split: {
12849
+ tokenId: spend.plan.split.tokenId,
12850
+ splitAmount: spend.plan.split.splitAmount.toString(),
12851
+ remainderAmount: spend.plan.split.remainderAmount.toString()
12852
+ }
12853
+ } : {},
12854
+ spentStates
12855
+ };
12856
+ }
12857
+ function messageOf(err) {
12858
+ return err instanceof Error ? err.message : String(err);
12859
+ }
12860
+ function buildWholePayload(recipientPubkey, request, spentStates) {
12861
+ return {
12862
+ v: 2,
12863
+ kind: "whole",
12864
+ recipient: recipientPubkey,
12865
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
12866
+ direct: [request.tokenId],
12867
+ spentStates
12868
+ };
12869
+ }
12870
+
12871
+ // modules/payments-v2/mint-params.ts
12872
+ function mintParams(recipientPubkey, coinId, amount) {
12873
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12874
+ }
12875
+
12876
+ // modules/payments-v2/mint.ts
12877
+ function runMintUnderJournal(deps, input) {
12878
+ return mintUnderJournal(deps, input);
12879
+ }
12880
+ async function mintUnderJournal(deps, input) {
12881
+ const { mintId, coinId, amount } = input;
12882
+ const engine = deps.engine;
12883
+ const entry = {
12884
+ mintId,
12885
+ coinId,
12886
+ amount: amount.toString(),
12887
+ tokenId: "",
12888
+ createdAt: deps.now()
12889
+ };
12890
+ await deps.mintJournal.upsert(entry);
12891
+ let token;
12892
+ try {
12893
+ token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
12894
+ } catch (err) {
12895
+ deps.armHeartbeat();
12896
+ return { success: false, error: messageOf(err) };
12897
+ }
12898
+ if (token.blob.tokenId !== entry.tokenId) {
12899
+ await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
12900
+ }
12901
+ try {
12902
+ await finalizeMint(deps, mintId, token, coinId, amount.toString());
12903
+ } catch (err) {
12904
+ deps.armHeartbeat();
12905
+ return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
12906
+ }
12907
+ return { success: true, tokenId: token.blob.tokenId };
12908
+ }
12909
+ async function finalizeMint(deps, mintId, token, coinId, amount) {
12910
+ const engine = deps.engine;
12911
+ const bytes = token.blob.token;
12912
+ const digest = bytesToHex3(sha256(bytes));
12913
+ const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
12914
+ const key = keys.get(digest);
12915
+ if (key === void 0) {
12916
+ throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
12917
+ }
12918
+ await deps.storagePort.applyDelta({
12919
+ transferId: mintId,
12920
+ spent: [],
12921
+ added: [{ tokenId: token.blob.tokenId, key }]
12922
+ });
12923
+ await deps.recordMint({
12924
+ tokenId: token.blob.tokenId,
12925
+ assets: [{ coinId, amount }]
12926
+ });
12927
+ await deps.mintJournal.removeByKey(mintId);
12928
+ deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
12929
+ deps.refreshView();
12930
+ }
12931
+
12932
+ // modules/payments-v2/inventory/presentation.ts
12933
+ function toToken(tokenId, snapshot, asset, registry, flags) {
12934
+ const iconUrl = registry.getIconUrl(asset.coinId);
12935
+ return {
12936
+ id: tokenId,
12937
+ coinId: asset.coinId,
12938
+ symbol: registry.getSymbol(asset.coinId),
12939
+ name: registry.getName(asset.coinId),
12940
+ decimals: registry.getDecimals(asset.coinId),
12941
+ ...iconUrl !== null ? { iconUrl } : {},
12942
+ amount: asset.amount,
12943
+ status: flags.transferring ? "transferring" : "confirmed",
12944
+ createdAt: snapshot.createdAt,
12945
+ updatedAt: snapshot.updatedAt,
12946
+ lazy: true,
12947
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12948
+ };
12949
+ }
12950
+ function aggregateAssets(entries, isTransferring, registry) {
12951
+ const groups = /* @__PURE__ */ new Map();
12952
+ for (const [tokenId, entry] of entries) {
12953
+ const moving = isTransferring(tokenId);
12954
+ for (const asset of entry.assets) {
12955
+ let group = groups.get(asset.coinId);
12956
+ if (!group) {
12957
+ group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12958
+ groups.set(asset.coinId, group);
12959
+ }
12960
+ if (moving) {
12961
+ group.transferring += BigInt(asset.amount);
12962
+ group.transferringCount += 1;
12963
+ } else {
12964
+ group.total += BigInt(asset.amount);
12965
+ group.count += 1;
12966
+ }
12967
+ }
12968
+ }
12969
+ return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12970
+ }
12971
+ function toAsset(coinId, totals, registry) {
12972
+ const iconUrl = registry.getIconUrl(coinId);
12973
+ return {
12974
+ coinId,
12975
+ symbol: registry.getSymbol(coinId),
12976
+ name: registry.getName(coinId),
12977
+ decimals: registry.getDecimals(coinId),
12978
+ ...iconUrl !== null ? { iconUrl } : {},
12979
+ totalAmount: totals.total.toString(),
12980
+ tokenCount: totals.count,
12981
+ confirmedAmount: totals.total.toString(),
12982
+ unconfirmedAmount: "0",
12983
+ confirmedTokenCount: totals.count,
12984
+ unconfirmedTokenCount: 0,
12985
+ transferringTokenCount: totals.transferringCount,
12986
+ transferringAmount: totals.transferring.toString(),
12987
+ priceUsd: null,
12988
+ priceEur: null,
12989
+ change24h: null,
12990
+ fiatValueUsd: null,
12991
+ fiatValueEur: null
12992
+ };
12993
+ }
12994
+ function priceIdOf(registry, coinId) {
12995
+ return registry.getName(coinId).toLowerCase();
12996
+ }
12997
+ async function withPrices(raw, registry, price) {
12998
+ try {
12999
+ const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
13000
+ const quotes = await price.getPrices(ids);
13001
+ return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13002
+ } catch {
13003
+ return raw;
13004
+ }
13005
+ }
13006
+ function priceOne(asset, quote) {
13007
+ if (!quote) return asset;
13008
+ const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
13009
+ return {
13010
+ ...asset,
13011
+ priceUsd: quote.priceUsd,
13012
+ priceEur: quote.priceEur ?? null,
13013
+ change24h: quote.change24h ?? null,
13014
+ fiatValueUsd: human * quote.priceUsd,
13015
+ fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
13016
+ };
13017
+ }
13018
+ function transferringToken(tokenId, coinId, amount, registry, now) {
13019
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
13020
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
13021
+ transferring: true,
13022
+ suspectedSpent: false
13023
+ });
13024
+ }
13025
+
13026
+ // modules/payments-v2/send-whole.ts
13027
+ async function materializeWholeSpend(deps, input) {
13028
+ const { engine } = deps;
13029
+ const tokenId = input.request.tokenId;
13030
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
13031
+ if (bytes === void 0) {
13032
+ throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
13033
+ }
13034
+ const token = await engine.decodeToken({ tokenId, token: bytes });
13035
+ if (token.valueEnvelope === "bare_collection") {
13036
+ throw new SphereError(
13037
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
13038
+ "VALIDATION_ERROR"
13039
+ );
13040
+ }
13041
+ if (input.requireCoinless && token.value !== null) {
13042
+ throw new SphereError(
13043
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
13044
+ "VALIDATION_ERROR"
13045
+ );
13046
+ }
13047
+ const keys = await engine.deliveryKeys(bytes);
13048
+ const payload = buildWholePayload(input.recipientPubkey, input.request, {
13049
+ [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
13050
+ });
13051
+ const plan = {
13052
+ transferId: input.transferId,
13053
+ recipientPubkey: input.recipientPubkey,
13054
+ payload,
13055
+ ops: buildOps(payload),
13056
+ sources: /* @__PURE__ */ new Map([[tokenId, token]]),
13057
+ ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
13058
+ };
13059
+ const sourceTokens = (token.value?.assets ?? []).map(
13060
+ (asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
13061
+ );
13062
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
13063
+ }
13064
+
13065
+ // modules/payments-v2/send-errors.ts
13066
+ function partialize(err, run) {
13067
+ if (run.delivered.length === 0) return err;
13068
+ return new PartialSendConflictError(
13069
+ "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.",
13070
+ run.firstPartialId ?? "",
13071
+ [...run.delivered],
13072
+ run.amount,
13073
+ err
13074
+ );
13075
+ }
13076
+ function stampTransferId(err, transferId) {
13077
+ if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
13078
+ err.transferId = transferId;
13079
+ }
13080
+ }
13081
+
12624
13082
  // modules/payments-v2/recipient.ts
12625
13083
  var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
12626
13084
  async function requireSameNetworkRecipient(deps, identifier) {
@@ -12669,11 +13127,6 @@ async function splitOpIndex(deps, envelope) {
12669
13127
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
12670
13128
  }
12671
13129
 
12672
- // modules/payments-v2/mint-params.ts
12673
- function mintParams(recipientPubkey, coinId, amount) {
12674
- return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12675
- }
12676
-
12677
13130
  // modules/payments-v2/prewarm-cache.ts
12678
13131
  var PrewarmCache = class _PrewarmCache {
12679
13132
  entries = /* @__PURE__ */ new Map();
@@ -12753,124 +13206,15 @@ async function takeSourceBlobs(deps, sourceIds) {
12753
13206
  return blobs;
12754
13207
  }
12755
13208
 
12756
- // modules/payments-v2/inventory/presentation.ts
12757
- function toToken(tokenId, snapshot, asset, registry, flags) {
12758
- const iconUrl = registry.getIconUrl(asset.coinId);
12759
- return {
12760
- id: tokenId,
12761
- coinId: asset.coinId,
12762
- symbol: registry.getSymbol(asset.coinId),
12763
- name: registry.getName(asset.coinId),
12764
- decimals: registry.getDecimals(asset.coinId),
12765
- ...iconUrl !== null ? { iconUrl } : {},
12766
- amount: asset.amount,
12767
- status: flags.transferring ? "transferring" : "confirmed",
12768
- createdAt: snapshot.createdAt,
12769
- updatedAt: snapshot.updatedAt,
12770
- lazy: true,
12771
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12772
- };
12773
- }
12774
- function aggregateAssets(entries, isTransferring, registry) {
12775
- const groups = /* @__PURE__ */ new Map();
12776
- for (const [tokenId, entry] of entries) {
12777
- const moving = isTransferring(tokenId);
12778
- for (const asset of entry.assets) {
12779
- let group = groups.get(asset.coinId);
12780
- if (!group) {
12781
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12782
- groups.set(asset.coinId, group);
12783
- }
12784
- if (moving) {
12785
- group.transferring += BigInt(asset.amount);
12786
- group.transferringCount += 1;
12787
- } else {
12788
- group.total += BigInt(asset.amount);
12789
- group.count += 1;
12790
- }
12791
- }
12792
- }
12793
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12794
- }
12795
- function toAsset(coinId, totals, registry) {
12796
- const iconUrl = registry.getIconUrl(coinId);
12797
- return {
12798
- coinId,
12799
- symbol: registry.getSymbol(coinId),
12800
- name: registry.getName(coinId),
12801
- decimals: registry.getDecimals(coinId),
12802
- ...iconUrl !== null ? { iconUrl } : {},
12803
- totalAmount: totals.total.toString(),
12804
- tokenCount: totals.count,
12805
- confirmedAmount: totals.total.toString(),
12806
- unconfirmedAmount: "0",
12807
- confirmedTokenCount: totals.count,
12808
- unconfirmedTokenCount: 0,
12809
- transferringTokenCount: totals.transferringCount,
12810
- transferringAmount: totals.transferring.toString(),
12811
- priceUsd: null,
12812
- priceEur: null,
12813
- change24h: null,
12814
- fiatValueUsd: null,
12815
- fiatValueEur: null
12816
- };
12817
- }
12818
- function priceIdOf(registry, coinId) {
12819
- return registry.getName(coinId).toLowerCase();
12820
- }
12821
- async function withPrices(raw, registry, price) {
12822
- try {
12823
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
12824
- const quotes = await price.getPrices(ids);
12825
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
12826
- } catch {
12827
- return raw;
12828
- }
12829
- }
12830
- function priceOne(asset, quote) {
12831
- if (!quote) return asset;
12832
- const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
12833
- return {
12834
- ...asset,
12835
- priceUsd: quote.priceUsd,
12836
- priceEur: quote.priceEur ?? null,
12837
- change24h: quote.change24h ?? null,
12838
- fiatValueUsd: human * quote.priceUsd,
12839
- fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12840
- };
12841
- }
12842
- function transferringToken(tokenId, coinId, amount, registry, now) {
12843
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
12844
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12845
- transferring: true,
12846
- suspectedSpent: false
12847
- });
12848
- }
12849
-
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
- };
13209
+ // modules/payments-v2/history/History.ts
13210
+ function wireAssets(assets) {
13211
+ return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
12868
13212
  }
12869
- function messageOf(err) {
12870
- return err instanceof Error ? err.message : String(err);
13213
+ function isPermanentReject(err) {
13214
+ const status = err?.status;
13215
+ if (typeof status !== "number") return false;
13216
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
12871
13217
  }
12872
-
12873
- // modules/payments-v2/history/History.ts
12874
13218
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
12875
13219
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
12876
13220
  var History = class {
@@ -12895,7 +13239,7 @@ var History = class {
12895
13239
  dedupKey: input.transferId,
12896
13240
  id: (this.deps.newId ?? randomUUID)(),
12897
13241
  type: "SENT",
12898
- assets: [{ coinId: input.coinId, amount: input.amount }],
13242
+ assets: wireAssets(input.assets),
12899
13243
  ts: this.ts(input.timestamp),
12900
13244
  transferId: input.transferId,
12901
13245
  ...this.wireTokenId(input.tokenId),
@@ -12908,7 +13252,7 @@ var History = class {
12908
13252
  dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
12909
13253
  id: (this.deps.newId ?? randomUUID)(),
12910
13254
  type: "RECEIVED",
12911
- assets: [{ coinId: input.coinId, amount: input.amount }],
13255
+ assets: wireAssets(input.assets),
12912
13256
  ts: this.ts(input.timestamp),
12913
13257
  ...input.transferId !== void 0 ? { transferId: input.transferId } : {},
12914
13258
  ...this.wireTokenId(input.tokenId),
@@ -12921,7 +13265,7 @@ var History = class {
12921
13265
  dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
12922
13266
  id: (this.deps.newId ?? randomUUID)(),
12923
13267
  type: "MINT",
12924
- assets: [{ coinId: input.coinId, amount: input.amount }],
13268
+ assets: wireAssets(input.assets),
12925
13269
  ts: this.ts(input.timestamp),
12926
13270
  ...this.wireTokenId(input.tokenId)
12927
13271
  }));
@@ -12932,7 +13276,10 @@ var History = class {
12932
13276
  try {
12933
13277
  await this.deps.client.postHistory([record]);
12934
13278
  } catch (err) {
12935
- this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
13279
+ this.deps.log?.(
13280
+ isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
13281
+ err
13282
+ );
12936
13283
  return;
12937
13284
  }
12938
13285
  this.deps.emit("history:updated", this.toEntry(record));
@@ -12999,6 +13346,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
12999
13346
  function stateKey(tokenId, stateHash) {
13000
13347
  return `${tokenId}:${stateHash}`;
13001
13348
  }
13349
+ function unchanged(prev, item, tokenType) {
13350
+ return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
13351
+ }
13352
+ function isCoinless(item, prev) {
13353
+ if (item.status !== "active") return prev?.coinless ?? false;
13354
+ return (item.assets?.length ?? 0) === 0;
13355
+ }
13002
13356
  var InventoryView = class {
13003
13357
  constructor(deps) {
13004
13358
  this.deps = deps;
@@ -13092,14 +13446,26 @@ var InventoryView = class {
13092
13446
  pool(coinId) {
13093
13447
  const out = [];
13094
13448
  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;
13449
+ if (!this.isSpendable(tokenId, entry)) continue;
13098
13450
  const asset = entry.assets.find((a) => a.coinId === coinId);
13099
13451
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
13100
13452
  }
13101
13453
  return out;
13102
13454
  }
13455
+ /**
13456
+ * The eligibility gates every spend shares, coin or token — ONE definition, so a
13457
+ * single probe covers both verbs and neither can drift from the other.
13458
+ */
13459
+ isSpendable(tokenId, entry) {
13460
+ if (entry.status !== "active") return false;
13461
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13462
+ return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13463
+ }
13464
+ /** Is this NAMED token spendable whole — coinless or valued (#777)? */
13465
+ spendableToken(tokenId) {
13466
+ const entry = this.mirror.get(tokenId);
13467
+ return entry !== void 0 && this.isSpendable(tokenId, entry);
13468
+ }
13103
13469
  /**
13104
13470
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
13105
13471
  * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
@@ -13125,6 +13491,29 @@ var InventoryView = class {
13125
13491
  }
13126
13492
  return out;
13127
13493
  }
13494
+ /**
13495
+ * Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
13496
+ * exactly when it is not, so no mirror row can appear in both reads.
13497
+ */
13498
+ coinless(registry) {
13499
+ const out = [];
13500
+ for (const [tokenId, entry] of this.mirror) {
13501
+ if (entry.status !== "active" || !entry.coinless) continue;
13502
+ const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
13503
+ out.push({
13504
+ tokenId,
13505
+ ...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
13506
+ ...meta ? { name: meta.name } : {},
13507
+ ...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
13508
+ stateHash: entry.stateHash,
13509
+ transferring: this.held(tokenId),
13510
+ ...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
13511
+ createdAt: entry.createdAt,
13512
+ updatedAt: entry.updatedAt
13513
+ });
13514
+ }
13515
+ return out;
13516
+ }
13128
13517
  async assets(registry, price) {
13129
13518
  const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
13130
13519
  if (!price || raw.length === 0) return raw;
@@ -13196,14 +13585,19 @@ var InventoryView = class {
13196
13585
  applyOne(item, now) {
13197
13586
  const prev = this.mirror.get(item.tokenId);
13198
13587
  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
- }
13588
+ const tokenType = item.tokenType ?? prev?.tokenType;
13589
+ if (prev && unchanged(prev, item, tokenType)) return false;
13202
13590
  this.mirror.set(item.tokenId, {
13203
13591
  stateHash: item.stateHash,
13204
13592
  seq: item.seq,
13205
13593
  status: item.status,
13206
- assets: item.assets ?? prev?.assets ?? [],
13594
+ // ONLY a tombstone inherits: it omits assets for an unrelated reason and
13595
+ // recoverRemoved must know the amount it restores. An ACTIVE row that omits
13596
+ // them is stating coinlessness, and inheriting there would keep stale assets
13597
+ // in tokens() while `coinless` is true — the row in BOTH reads (§16).
13598
+ assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
13599
+ coinless: isCoinless(item, prev),
13600
+ ...tokenType !== void 0 ? { tokenType } : {},
13207
13601
  createdAt: prev?.createdAt ?? now,
13208
13602
  updatedAt: now
13209
13603
  });
@@ -13439,7 +13833,12 @@ async function screen(deps, engine, entry) {
13439
13833
  }
13440
13834
  return {
13441
13835
  kind: "accept",
13442
- record: { tokenId: keys.tokenId, stateHash: keys.stateHash, assets: toAssetAmounts(token) }
13836
+ record: {
13837
+ tokenId: keys.tokenId,
13838
+ stateHash: keys.stateHash,
13839
+ assets: toAssetAmounts(token),
13840
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
13841
+ }
13443
13842
  };
13444
13843
  }
13445
13844
  async function announce(deps, entry, record) {
@@ -13450,6 +13849,7 @@ async function announce(deps, entry, record) {
13450
13849
  tokenId: record.tokenId,
13451
13850
  stateHash: record.stateHash,
13452
13851
  assets: record.assets,
13852
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
13453
13853
  ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
13454
13854
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13455
13855
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
@@ -13463,6 +13863,19 @@ async function announce(deps, entry, record) {
13463
13863
  senderPubkey: entry.senderPubkey ?? "",
13464
13864
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13465
13865
  tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
13866
+ // #777: named here rather than mapped from assets, which announced an EMPTY list.
13867
+ ...record.tokenType !== void 0 ? {
13868
+ coinless: [
13869
+ {
13870
+ tokenId: record.tokenId,
13871
+ tokenType: record.tokenType,
13872
+ stateHash: record.stateHash,
13873
+ transferring: false,
13874
+ createdAt: receivedAt,
13875
+ updatedAt: receivedAt
13876
+ }
13877
+ ]
13878
+ } : {},
13466
13879
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
13467
13880
  receivedAt
13468
13881
  };
@@ -14128,6 +14541,34 @@ var SpendQueue = class {
14128
14541
  }
14129
14542
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
14130
14543
  }
14544
+ /**
14545
+ * #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
14546
+ * for. A coin plan waits because some OTHER combination may free up; a named
14547
+ * token is either free now or held by a reservation that only its own transfer
14548
+ * can release, so queueing would block until a timeout on a spend that cannot
14549
+ * become possible.
14550
+ */
14551
+ planWhole(reservationId, tokenId) {
14552
+ if (this.destroyed) {
14553
+ throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
14554
+ }
14555
+ const unproven = this.deps.ledger.unprovenReason();
14556
+ if (unproven !== null) {
14557
+ throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
14558
+ }
14559
+ if (this.deps.spendableToken?.(tokenId) !== true) {
14560
+ throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
14561
+ }
14562
+ const holder = this.deps.ledger.holderOf(tokenId);
14563
+ if (holder !== void 0) {
14564
+ throw new SphereError(
14565
+ `Token ${tokenId} is already reserved by transfer ${holder}`,
14566
+ "VALIDATION_ERROR"
14567
+ );
14568
+ }
14569
+ this.deps.ledger.reserve(reservationId, [{ tokenId }]);
14570
+ return { reservationId, plan: { direct: [tokenId] } };
14571
+ }
14131
14572
  /**
14132
14573
  * The sources `plan()` WOULD pick right now, reserving nothing and queueing
14133
14574
  * nothing — a read-only probe used to warm blobs while the user is still
@@ -14317,6 +14758,7 @@ function composeFacadeParts(deps, hooks) {
14317
14758
  const queue = new SpendQueue({
14318
14759
  ledger,
14319
14760
  getPool: (coinId) => view.pool(coinId),
14761
+ spendableToken: (tokenId) => view.spendableToken(tokenId),
14320
14762
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
14321
14763
  });
14322
14764
  const historyStore = new History({
@@ -14397,13 +14839,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
14397
14839
  ...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
14398
14840
  emit: deps.emit,
14399
14841
  now: deps.now ?? Date.now,
14400
- recordHistory: async ({ transferId, payload, committedAmount }) => {
14842
+ recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
14401
14843
  await historyStore.recordSent({
14402
14844
  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,
14845
+ // §5.9: the SETTLED amount, never payload.amount — the plan. A token
14846
+ // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
14847
+ ...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
14407
14848
  recipientPubkey: payload.recipient,
14408
14849
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
14409
14850
  });
@@ -14438,12 +14879,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
14438
14879
  });
14439
14880
  }
14440
14881
  async function recordReceived(historyStore, record) {
14441
- const first = record.assets[0];
14442
14882
  await historyStore.recordReceived({
14443
14883
  tokenId: record.tokenId,
14444
14884
  stateHash: record.stateHash,
14445
- coinId: first?.coinId ?? "",
14446
- amount: first?.amount ?? "0",
14885
+ assets: record.assets,
14447
14886
  ...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
14448
14887
  ...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
14449
14888
  ...record.memo !== void 0 ? { memo: record.memo } : {},
@@ -14591,6 +15030,13 @@ var PaymentsFacade = class {
14591
15030
  tokens(filter) {
14592
15031
  return this.view.tokens(this.deps.registry, filter);
14593
15032
  }
15033
+ coinless() {
15034
+ return this.view.coinless(this.deps.registry);
15035
+ }
15036
+ tokenData(tokenId) {
15037
+ const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
15038
+ return readTokenData(deps, tokenId);
15039
+ }
14594
15040
  history(page) {
14595
15041
  return this.historyStore.page(page ?? {});
14596
15042
  }
@@ -14613,7 +15059,14 @@ var PaymentsFacade = class {
14613
15059
  return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
14614
15060
  }
14615
15061
  send(request) {
14616
- return this.track(this.sendOutcome(request));
15062
+ return this.track(this.runJob({ kind: "coin", request }, request.amount));
15063
+ }
15064
+ sendWholeToken(request) {
15065
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
15066
+ }
15067
+ /** NFT-scoped twin: refuses a valued source. */
15068
+ sendCoinless(request) {
15069
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
14617
15070
  }
14618
15071
  async receive() {
14619
15072
  const transfers = await this.track(this.receiveLoop.drainOnce());
@@ -14645,12 +15098,25 @@ var PaymentsFacade = class {
14645
15098
  nowMs() {
14646
15099
  return (this.deps.now ?? Date.now)();
14647
15100
  }
15101
+ mintDeps() {
15102
+ return {
15103
+ engine: this.engine(),
15104
+ mintJournal: this.machineStores.mintJournal,
15105
+ storagePort: this.deps.storagePort,
15106
+ recordMint: (input) => this.historyStore.recordMint(input),
15107
+ armHeartbeat: () => this.heartbeat.arm(),
15108
+ noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
15109
+ refreshView: () => this.trackTail(this.view.delta()),
15110
+ ownPubkeyBytes: this.ownPubkeyBytes,
15111
+ now: () => this.nowMs()
15112
+ };
15113
+ }
14648
15114
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
14649
15115
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
14650
15116
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
14651
- async sendOutcome(request) {
15117
+ async runJob(job, amount) {
14652
15118
  const run = {
14653
- amount: request.amount,
15119
+ amount,
14654
15120
  delivered: [],
14655
15121
  sentTokens: [],
14656
15122
  tokenTransfers: [],
@@ -14660,7 +15126,7 @@ var PaymentsFacade = class {
14660
15126
  lastTransferId: ""
14661
15127
  };
14662
15128
  try {
14663
- return await this.sendWithPolicy(request, run);
15129
+ return await this.sendWithPolicy(job, run);
14664
15130
  } catch (err) {
14665
15131
  this.emitCleanFailure(err, run.lastTransferId);
14666
15132
  throw err;
@@ -14680,19 +15146,19 @@ var PaymentsFacade = class {
14680
15146
  };
14681
15147
  this.deps.emit("transfer:updated", failed);
14682
15148
  }
14683
- async sendWithPolicy(request, run) {
14684
- const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
15149
+ async sendWithPolicy(job, run) {
15150
+ const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
14685
15151
  for (let attempt = 0; ; attempt++) {
14686
15152
  let ctx;
14687
15153
  try {
14688
- ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
15154
+ ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
14689
15155
  } catch (err) {
14690
- throw this.partialize(err, run);
15156
+ throw partialize(err, run);
14691
15157
  }
14692
15158
  const disposition = await this.runAttempt(ctx);
14693
- if (disposition.kind === "rethrow") throw this.partialize(disposition.error, run);
15159
+ if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
14694
15160
  if (disposition.kind === "retry-full") {
14695
- if (attempt >= MAX_RESELECT) throw this.partialize(disposition.error, run);
15161
+ if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
14696
15162
  continue;
14697
15163
  }
14698
15164
  if (disposition.kind === "success") {
@@ -14748,7 +15214,7 @@ var PaymentsFacade = class {
14748
15214
  async disposeFailedAttempt(ctx, err) {
14749
15215
  if (isPossiblyCommittedSendOutcome(err)) {
14750
15216
  this.settleKeepOpen(ctx);
14751
- this.stampTransferId(err, ctx.transferId);
15217
+ stampTransferId(err, ctx.transferId);
14752
15218
  return { kind: "rethrow", error: err };
14753
15219
  }
14754
15220
  const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
@@ -14782,7 +15248,7 @@ var PaymentsFacade = class {
14782
15248
  async disposeConvergeFailure(ctx, err) {
14783
15249
  if (isPossiblyCommittedSendOutcome(err)) {
14784
15250
  this.settleKeepOpen(ctx);
14785
- this.stampTransferId(err, ctx.transferId);
15251
+ stampTransferId(err, ctx.transferId);
14786
15252
  return { kind: "rethrow", error: err };
14787
15253
  }
14788
15254
  if (classifyError(err) === "conflict") {
@@ -14793,9 +15259,29 @@ var PaymentsFacade = class {
14793
15259
  }
14794
15260
  return { kind: "rethrow", error: err };
14795
15261
  }
14796
- async planAndMaterialize(recipientPubkey, request, run) {
15262
+ async planAndMaterialize(recipientPubkey, job, run) {
14797
15263
  const transferId = this.newId();
14798
15264
  run.lastTransferId = transferId;
15265
+ if (job.kind === "whole") {
15266
+ const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
15267
+ const sourceIds2 = this.markPlanned(transferId, "", spend2);
15268
+ try {
15269
+ return await materializeWholeSpend(
15270
+ { engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
15271
+ {
15272
+ transferId,
15273
+ recipientPubkey,
15274
+ request: job.request,
15275
+ sourceIds: sourceIds2,
15276
+ requireCoinless: job.requireCoinless
15277
+ }
15278
+ );
15279
+ } catch (err) {
15280
+ this.settleFailure(transferId, "", sourceIds2);
15281
+ throw err;
15282
+ }
15283
+ }
15284
+ const request = { ...job.request, amount: run.amount };
14799
15285
  const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
14800
15286
  const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
14801
15287
  const sourceIds = this.markPlanned(transferId, request.coinId, spend);
@@ -14929,22 +15415,6 @@ var PaymentsFacade = class {
14929
15415
  return result;
14930
15416
  }
14931
15417
  /** 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
15418
  async softAbort(transferId) {
14949
15419
  try {
14950
15420
  await this.deps.client.abortIntent(transferId);
@@ -14960,57 +15430,11 @@ var PaymentsFacade = class {
14960
15430
  const mintId = this.newId();
14961
15431
  this.activeMoneyOps.add(mintId);
14962
15432
  try {
14963
- return await this.mintUnderJournal(mintId, coinId, amount);
15433
+ return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
14964
15434
  } finally {
14965
15435
  this.activeMoneyOps.delete(mintId);
14966
15436
  }
14967
15437
  }
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
15438
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
15015
15439
  async replayMints() {
15016
15440
  let resolved = 0;
@@ -15042,7 +15466,7 @@ var PaymentsFacade = class {
15042
15466
  if (token.blob.tokenId !== entry.tokenId) {
15043
15467
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
15044
15468
  }
15045
- await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
15469
+ await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
15046
15470
  return true;
15047
15471
  }
15048
15472
  async tokenInServerInventory(tokenId) {