@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
@@ -8127,6 +8127,8 @@ var FETCH_TIMEOUT_MS = 1e4;
8127
8127
  var TokenRegistry = class _TokenRegistry {
8128
8128
  static instance = null;
8129
8129
  definitionsById;
8130
+ /** Non-fungible definitions keyed by TOKEN TYPE — a separate namespace (#147). */
8131
+ definitionsByType;
8130
8132
  definitionsBySymbol;
8131
8133
  definitionsByName;
8132
8134
  // Remote refresh state
@@ -8145,6 +8147,7 @@ var TokenRegistry = class _TokenRegistry {
8145
8147
  inFlight = null;
8146
8148
  constructor() {
8147
8149
  this.definitionsById = /* @__PURE__ */ new Map();
8150
+ this.definitionsByType = /* @__PURE__ */ new Map();
8148
8151
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
8149
8152
  this.definitionsByName = /* @__PURE__ */ new Map();
8150
8153
  }
@@ -8372,9 +8375,11 @@ var TokenRegistry = class _TokenRegistry {
8372
8375
  this.definitionsById.clear();
8373
8376
  this.definitionsBySymbol.clear();
8374
8377
  this.definitionsByName.clear();
8378
+ this.definitionsByType.clear();
8375
8379
  for (const def of definitions) {
8376
8380
  const idLower = def.id.toLowerCase();
8377
8381
  this.definitionsById.set(idLower, def);
8382
+ if (def.assetKind === "non-fungible") this.definitionsByType.set(idLower, def);
8378
8383
  if (def.symbol) {
8379
8384
  this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
8380
8385
  }
@@ -8588,6 +8593,20 @@ var TokenRegistry = class _TokenRegistry {
8588
8593
  getAllDefinitions() {
8589
8594
  return Array.from(this.definitionsById.values());
8590
8595
  }
8596
+ /**
8597
+ * Definition for a coinless token's genesis TOKEN TYPE (#777). Never falls back
8598
+ * to `getDefinition`: a type and a coin id live in different namespaces, so a
8599
+ * type that collided with a coin id would otherwise render as that coin.
8600
+ */
8601
+ getTypeMeta(tokenType) {
8602
+ const def = this.getTypeDefinition(tokenType);
8603
+ if (!def) return null;
8604
+ return { name: def.name, iconUrl: def.icons?.[0]?.url ?? null };
8605
+ }
8606
+ getTypeDefinition(tokenType) {
8607
+ if (!tokenType) return void 0;
8608
+ return this.definitionsByType.get(tokenType.toLowerCase());
8609
+ }
8591
8610
  /**
8592
8611
  * Get all fungible token definitions
8593
8612
  * @returns Array of fungible token definitions
@@ -9423,6 +9442,8 @@ import { DataHasherFactory } from "@unicitylabs/state-transition-sdk/lib/crypto/
9423
9442
  import { CborSerializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js";
9424
9443
  import { CborDeserializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js";
9425
9444
  import { CborError } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js";
9445
+ import { CborReader } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js";
9446
+ import { MajorType } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js";
9426
9447
  import { Asset } from "@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js";
9427
9448
  import { AssetId } from "@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js";
9428
9449
  import { PaymentAssetCollection } from "@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js";
@@ -9835,6 +9856,126 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
9835
9856
  return burntToken;
9836
9857
  }
9837
9858
 
9859
+ // token-engine/value-envelope.ts
9860
+ function isCoinlessEnvelope(envelope) {
9861
+ return envelope.startsWith("none_");
9862
+ }
9863
+ var SELF_DESCRIBED_CBOR_TAG = 55799n;
9864
+ var CBOR_MAJOR_TYPE_MASK = 224;
9865
+ var describe = (error) => error instanceof Error ? error.message : String(error);
9866
+ var invalid = (message) => new SphereError(message, "VALIDATION_ERROR");
9867
+ function majorTypeOf(bytes) {
9868
+ const first = bytes.at(0);
9869
+ return first === void 0 ? null : first & CBOR_MAJOR_TYPE_MASK;
9870
+ }
9871
+ function headTagNumber(bytes) {
9872
+ try {
9873
+ return new CborReader(bytes).readLength(MajorType.TAG);
9874
+ } catch {
9875
+ return null;
9876
+ }
9877
+ }
9878
+ function isAssetShaped(item) {
9879
+ try {
9880
+ return CborDeserializer.decodeArray(item, 2).every(
9881
+ (field) => majorTypeOf(field) === MajorType.BYTE_STRING
9882
+ );
9883
+ } catch {
9884
+ return false;
9885
+ }
9886
+ }
9887
+ function arrayItemsOrNull(bytes) {
9888
+ try {
9889
+ return CborDeserializer.decodeArray(bytes);
9890
+ } catch {
9891
+ return null;
9892
+ }
9893
+ }
9894
+ function decodeSphere(genesisData) {
9895
+ try {
9896
+ return SpherePaymentData.fromCBOR(genesisData).toValue();
9897
+ } catch (error) {
9898
+ throw invalid(`Failed to decode token payment data: ${describe(error)}`);
9899
+ }
9900
+ }
9901
+ function taggedItemBody(genesisData) {
9902
+ try {
9903
+ return CborDeserializer.decodeTag(genesisData).data;
9904
+ } catch (error) {
9905
+ throw invalid(
9906
+ `Token value payload is a CBOR tag whose item is malformed or carries trailing bytes: ${describe(error)}`
9907
+ );
9908
+ }
9909
+ }
9910
+ function selfDescribedBodyCarriesValue(body) {
9911
+ const major = majorTypeOf(body);
9912
+ if (major === MajorType.TAG) {
9913
+ const inner = headTagNumber(body);
9914
+ return inner === SpherePaymentData.CBOR_TAG || inner === SELF_DESCRIBED_CBOR_TAG;
9915
+ }
9916
+ if (major === MajorType.ARRAY) {
9917
+ const items = arrayItemsOrNull(body);
9918
+ return items !== null && items.some(isAssetShaped);
9919
+ }
9920
+ return false;
9921
+ }
9922
+ function classifyTagged(genesisData) {
9923
+ const tag = headTagNumber(genesisData);
9924
+ if (tag === null) {
9925
+ throw invalid(
9926
+ "Token value payload has an unreadable or non-canonically encoded CBOR tag head \u2014 it could be a malformed SpherePaymentData envelope"
9927
+ );
9928
+ }
9929
+ if (tag === SpherePaymentData.CBOR_TAG) {
9930
+ return { envelope: "sphere", value: decodeSphere(genesisData) };
9931
+ }
9932
+ const body = taggedItemBody(genesisData);
9933
+ if (tag === SELF_DESCRIBED_CBOR_TAG && selfDescribedBodyCarriesValue(body)) {
9934
+ throw invalid(
9935
+ "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"
9936
+ );
9937
+ }
9938
+ return { envelope: "none_tag", value: null };
9939
+ }
9940
+ function classifyArray(genesisData) {
9941
+ const items = arrayItemsOrNull(genesisData);
9942
+ if (items === null) {
9943
+ throw invalid("Token value payload is not PaymentAssetCollection: malformed CBOR array");
9944
+ }
9945
+ return items.some(isAssetShaped) ? { envelope: "bare_collection", value: null } : { envelope: "none_other", value: null };
9946
+ }
9947
+ function classifyValueEnvelope(genesisData) {
9948
+ if (genesisData === null) return { envelope: "none_absent", value: null };
9949
+ const major = majorTypeOf(genesisData);
9950
+ if (major === null) return { envelope: "none_absent", value: null };
9951
+ if (major === MajorType.TAG) return classifyTagged(genesisData);
9952
+ if (major === MajorType.ARRAY) return classifyArray(genesisData);
9953
+ return { envelope: "none_other", value: null };
9954
+ }
9955
+ function wrapToken(sdkToken) {
9956
+ const { envelope, value } = classifyValueEnvelope(sdkToken.genesis.data);
9957
+ const blob = {
9958
+ tokenId: HexConverter.encode(sdkToken.id.bytes),
9959
+ token: sdkToken.toCBOR()
9960
+ };
9961
+ return {
9962
+ sdkToken,
9963
+ blob,
9964
+ value,
9965
+ valueEnvelope: envelope,
9966
+ tokenType: HexConverter.encode(sdkToken.type.bytes)
9967
+ };
9968
+ }
9969
+ function assertMintableData(data) {
9970
+ try {
9971
+ classifyValueEnvelope(data);
9972
+ } catch (error) {
9973
+ throw invalid(
9974
+ `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.`
9975
+ );
9976
+ }
9977
+ }
9978
+
9838
9979
  // token-engine/SphereTokenEngine.ts
9839
9980
  var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
9840
9981
  var MAX_MINT_CONCURRENCY = 8;
@@ -9891,7 +10032,7 @@ var SphereTokenEngine = class {
9891
10032
  return sdkToken.latestTransaction.data;
9892
10033
  }
9893
10034
  const data = sdkToken.genesis.data;
9894
- if (data && this.isSpherePaymentData(data)) {
10035
+ if (data && classifyValueEnvelope(data).envelope === "sphere") {
9895
10036
  return SpherePaymentData.fromCBOR(data).memo;
9896
10037
  }
9897
10038
  return null;
@@ -9921,9 +10062,10 @@ var SphereTokenEngine = class {
9921
10062
  );
9922
10063
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
9923
10064
  const token = await Token.mint(certified, this.deps.verificationContext);
9924
- return this.wrapToken(token);
10065
+ return wrapToken(token);
9925
10066
  }
9926
10067
  async mintDataToken(params, options) {
10068
+ assertMintableData(params.data);
9927
10069
  const recipient = SignaturePredicate.create(params.recipientPubkey);
9928
10070
  const tokenType = params.tokenType ? new TokenType(params.tokenType) : TokenType.generate();
9929
10071
  const salt = params.salt ? TokenSalt.fromBytes(params.salt) : TokenSalt.generate();
@@ -9938,7 +10080,7 @@ var SphereTokenEngine = class {
9938
10080
  );
9939
10081
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
9940
10082
  const token = await Token.mint(certified, this.deps.verificationContext);
9941
- return this.wrapToken(token);
10083
+ return wrapToken(token);
9942
10084
  }
9943
10085
  async transfer(params, options) {
9944
10086
  this.assertOwned(params.token);
@@ -9961,13 +10103,19 @@ var SphereTokenEngine = class {
9961
10103
  );
9962
10104
  const certified = await transferTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
9963
10105
  const transferred = await params.token.sdkToken.transfer(certified, this.deps.verificationContext);
9964
- return this.wrapToken(transferred);
10106
+ return wrapToken(transferred);
9965
10107
  }
9966
10108
  async split(params, options) {
9967
10109
  this.assertOwned(params.token);
9968
10110
  if (params.outputs.length === 0) {
9969
10111
  throw new SphereError("Split requires at least one output", "VALIDATION_ERROR");
9970
10112
  }
10113
+ if (params.token.value === null) {
10114
+ throw new SphereError(
10115
+ `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.`,
10116
+ "VALIDATION_ERROR"
10117
+ );
10118
+ }
9971
10119
  const transferId = this.resolveTransferId(options);
9972
10120
  const requests = params.outputs.map(
9973
10121
  (o, i) => SplitTokenRequest.create(
@@ -10089,7 +10237,7 @@ var SphereTokenEngine = class {
10089
10237
  const proof = await this.submitSplitMintLeg(certData, mintTx, options);
10090
10238
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10091
10239
  const token = await Token.mint(certified, this.deps.verificationContext);
10092
- return this.wrapToken(token);
10240
+ return wrapToken(token);
10093
10241
  }
10094
10242
  /**
10095
10243
  * Submit one split-mint leg. A `TRANSACTION_HASH_MISMATCH` on a mint leg is NOT a foreign spend
@@ -10147,7 +10295,7 @@ var SphereTokenEngine = class {
10147
10295
  "VALIDATION_ERROR"
10148
10296
  );
10149
10297
  }
10150
- return this.wrapToken(sdkToken);
10298
+ return wrapToken(sdkToken);
10151
10299
  }
10152
10300
  // ── internals ────────────────────────────────────────────────────────────────
10153
10301
  /**
@@ -10259,34 +10407,6 @@ var SphereTokenEngine = class {
10259
10407
  throw new SphereError("Cannot transfer a token not owned by this engine identity", "VALIDATION_ERROR");
10260
10408
  }
10261
10409
  }
10262
- /** Wrap an SDK token into a SphereToken: cache its blob (incl. stable tokenId) + decoded value. */
10263
- wrapToken(sdkToken) {
10264
- const data = sdkToken.genesis.data;
10265
- let value = null;
10266
- if (data && this.isSpherePaymentData(data)) {
10267
- try {
10268
- value = SpherePaymentData.fromCBOR(data).toValue();
10269
- } catch (err) {
10270
- throw new SphereError(
10271
- `Failed to decode token payment data: ${err instanceof Error ? err.message : String(err)}`,
10272
- "VALIDATION_ERROR"
10273
- );
10274
- }
10275
- }
10276
- const blob = {
10277
- tokenId: HexConverter.encode(sdkToken.id.bytes),
10278
- token: sdkToken.toCBOR()
10279
- };
10280
- return { sdkToken, blob, value };
10281
- }
10282
- /** True if the bytes are a SpherePaymentData envelope (value token) vs a raw data token. */
10283
- isSpherePaymentData(data) {
10284
- try {
10285
- return CborDeserializer.decodeTag(data).tag === SpherePaymentData.CBOR_TAG;
10286
- } catch {
10287
- return false;
10288
- }
10289
- }
10290
10410
  /**
10291
10411
  * Terminate the verification worker pool, if this engine was configured with
10292
10412
  * one. Idempotent — the pool's dispose only touches workers it spawned.
@@ -11682,6 +11802,17 @@ function createMachineStores(kv) {
11682
11802
  };
11683
11803
  }
11684
11804
 
11805
+ // modules/payments-v2/machine/payload-view.ts
11806
+ function isWholeIntent(p) {
11807
+ return p.kind === "whole" || p.kind === "coinless";
11808
+ }
11809
+ function splitOf(p) {
11810
+ return p.kind === "coin" ? p.split : void 0;
11811
+ }
11812
+ function coinIdOf(p) {
11813
+ return p.kind === "coin" ? p.coinId : void 0;
11814
+ }
11815
+
11685
11816
  // modules/payments-v2/machine/TransferMachine.ts
11686
11817
  var CERTIFY_WIDTH = 20;
11687
11818
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
@@ -11884,13 +12015,14 @@ var TransferMachine = class {
11884
12015
  const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
11885
12016
  return { recipientBlob: finished.blob.token };
11886
12017
  }
11887
- const split = ctx.payload.split;
12018
+ const split = splitOf(ctx.payload);
12019
+ const splitCoinId = coinIdOf(ctx.payload);
11888
12020
  const { outputs } = await engine.split(
11889
12021
  {
11890
12022
  token: source,
11891
12023
  outputs: [
11892
- { recipientPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.splitAmount) },
11893
- { recipientPubkey: this.deps.ownPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.remainderAmount) }
12024
+ { recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
12025
+ { recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
11894
12026
  ]
11895
12027
  },
11896
12028
  opts
@@ -12012,7 +12144,9 @@ var TransferMachine = class {
12012
12144
  const entry = {
12013
12145
  transferId: r.transferId,
12014
12146
  remainingAmount: undelivered.toString(),
12015
- coinId: r.payload.coinId,
12147
+ // '' only on a token-addressed intent, which is one leg: it either lands or
12148
+ // conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
12149
+ coinId: coinIdOf(r.payload) ?? "",
12016
12150
  recipient: r.payload.recipient,
12017
12151
  committedTokenIds: committed.map((o) => o.op.sourceTokenId),
12018
12152
  createdAt: this.deps.now()
@@ -12038,8 +12172,16 @@ var TransferMachine = class {
12038
12172
  }
12039
12173
  async finishBookkeeping(engine, transferId, payload, phase, committed) {
12040
12174
  try {
12041
- const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
12042
- await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
12175
+ const coinId = coinIdOf(payload);
12176
+ const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
12177
+ const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
12178
+ await this.deps.recordHistory?.({
12179
+ transferId,
12180
+ payload,
12181
+ phase,
12182
+ committedAmount,
12183
+ ...committedAssets !== void 0 ? { committedAssets } : {}
12184
+ });
12043
12185
  } catch {
12044
12186
  }
12045
12187
  try {
@@ -12053,6 +12195,18 @@ var TransferMachine = class {
12053
12195
  * a journal-rehydrated resume leg has no source token, but every committed
12054
12196
  * leg carries its recipient blob, so the outcome-derived rule is uniform).
12055
12197
  */
12198
+ /** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
12199
+ async settledAssets(engine, committed) {
12200
+ const total = /* @__PURE__ */ new Map();
12201
+ for (const outcome of committed) {
12202
+ if (outcome.recipientBlob === void 0) continue;
12203
+ const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
12204
+ for (const asset of engine.readValue(token)?.assets ?? []) {
12205
+ total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
12206
+ }
12207
+ }
12208
+ return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
12209
+ }
12056
12210
  async settledAmount(engine, coinId, committed) {
12057
12211
  let total = 0n;
12058
12212
  for (const outcome of committed) {
@@ -12081,8 +12235,11 @@ var TransferMachine = class {
12081
12235
  }
12082
12236
  };
12083
12237
  function conflictAmount(engine, payload, op, source) {
12084
- if (op.kind === "split") return BigInt(payload.split.splitAmount);
12085
- return source !== void 0 ? engine.balanceOf(source, payload.coinId) : 0n;
12238
+ const split = splitOf(payload);
12239
+ if (op.kind === "split") return BigInt(split.splitAmount);
12240
+ const coinId = coinIdOf(payload);
12241
+ if (coinId === void 0) return 0n;
12242
+ return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
12086
12243
  }
12087
12244
 
12088
12245
  // modules/payments-v2/machine/resume.ts
@@ -12189,6 +12346,14 @@ async function runOne(ctx, job, report) {
12189
12346
  report.failed.push(job.transferId);
12190
12347
  return;
12191
12348
  }
12349
+ if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
12350
+ logger.warn(
12351
+ "PaymentsV2",
12352
+ `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
12353
+ );
12354
+ report.failed.push(job.transferId);
12355
+ return;
12356
+ }
12192
12357
  mine.set(op.sourceTokenId, token);
12193
12358
  }
12194
12359
  try {
@@ -12219,6 +12384,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
12219
12384
  }
12220
12385
  report.failed.push(transferId);
12221
12386
  }
12387
+ function validateCoinPayload(c) {
12388
+ if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
12389
+ throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
12390
+ }
12391
+ const s = c.split;
12392
+ if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
12393
+ throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
12394
+ }
12395
+ return { ...c, kind: "coin" };
12396
+ }
12397
+ function validateWholePayload(p) {
12398
+ if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
12399
+ throw new SphereError(
12400
+ "token intent payload must name exactly one source token",
12401
+ "VALIDATION_ERROR"
12402
+ );
12403
+ }
12404
+ if (p.split !== void 0) {
12405
+ throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
12406
+ }
12407
+ return { ...p, kind: "whole", direct: [p.direct[0]] };
12408
+ }
12222
12409
  function validatePayload(raw) {
12223
12410
  const p = raw !== null && typeof raw === "object" ? raw : null;
12224
12411
  if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
@@ -12227,14 +12414,17 @@ function validatePayload(raw) {
12227
12414
  "VALIDATION_ERROR"
12228
12415
  );
12229
12416
  }
12230
- if (typeof p.recipient !== "string" || typeof p.coinId !== "string" || typeof p.amount !== "string") {
12231
- throw new SphereError("intent payload is missing recipient/coinId/amount", "VALIDATION_ERROR");
12417
+ if (typeof p.recipient !== "string") {
12418
+ throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
12232
12419
  }
12233
- const s = p.split;
12234
- if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
12235
- throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
12420
+ const kind = p.kind ?? "coin";
12421
+ if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
12422
+ throw new SphereError(
12423
+ `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
12424
+ "VALIDATION_ERROR"
12425
+ );
12236
12426
  }
12237
- return p;
12427
+ return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
12238
12428
  }
12239
12429
 
12240
12430
  // modules/payments-v2/convergence.ts
@@ -12440,13 +12630,24 @@ async function openRow(entry, journal, decryptPayload) {
12440
12630
  transferId: entry.transferId,
12441
12631
  kind: "open",
12442
12632
  recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
12443
- coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
12444
- amount: typeof payload?.amount === "string" ? payload.amount : "",
12633
+ ...subject(payload),
12445
12634
  legs: { certified, total: Math.max(total, certified) },
12446
12635
  deliveryPending: journal.length > 0,
12447
12636
  createdAt: entry.createdAt
12448
12637
  };
12449
12638
  }
12639
+ function subject(payload) {
12640
+ const rawKind = payload?.kind;
12641
+ if (rawKind === "whole" || rawKind === "coinless") {
12642
+ const tokenId = payload?.direct?.[0];
12643
+ return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
12644
+ }
12645
+ const coin = payload;
12646
+ return {
12647
+ coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
12648
+ amount: typeof coin?.amount === "string" ? coin.amount : ""
12649
+ };
12650
+ }
12450
12651
  function journalOnlyRow(transferId, entries) {
12451
12652
  const certified = distinctOpCount(entries);
12452
12653
  return {
@@ -12476,6 +12677,263 @@ function distinctOpCount(entries) {
12476
12677
  return new Set(entries.map((entry) => entry.opIndex)).size;
12477
12678
  }
12478
12679
 
12680
+ // modules/payments-v2/inventory/token-data.ts
12681
+ async function readTokenData(deps, tokenId) {
12682
+ if (deps.view.stateHashOf(tokenId) === void 0) {
12683
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
12684
+ }
12685
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
12686
+ if (bytes === void 0) {
12687
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
12688
+ }
12689
+ return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
12690
+ }
12691
+
12692
+ // modules/payments-v2/machine/payload.ts
12693
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
12694
+ return {
12695
+ v: 2,
12696
+ kind: "coin",
12697
+ recipient: recipientPubkey,
12698
+ coinId: request.coinId,
12699
+ amount: request.amount,
12700
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
12701
+ direct: [...spend.plan.direct],
12702
+ ...spend.plan.split !== void 0 ? {
12703
+ split: {
12704
+ tokenId: spend.plan.split.tokenId,
12705
+ splitAmount: spend.plan.split.splitAmount.toString(),
12706
+ remainderAmount: spend.plan.split.remainderAmount.toString()
12707
+ }
12708
+ } : {},
12709
+ spentStates
12710
+ };
12711
+ }
12712
+ function messageOf(err) {
12713
+ return err instanceof Error ? err.message : String(err);
12714
+ }
12715
+ function buildWholePayload(recipientPubkey, request, spentStates) {
12716
+ return {
12717
+ v: 2,
12718
+ kind: "whole",
12719
+ recipient: recipientPubkey,
12720
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
12721
+ direct: [request.tokenId],
12722
+ spentStates
12723
+ };
12724
+ }
12725
+
12726
+ // modules/payments-v2/mint-params.ts
12727
+ function mintParams(recipientPubkey, coinId, amount) {
12728
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12729
+ }
12730
+
12731
+ // modules/payments-v2/mint.ts
12732
+ function runMintUnderJournal(deps, input) {
12733
+ return mintUnderJournal(deps, input);
12734
+ }
12735
+ async function mintUnderJournal(deps, input) {
12736
+ const { mintId, coinId, amount } = input;
12737
+ const engine = deps.engine;
12738
+ const entry = {
12739
+ mintId,
12740
+ coinId,
12741
+ amount: amount.toString(),
12742
+ tokenId: "",
12743
+ createdAt: deps.now()
12744
+ };
12745
+ await deps.mintJournal.upsert(entry);
12746
+ let token;
12747
+ try {
12748
+ token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
12749
+ } catch (err) {
12750
+ deps.armHeartbeat();
12751
+ return { success: false, error: messageOf(err) };
12752
+ }
12753
+ if (token.blob.tokenId !== entry.tokenId) {
12754
+ await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
12755
+ }
12756
+ try {
12757
+ await finalizeMint(deps, mintId, token, coinId, amount.toString());
12758
+ } catch (err) {
12759
+ deps.armHeartbeat();
12760
+ return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
12761
+ }
12762
+ return { success: true, tokenId: token.blob.tokenId };
12763
+ }
12764
+ async function finalizeMint(deps, mintId, token, coinId, amount) {
12765
+ const engine = deps.engine;
12766
+ const bytes = token.blob.token;
12767
+ const digest = bytesToHex3(sha256(bytes));
12768
+ const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
12769
+ const key = keys.get(digest);
12770
+ if (key === void 0) {
12771
+ throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
12772
+ }
12773
+ await deps.storagePort.applyDelta({
12774
+ transferId: mintId,
12775
+ spent: [],
12776
+ added: [{ tokenId: token.blob.tokenId, key }]
12777
+ });
12778
+ await deps.recordMint({
12779
+ tokenId: token.blob.tokenId,
12780
+ assets: [{ coinId, amount }]
12781
+ });
12782
+ await deps.mintJournal.removeByKey(mintId);
12783
+ deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
12784
+ deps.refreshView();
12785
+ }
12786
+
12787
+ // modules/payments-v2/inventory/presentation.ts
12788
+ function toToken(tokenId, snapshot, asset, registry, flags) {
12789
+ const iconUrl = registry.getIconUrl(asset.coinId);
12790
+ return {
12791
+ id: tokenId,
12792
+ coinId: asset.coinId,
12793
+ symbol: registry.getSymbol(asset.coinId),
12794
+ name: registry.getName(asset.coinId),
12795
+ decimals: registry.getDecimals(asset.coinId),
12796
+ ...iconUrl !== null ? { iconUrl } : {},
12797
+ amount: asset.amount,
12798
+ status: flags.transferring ? "transferring" : "confirmed",
12799
+ createdAt: snapshot.createdAt,
12800
+ updatedAt: snapshot.updatedAt,
12801
+ lazy: true,
12802
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12803
+ };
12804
+ }
12805
+ function aggregateAssets(entries, isTransferring, registry) {
12806
+ const groups = /* @__PURE__ */ new Map();
12807
+ for (const [tokenId, entry] of entries) {
12808
+ const moving = isTransferring(tokenId);
12809
+ for (const asset of entry.assets) {
12810
+ let group = groups.get(asset.coinId);
12811
+ if (!group) {
12812
+ group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12813
+ groups.set(asset.coinId, group);
12814
+ }
12815
+ if (moving) {
12816
+ group.transferring += BigInt(asset.amount);
12817
+ group.transferringCount += 1;
12818
+ } else {
12819
+ group.total += BigInt(asset.amount);
12820
+ group.count += 1;
12821
+ }
12822
+ }
12823
+ }
12824
+ return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12825
+ }
12826
+ function toAsset(coinId, totals, registry) {
12827
+ const iconUrl = registry.getIconUrl(coinId);
12828
+ return {
12829
+ coinId,
12830
+ symbol: registry.getSymbol(coinId),
12831
+ name: registry.getName(coinId),
12832
+ decimals: registry.getDecimals(coinId),
12833
+ ...iconUrl !== null ? { iconUrl } : {},
12834
+ totalAmount: totals.total.toString(),
12835
+ tokenCount: totals.count,
12836
+ confirmedAmount: totals.total.toString(),
12837
+ unconfirmedAmount: "0",
12838
+ confirmedTokenCount: totals.count,
12839
+ unconfirmedTokenCount: 0,
12840
+ transferringTokenCount: totals.transferringCount,
12841
+ transferringAmount: totals.transferring.toString(),
12842
+ priceUsd: null,
12843
+ priceEur: null,
12844
+ change24h: null,
12845
+ fiatValueUsd: null,
12846
+ fiatValueEur: null
12847
+ };
12848
+ }
12849
+ function priceIdOf(registry, coinId) {
12850
+ return registry.getName(coinId).toLowerCase();
12851
+ }
12852
+ async function withPrices(raw, registry, price) {
12853
+ try {
12854
+ const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
12855
+ const quotes = await price.getPrices(ids);
12856
+ return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
12857
+ } catch {
12858
+ return raw;
12859
+ }
12860
+ }
12861
+ function priceOne(asset, quote) {
12862
+ if (!quote) return asset;
12863
+ const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
12864
+ return {
12865
+ ...asset,
12866
+ priceUsd: quote.priceUsd,
12867
+ priceEur: quote.priceEur ?? null,
12868
+ change24h: quote.change24h ?? null,
12869
+ fiatValueUsd: human * quote.priceUsd,
12870
+ fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12871
+ };
12872
+ }
12873
+ function transferringToken(tokenId, coinId, amount, registry, now) {
12874
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
12875
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12876
+ transferring: true,
12877
+ suspectedSpent: false
12878
+ });
12879
+ }
12880
+
12881
+ // modules/payments-v2/send-whole.ts
12882
+ async function materializeWholeSpend(deps, input) {
12883
+ const { engine } = deps;
12884
+ const tokenId = input.request.tokenId;
12885
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
12886
+ if (bytes === void 0) {
12887
+ throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
12888
+ }
12889
+ const token = await engine.decodeToken({ tokenId, token: bytes });
12890
+ if (token.valueEnvelope === "bare_collection") {
12891
+ throw new SphereError(
12892
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
12893
+ "VALIDATION_ERROR"
12894
+ );
12895
+ }
12896
+ if (input.requireCoinless && token.value !== null) {
12897
+ throw new SphereError(
12898
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
12899
+ "VALIDATION_ERROR"
12900
+ );
12901
+ }
12902
+ const keys = await engine.deliveryKeys(bytes);
12903
+ const payload = buildWholePayload(input.recipientPubkey, input.request, {
12904
+ [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
12905
+ });
12906
+ const plan = {
12907
+ transferId: input.transferId,
12908
+ recipientPubkey: input.recipientPubkey,
12909
+ payload,
12910
+ ops: buildOps(payload),
12911
+ sources: /* @__PURE__ */ new Map([[tokenId, token]]),
12912
+ ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
12913
+ };
12914
+ const sourceTokens = (token.value?.assets ?? []).map(
12915
+ (asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
12916
+ );
12917
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
12918
+ }
12919
+
12920
+ // modules/payments-v2/send-errors.ts
12921
+ function partialize(err, run) {
12922
+ if (run.delivered.length === 0) return err;
12923
+ return new PartialSendConflictError(
12924
+ "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.",
12925
+ run.firstPartialId ?? "",
12926
+ [...run.delivered],
12927
+ run.amount,
12928
+ err
12929
+ );
12930
+ }
12931
+ function stampTransferId(err, transferId) {
12932
+ if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
12933
+ err.transferId = transferId;
12934
+ }
12935
+ }
12936
+
12479
12937
  // modules/payments-v2/recipient.ts
12480
12938
  var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
12481
12939
  async function requireSameNetworkRecipient(deps, identifier) {
@@ -12524,11 +12982,6 @@ async function splitOpIndex(deps, envelope) {
12524
12982
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
12525
12983
  }
12526
12984
 
12527
- // modules/payments-v2/mint-params.ts
12528
- function mintParams(recipientPubkey, coinId, amount) {
12529
- return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12530
- }
12531
-
12532
12985
  // modules/payments-v2/prewarm-cache.ts
12533
12986
  var PrewarmCache = class _PrewarmCache {
12534
12987
  entries = /* @__PURE__ */ new Map();
@@ -12608,124 +13061,15 @@ async function takeSourceBlobs(deps, sourceIds) {
12608
13061
  return blobs;
12609
13062
  }
12610
13063
 
12611
- // modules/payments-v2/inventory/presentation.ts
12612
- function toToken(tokenId, snapshot, asset, registry, flags) {
12613
- const iconUrl = registry.getIconUrl(asset.coinId);
12614
- return {
12615
- id: tokenId,
12616
- coinId: asset.coinId,
12617
- symbol: registry.getSymbol(asset.coinId),
12618
- name: registry.getName(asset.coinId),
12619
- decimals: registry.getDecimals(asset.coinId),
12620
- ...iconUrl !== null ? { iconUrl } : {},
12621
- amount: asset.amount,
12622
- status: flags.transferring ? "transferring" : "confirmed",
12623
- createdAt: snapshot.createdAt,
12624
- updatedAt: snapshot.updatedAt,
12625
- lazy: true,
12626
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12627
- };
12628
- }
12629
- function aggregateAssets(entries, isTransferring, registry) {
12630
- const groups = /* @__PURE__ */ new Map();
12631
- for (const [tokenId, entry] of entries) {
12632
- const moving = isTransferring(tokenId);
12633
- for (const asset of entry.assets) {
12634
- let group = groups.get(asset.coinId);
12635
- if (!group) {
12636
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12637
- groups.set(asset.coinId, group);
12638
- }
12639
- if (moving) {
12640
- group.transferring += BigInt(asset.amount);
12641
- group.transferringCount += 1;
12642
- } else {
12643
- group.total += BigInt(asset.amount);
12644
- group.count += 1;
12645
- }
12646
- }
12647
- }
12648
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12649
- }
12650
- function toAsset(coinId, totals, registry) {
12651
- const iconUrl = registry.getIconUrl(coinId);
12652
- return {
12653
- coinId,
12654
- symbol: registry.getSymbol(coinId),
12655
- name: registry.getName(coinId),
12656
- decimals: registry.getDecimals(coinId),
12657
- ...iconUrl !== null ? { iconUrl } : {},
12658
- totalAmount: totals.total.toString(),
12659
- tokenCount: totals.count,
12660
- confirmedAmount: totals.total.toString(),
12661
- unconfirmedAmount: "0",
12662
- confirmedTokenCount: totals.count,
12663
- unconfirmedTokenCount: 0,
12664
- transferringTokenCount: totals.transferringCount,
12665
- transferringAmount: totals.transferring.toString(),
12666
- priceUsd: null,
12667
- priceEur: null,
12668
- change24h: null,
12669
- fiatValueUsd: null,
12670
- fiatValueEur: null
12671
- };
12672
- }
12673
- function priceIdOf(registry, coinId) {
12674
- return registry.getName(coinId).toLowerCase();
12675
- }
12676
- async function withPrices(raw, registry, price) {
12677
- try {
12678
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
12679
- const quotes = await price.getPrices(ids);
12680
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
12681
- } catch {
12682
- return raw;
12683
- }
12684
- }
12685
- function priceOne(asset, quote) {
12686
- if (!quote) return asset;
12687
- const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
12688
- return {
12689
- ...asset,
12690
- priceUsd: quote.priceUsd,
12691
- priceEur: quote.priceEur ?? null,
12692
- change24h: quote.change24h ?? null,
12693
- fiatValueUsd: human * quote.priceUsd,
12694
- fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12695
- };
12696
- }
12697
- function transferringToken(tokenId, coinId, amount, registry, now) {
12698
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
12699
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12700
- transferring: true,
12701
- suspectedSpent: false
12702
- });
12703
- }
12704
-
12705
- // modules/payments-v2/machine/payload.ts
12706
- function buildPayload(recipientPubkey, request, spend, spentStates) {
12707
- return {
12708
- v: 2,
12709
- recipient: recipientPubkey,
12710
- coinId: request.coinId,
12711
- amount: request.amount,
12712
- ...request.memo !== void 0 ? { memo: request.memo } : {},
12713
- direct: [...spend.plan.direct],
12714
- ...spend.plan.split !== void 0 ? {
12715
- split: {
12716
- tokenId: spend.plan.split.tokenId,
12717
- splitAmount: spend.plan.split.splitAmount.toString(),
12718
- remainderAmount: spend.plan.split.remainderAmount.toString()
12719
- }
12720
- } : {},
12721
- spentStates
12722
- };
13064
+ // modules/payments-v2/history/History.ts
13065
+ function wireAssets(assets) {
13066
+ return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
12723
13067
  }
12724
- function messageOf(err) {
12725
- return err instanceof Error ? err.message : String(err);
13068
+ function isPermanentReject(err) {
13069
+ const status = err?.status;
13070
+ if (typeof status !== "number") return false;
13071
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
12726
13072
  }
12727
-
12728
- // modules/payments-v2/history/History.ts
12729
13073
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
12730
13074
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
12731
13075
  var History = class {
@@ -12750,7 +13094,7 @@ var History = class {
12750
13094
  dedupKey: input.transferId,
12751
13095
  id: (this.deps.newId ?? randomUUID)(),
12752
13096
  type: "SENT",
12753
- assets: [{ coinId: input.coinId, amount: input.amount }],
13097
+ assets: wireAssets(input.assets),
12754
13098
  ts: this.ts(input.timestamp),
12755
13099
  transferId: input.transferId,
12756
13100
  ...this.wireTokenId(input.tokenId),
@@ -12763,7 +13107,7 @@ var History = class {
12763
13107
  dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
12764
13108
  id: (this.deps.newId ?? randomUUID)(),
12765
13109
  type: "RECEIVED",
12766
- assets: [{ coinId: input.coinId, amount: input.amount }],
13110
+ assets: wireAssets(input.assets),
12767
13111
  ts: this.ts(input.timestamp),
12768
13112
  ...input.transferId !== void 0 ? { transferId: input.transferId } : {},
12769
13113
  ...this.wireTokenId(input.tokenId),
@@ -12776,7 +13120,7 @@ var History = class {
12776
13120
  dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
12777
13121
  id: (this.deps.newId ?? randomUUID)(),
12778
13122
  type: "MINT",
12779
- assets: [{ coinId: input.coinId, amount: input.amount }],
13123
+ assets: wireAssets(input.assets),
12780
13124
  ts: this.ts(input.timestamp),
12781
13125
  ...this.wireTokenId(input.tokenId)
12782
13126
  }));
@@ -12787,7 +13131,10 @@ var History = class {
12787
13131
  try {
12788
13132
  await this.deps.client.postHistory([record]);
12789
13133
  } catch (err) {
12790
- this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
13134
+ this.deps.log?.(
13135
+ isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
13136
+ err
13137
+ );
12791
13138
  return;
12792
13139
  }
12793
13140
  this.deps.emit("history:updated", this.toEntry(record));
@@ -12854,6 +13201,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
12854
13201
  function stateKey(tokenId, stateHash) {
12855
13202
  return `${tokenId}:${stateHash}`;
12856
13203
  }
13204
+ function unchanged(prev, item, tokenType) {
13205
+ return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
13206
+ }
13207
+ function isCoinless(item, prev) {
13208
+ if (item.status !== "active") return prev?.coinless ?? false;
13209
+ return (item.assets?.length ?? 0) === 0;
13210
+ }
12857
13211
  var InventoryView = class {
12858
13212
  constructor(deps) {
12859
13213
  this.deps = deps;
@@ -12947,14 +13301,26 @@ var InventoryView = class {
12947
13301
  pool(coinId) {
12948
13302
  const out = [];
12949
13303
  for (const [tokenId, entry] of this.mirror) {
12950
- if (entry.status !== "active") continue;
12951
- if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
12952
- if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
13304
+ if (!this.isSpendable(tokenId, entry)) continue;
12953
13305
  const asset = entry.assets.find((a) => a.coinId === coinId);
12954
13306
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
12955
13307
  }
12956
13308
  return out;
12957
13309
  }
13310
+ /**
13311
+ * The eligibility gates every spend shares, coin or token — ONE definition, so a
13312
+ * single probe covers both verbs and neither can drift from the other.
13313
+ */
13314
+ isSpendable(tokenId, entry) {
13315
+ if (entry.status !== "active") return false;
13316
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13317
+ return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13318
+ }
13319
+ /** Is this NAMED token spendable whole — coinless or valued (#777)? */
13320
+ spendableToken(tokenId) {
13321
+ const entry = this.mirror.get(tokenId);
13322
+ return entry !== void 0 && this.isSpendable(tokenId, entry);
13323
+ }
12958
13324
  /**
12959
13325
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
12960
13326
  * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
@@ -12980,6 +13346,29 @@ var InventoryView = class {
12980
13346
  }
12981
13347
  return out;
12982
13348
  }
13349
+ /**
13350
+ * Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
13351
+ * exactly when it is not, so no mirror row can appear in both reads.
13352
+ */
13353
+ coinless(registry) {
13354
+ const out = [];
13355
+ for (const [tokenId, entry] of this.mirror) {
13356
+ if (entry.status !== "active" || !entry.coinless) continue;
13357
+ const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
13358
+ out.push({
13359
+ tokenId,
13360
+ ...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
13361
+ ...meta ? { name: meta.name } : {},
13362
+ ...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
13363
+ stateHash: entry.stateHash,
13364
+ transferring: this.held(tokenId),
13365
+ ...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
13366
+ createdAt: entry.createdAt,
13367
+ updatedAt: entry.updatedAt
13368
+ });
13369
+ }
13370
+ return out;
13371
+ }
12983
13372
  async assets(registry, price) {
12984
13373
  const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
12985
13374
  if (!price || raw.length === 0) return raw;
@@ -13051,14 +13440,19 @@ var InventoryView = class {
13051
13440
  applyOne(item, now) {
13052
13441
  const prev = this.mirror.get(item.tokenId);
13053
13442
  if (prev && item.seq < prev.seq) return false;
13054
- if (prev && prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash) {
13055
- return false;
13056
- }
13443
+ const tokenType = item.tokenType ?? prev?.tokenType;
13444
+ if (prev && unchanged(prev, item, tokenType)) return false;
13057
13445
  this.mirror.set(item.tokenId, {
13058
13446
  stateHash: item.stateHash,
13059
13447
  seq: item.seq,
13060
13448
  status: item.status,
13061
- assets: item.assets ?? prev?.assets ?? [],
13449
+ // ONLY a tombstone inherits: it omits assets for an unrelated reason and
13450
+ // recoverRemoved must know the amount it restores. An ACTIVE row that omits
13451
+ // them is stating coinlessness, and inheriting there would keep stale assets
13452
+ // in tokens() while `coinless` is true — the row in BOTH reads (§16).
13453
+ assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
13454
+ coinless: isCoinless(item, prev),
13455
+ ...tokenType !== void 0 ? { tokenType } : {},
13062
13456
  createdAt: prev?.createdAt ?? now,
13063
13457
  updatedAt: now
13064
13458
  });
@@ -13294,7 +13688,12 @@ async function screen(deps, engine, entry) {
13294
13688
  }
13295
13689
  return {
13296
13690
  kind: "accept",
13297
- record: { tokenId: keys.tokenId, stateHash: keys.stateHash, assets: toAssetAmounts(token) }
13691
+ record: {
13692
+ tokenId: keys.tokenId,
13693
+ stateHash: keys.stateHash,
13694
+ assets: toAssetAmounts(token),
13695
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
13696
+ }
13298
13697
  };
13299
13698
  }
13300
13699
  async function announce(deps, entry, record) {
@@ -13305,6 +13704,7 @@ async function announce(deps, entry, record) {
13305
13704
  tokenId: record.tokenId,
13306
13705
  stateHash: record.stateHash,
13307
13706
  assets: record.assets,
13707
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
13308
13708
  ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
13309
13709
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13310
13710
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
@@ -13318,6 +13718,19 @@ async function announce(deps, entry, record) {
13318
13718
  senderPubkey: entry.senderPubkey ?? "",
13319
13719
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13320
13720
  tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
13721
+ // #777: named here rather than mapped from assets, which announced an EMPTY list.
13722
+ ...record.tokenType !== void 0 ? {
13723
+ coinless: [
13724
+ {
13725
+ tokenId: record.tokenId,
13726
+ tokenType: record.tokenType,
13727
+ stateHash: record.stateHash,
13728
+ transferring: false,
13729
+ createdAt: receivedAt,
13730
+ updatedAt: receivedAt
13731
+ }
13732
+ ]
13733
+ } : {},
13321
13734
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
13322
13735
  receivedAt
13323
13736
  };
@@ -13983,6 +14396,34 @@ var SpendQueue = class {
13983
14396
  }
13984
14397
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
13985
14398
  }
14399
+ /**
14400
+ * #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
14401
+ * for. A coin plan waits because some OTHER combination may free up; a named
14402
+ * token is either free now or held by a reservation that only its own transfer
14403
+ * can release, so queueing would block until a timeout on a spend that cannot
14404
+ * become possible.
14405
+ */
14406
+ planWhole(reservationId, tokenId) {
14407
+ if (this.destroyed) {
14408
+ throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
14409
+ }
14410
+ const unproven = this.deps.ledger.unprovenReason();
14411
+ if (unproven !== null) {
14412
+ throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
14413
+ }
14414
+ if (this.deps.spendableToken?.(tokenId) !== true) {
14415
+ throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
14416
+ }
14417
+ const holder = this.deps.ledger.holderOf(tokenId);
14418
+ if (holder !== void 0) {
14419
+ throw new SphereError(
14420
+ `Token ${tokenId} is already reserved by transfer ${holder}`,
14421
+ "VALIDATION_ERROR"
14422
+ );
14423
+ }
14424
+ this.deps.ledger.reserve(reservationId, [{ tokenId }]);
14425
+ return { reservationId, plan: { direct: [tokenId] } };
14426
+ }
13986
14427
  /**
13987
14428
  * The sources `plan()` WOULD pick right now, reserving nothing and queueing
13988
14429
  * nothing — a read-only probe used to warm blobs while the user is still
@@ -14172,6 +14613,7 @@ function composeFacadeParts(deps, hooks) {
14172
14613
  const queue = new SpendQueue({
14173
14614
  ledger,
14174
14615
  getPool: (coinId) => view.pool(coinId),
14616
+ spendableToken: (tokenId) => view.spendableToken(tokenId),
14175
14617
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
14176
14618
  });
14177
14619
  const historyStore = new History({
@@ -14252,13 +14694,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
14252
14694
  ...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
14253
14695
  emit: deps.emit,
14254
14696
  now: deps.now ?? Date.now,
14255
- recordHistory: async ({ transferId, payload, committedAmount }) => {
14697
+ recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
14256
14698
  await historyStore.recordSent({
14257
14699
  transferId,
14258
- coinId: payload.coinId,
14259
- // §5.9: the SETTLED amount (machine-computed from the certified
14260
- // recipient blobs), never payload.amount — the plan.
14261
- amount: committedAmount,
14700
+ // §5.9: the SETTLED amount, never payload.amount — the plan. A token
14701
+ // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
14702
+ ...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
14262
14703
  recipientPubkey: payload.recipient,
14263
14704
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
14264
14705
  });
@@ -14293,12 +14734,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
14293
14734
  });
14294
14735
  }
14295
14736
  async function recordReceived(historyStore, record) {
14296
- const first = record.assets[0];
14297
14737
  await historyStore.recordReceived({
14298
14738
  tokenId: record.tokenId,
14299
14739
  stateHash: record.stateHash,
14300
- coinId: first?.coinId ?? "",
14301
- amount: first?.amount ?? "0",
14740
+ assets: record.assets,
14302
14741
  ...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
14303
14742
  ...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
14304
14743
  ...record.memo !== void 0 ? { memo: record.memo } : {},
@@ -14446,6 +14885,13 @@ var PaymentsFacade = class {
14446
14885
  tokens(filter) {
14447
14886
  return this.view.tokens(this.deps.registry, filter);
14448
14887
  }
14888
+ coinless() {
14889
+ return this.view.coinless(this.deps.registry);
14890
+ }
14891
+ tokenData(tokenId) {
14892
+ const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
14893
+ return readTokenData(deps, tokenId);
14894
+ }
14449
14895
  history(page) {
14450
14896
  return this.historyStore.page(page ?? {});
14451
14897
  }
@@ -14468,7 +14914,14 @@ var PaymentsFacade = class {
14468
14914
  return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
14469
14915
  }
14470
14916
  send(request) {
14471
- return this.track(this.sendOutcome(request));
14917
+ return this.track(this.runJob({ kind: "coin", request }, request.amount));
14918
+ }
14919
+ sendWholeToken(request) {
14920
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
14921
+ }
14922
+ /** NFT-scoped twin: refuses a valued source. */
14923
+ sendCoinless(request) {
14924
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
14472
14925
  }
14473
14926
  async receive() {
14474
14927
  const transfers = await this.track(this.receiveLoop.drainOnce());
@@ -14500,12 +14953,25 @@ var PaymentsFacade = class {
14500
14953
  nowMs() {
14501
14954
  return (this.deps.now ?? Date.now)();
14502
14955
  }
14956
+ mintDeps() {
14957
+ return {
14958
+ engine: this.engine(),
14959
+ mintJournal: this.machineStores.mintJournal,
14960
+ storagePort: this.deps.storagePort,
14961
+ recordMint: (input) => this.historyStore.recordMint(input),
14962
+ armHeartbeat: () => this.heartbeat.arm(),
14963
+ noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
14964
+ refreshView: () => this.trackTail(this.view.delta()),
14965
+ ownPubkeyBytes: this.ownPubkeyBytes,
14966
+ now: () => this.nowMs()
14967
+ };
14968
+ }
14503
14969
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
14504
14970
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
14505
14971
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
14506
- async sendOutcome(request) {
14972
+ async runJob(job, amount) {
14507
14973
  const run = {
14508
- amount: request.amount,
14974
+ amount,
14509
14975
  delivered: [],
14510
14976
  sentTokens: [],
14511
14977
  tokenTransfers: [],
@@ -14515,7 +14981,7 @@ var PaymentsFacade = class {
14515
14981
  lastTransferId: ""
14516
14982
  };
14517
14983
  try {
14518
- return await this.sendWithPolicy(request, run);
14984
+ return await this.sendWithPolicy(job, run);
14519
14985
  } catch (err) {
14520
14986
  this.emitCleanFailure(err, run.lastTransferId);
14521
14987
  throw err;
@@ -14535,19 +15001,19 @@ var PaymentsFacade = class {
14535
15001
  };
14536
15002
  this.deps.emit("transfer:updated", failed);
14537
15003
  }
14538
- async sendWithPolicy(request, run) {
14539
- const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
15004
+ async sendWithPolicy(job, run) {
15005
+ const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
14540
15006
  for (let attempt = 0; ; attempt++) {
14541
15007
  let ctx;
14542
15008
  try {
14543
- ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
15009
+ ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
14544
15010
  } catch (err) {
14545
- throw this.partialize(err, run);
15011
+ throw partialize(err, run);
14546
15012
  }
14547
15013
  const disposition = await this.runAttempt(ctx);
14548
- if (disposition.kind === "rethrow") throw this.partialize(disposition.error, run);
15014
+ if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
14549
15015
  if (disposition.kind === "retry-full") {
14550
- if (attempt >= MAX_RESELECT) throw this.partialize(disposition.error, run);
15016
+ if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
14551
15017
  continue;
14552
15018
  }
14553
15019
  if (disposition.kind === "success") {
@@ -14603,7 +15069,7 @@ var PaymentsFacade = class {
14603
15069
  async disposeFailedAttempt(ctx, err) {
14604
15070
  if (isPossiblyCommittedSendOutcome(err)) {
14605
15071
  this.settleKeepOpen(ctx);
14606
- this.stampTransferId(err, ctx.transferId);
15072
+ stampTransferId(err, ctx.transferId);
14607
15073
  return { kind: "rethrow", error: err };
14608
15074
  }
14609
15075
  const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
@@ -14637,7 +15103,7 @@ var PaymentsFacade = class {
14637
15103
  async disposeConvergeFailure(ctx, err) {
14638
15104
  if (isPossiblyCommittedSendOutcome(err)) {
14639
15105
  this.settleKeepOpen(ctx);
14640
- this.stampTransferId(err, ctx.transferId);
15106
+ stampTransferId(err, ctx.transferId);
14641
15107
  return { kind: "rethrow", error: err };
14642
15108
  }
14643
15109
  if (classifyError(err) === "conflict") {
@@ -14648,9 +15114,29 @@ var PaymentsFacade = class {
14648
15114
  }
14649
15115
  return { kind: "rethrow", error: err };
14650
15116
  }
14651
- async planAndMaterialize(recipientPubkey, request, run) {
15117
+ async planAndMaterialize(recipientPubkey, job, run) {
14652
15118
  const transferId = this.newId();
14653
15119
  run.lastTransferId = transferId;
15120
+ if (job.kind === "whole") {
15121
+ const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
15122
+ const sourceIds2 = this.markPlanned(transferId, "", spend2);
15123
+ try {
15124
+ return await materializeWholeSpend(
15125
+ { engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
15126
+ {
15127
+ transferId,
15128
+ recipientPubkey,
15129
+ request: job.request,
15130
+ sourceIds: sourceIds2,
15131
+ requireCoinless: job.requireCoinless
15132
+ }
15133
+ );
15134
+ } catch (err) {
15135
+ this.settleFailure(transferId, "", sourceIds2);
15136
+ throw err;
15137
+ }
15138
+ }
15139
+ const request = { ...job.request, amount: run.amount };
14654
15140
  const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
14655
15141
  const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
14656
15142
  const sourceIds = this.markPlanned(transferId, request.coinId, spend);
@@ -14784,22 +15270,6 @@ var PaymentsFacade = class {
14784
15270
  return result;
14785
15271
  }
14786
15272
  /** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
14787
- partialize(err, run) {
14788
- if (run.delivered.length === 0) return err;
14789
- return new PartialSendConflictError(
14790
- "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.",
14791
- run.firstPartialId ?? "",
14792
- run.delivered,
14793
- run.amount,
14794
- err
14795
- );
14796
- }
14797
- /** #441: possibly-committed errors must carry the transferId for the settling journal. */
14798
- stampTransferId(err, transferId) {
14799
- if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
14800
- err.transferId = transferId;
14801
- }
14802
- }
14803
15273
  async softAbort(transferId) {
14804
15274
  try {
14805
15275
  await this.deps.client.abortIntent(transferId);
@@ -14815,57 +15285,11 @@ var PaymentsFacade = class {
14815
15285
  const mintId = this.newId();
14816
15286
  this.activeMoneyOps.add(mintId);
14817
15287
  try {
14818
- return await this.mintUnderJournal(mintId, coinId, amount);
15288
+ return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
14819
15289
  } finally {
14820
15290
  this.activeMoneyOps.delete(mintId);
14821
15291
  }
14822
15292
  }
14823
- async mintUnderJournal(mintId, coinId, amount) {
14824
- const engine = this.engine();
14825
- const entry = {
14826
- mintId,
14827
- coinId,
14828
- amount: amount.toString(),
14829
- tokenId: "",
14830
- createdAt: this.nowMs()
14831
- };
14832
- await this.machineStores.mintJournal.upsert(entry);
14833
- let token;
14834
- try {
14835
- token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
14836
- } catch (err) {
14837
- this.heartbeat.arm();
14838
- return { success: false, error: messageOf(err) };
14839
- }
14840
- if (token.blob.tokenId !== entry.tokenId) {
14841
- await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
14842
- }
14843
- try {
14844
- await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
14845
- } catch (err) {
14846
- this.heartbeat.arm();
14847
- return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
14848
- }
14849
- return { success: true, tokenId: token.blob.tokenId };
14850
- }
14851
- async finalizeMint(engine, mintId, token, coinId, amount) {
14852
- const bytes = token.blob.token;
14853
- const digest = bytesToHex3(sha256(bytes));
14854
- const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
14855
- const key = keys.get(digest);
14856
- if (key === void 0) {
14857
- throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
14858
- }
14859
- await this.deps.storagePort.applyDelta({
14860
- transferId: mintId,
14861
- spent: [],
14862
- added: [{ tokenId: token.blob.tokenId, key }]
14863
- });
14864
- await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
14865
- await this.machineStores.mintJournal.removeByKey(mintId);
14866
- this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
14867
- this.trackTail(this.view.delta());
14868
- }
14869
15293
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
14870
15294
  async replayMints() {
14871
15295
  let resolved = 0;
@@ -14897,7 +15321,7 @@ var PaymentsFacade = class {
14897
15321
  if (token.blob.tokenId !== entry.tokenId) {
14898
15322
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
14899
15323
  }
14900
- await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
15324
+ await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
14901
15325
  return true;
14902
15326
  }
14903
15327
  async tokenInServerInventory(tokenId) {