@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
@@ -8212,6 +8212,8 @@ var FETCH_TIMEOUT_MS = 1e4;
8212
8212
  var TokenRegistry = class _TokenRegistry {
8213
8213
  static instance = null;
8214
8214
  definitionsById;
8215
+ /** Non-fungible definitions keyed by TOKEN TYPE — a separate namespace (#147). */
8216
+ definitionsByType;
8215
8217
  definitionsBySymbol;
8216
8218
  definitionsByName;
8217
8219
  // Remote refresh state
@@ -8230,6 +8232,7 @@ var TokenRegistry = class _TokenRegistry {
8230
8232
  inFlight = null;
8231
8233
  constructor() {
8232
8234
  this.definitionsById = /* @__PURE__ */ new Map();
8235
+ this.definitionsByType = /* @__PURE__ */ new Map();
8233
8236
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
8234
8237
  this.definitionsByName = /* @__PURE__ */ new Map();
8235
8238
  }
@@ -8457,9 +8460,11 @@ var TokenRegistry = class _TokenRegistry {
8457
8460
  this.definitionsById.clear();
8458
8461
  this.definitionsBySymbol.clear();
8459
8462
  this.definitionsByName.clear();
8463
+ this.definitionsByType.clear();
8460
8464
  for (const def of definitions) {
8461
8465
  const idLower = def.id.toLowerCase();
8462
8466
  this.definitionsById.set(idLower, def);
8467
+ if (def.assetKind === "non-fungible") this.definitionsByType.set(idLower, def);
8463
8468
  if (def.symbol) {
8464
8469
  this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
8465
8470
  }
@@ -8673,6 +8678,20 @@ var TokenRegistry = class _TokenRegistry {
8673
8678
  getAllDefinitions() {
8674
8679
  return Array.from(this.definitionsById.values());
8675
8680
  }
8681
+ /**
8682
+ * Definition for a coinless token's genesis TOKEN TYPE (#777). Never falls back
8683
+ * to `getDefinition`: a type and a coin id live in different namespaces, so a
8684
+ * type that collided with a coin id would otherwise render as that coin.
8685
+ */
8686
+ getTypeMeta(tokenType) {
8687
+ const def = this.getTypeDefinition(tokenType);
8688
+ if (!def) return null;
8689
+ return { name: def.name, iconUrl: def.icons?.[0]?.url ?? null };
8690
+ }
8691
+ getTypeDefinition(tokenType) {
8692
+ if (!tokenType) return void 0;
8693
+ return this.definitionsByType.get(tokenType.toLowerCase());
8694
+ }
8676
8695
  /**
8677
8696
  * Get all fungible token definitions
8678
8697
  * @returns Array of fungible token definitions
@@ -9508,6 +9527,8 @@ var import_DataHasherFactory = require("@unicitylabs/state-transition-sdk/lib/cr
9508
9527
  var import_CborSerializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js");
9509
9528
  var import_CborDeserializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js");
9510
9529
  var import_CborError = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js");
9530
+ var import_CborReader = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js");
9531
+ var import_MajorType = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js");
9511
9532
  var import_Asset = require("@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js");
9512
9533
  var import_AssetId = require("@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js");
9513
9534
  var import_PaymentAssetCollection = require("@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js");
@@ -9920,6 +9941,126 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
9920
9941
  return burntToken;
9921
9942
  }
9922
9943
 
9944
+ // token-engine/value-envelope.ts
9945
+ function isCoinlessEnvelope(envelope) {
9946
+ return envelope.startsWith("none_");
9947
+ }
9948
+ var SELF_DESCRIBED_CBOR_TAG = 55799n;
9949
+ var CBOR_MAJOR_TYPE_MASK = 224;
9950
+ var describe = (error) => error instanceof Error ? error.message : String(error);
9951
+ var invalid = (message) => new SphereError(message, "VALIDATION_ERROR");
9952
+ function majorTypeOf(bytes) {
9953
+ const first = bytes.at(0);
9954
+ return first === void 0 ? null : first & CBOR_MAJOR_TYPE_MASK;
9955
+ }
9956
+ function headTagNumber(bytes) {
9957
+ try {
9958
+ return new import_CborReader.CborReader(bytes).readLength(import_MajorType.MajorType.TAG);
9959
+ } catch {
9960
+ return null;
9961
+ }
9962
+ }
9963
+ function isAssetShaped(item) {
9964
+ try {
9965
+ return import_CborDeserializer.CborDeserializer.decodeArray(item, 2).every(
9966
+ (field) => majorTypeOf(field) === import_MajorType.MajorType.BYTE_STRING
9967
+ );
9968
+ } catch {
9969
+ return false;
9970
+ }
9971
+ }
9972
+ function arrayItemsOrNull(bytes) {
9973
+ try {
9974
+ return import_CborDeserializer.CborDeserializer.decodeArray(bytes);
9975
+ } catch {
9976
+ return null;
9977
+ }
9978
+ }
9979
+ function decodeSphere(genesisData) {
9980
+ try {
9981
+ return SpherePaymentData.fromCBOR(genesisData).toValue();
9982
+ } catch (error) {
9983
+ throw invalid(`Failed to decode token payment data: ${describe(error)}`);
9984
+ }
9985
+ }
9986
+ function taggedItemBody(genesisData) {
9987
+ try {
9988
+ return import_CborDeserializer.CborDeserializer.decodeTag(genesisData).data;
9989
+ } catch (error) {
9990
+ throw invalid(
9991
+ `Token value payload is a CBOR tag whose item is malformed or carries trailing bytes: ${describe(error)}`
9992
+ );
9993
+ }
9994
+ }
9995
+ function selfDescribedBodyCarriesValue(body) {
9996
+ const major = majorTypeOf(body);
9997
+ if (major === import_MajorType.MajorType.TAG) {
9998
+ const inner = headTagNumber(body);
9999
+ return inner === SpherePaymentData.CBOR_TAG || inner === SELF_DESCRIBED_CBOR_TAG;
10000
+ }
10001
+ if (major === import_MajorType.MajorType.ARRAY) {
10002
+ const items = arrayItemsOrNull(body);
10003
+ return items !== null && items.some(isAssetShaped);
10004
+ }
10005
+ return false;
10006
+ }
10007
+ function classifyTagged(genesisData) {
10008
+ const tag = headTagNumber(genesisData);
10009
+ if (tag === null) {
10010
+ throw invalid(
10011
+ "Token value payload has an unreadable or non-canonically encoded CBOR tag head \u2014 it could be a malformed SpherePaymentData envelope"
10012
+ );
10013
+ }
10014
+ if (tag === SpherePaymentData.CBOR_TAG) {
10015
+ return { envelope: "sphere", value: decodeSphere(genesisData) };
10016
+ }
10017
+ const body = taggedItemBody(genesisData);
10018
+ if (tag === SELF_DESCRIBED_CBOR_TAG && selfDescribedBodyCarriesValue(body)) {
10019
+ throw invalid(
10020
+ "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"
10021
+ );
10022
+ }
10023
+ return { envelope: "none_tag", value: null };
10024
+ }
10025
+ function classifyArray(genesisData) {
10026
+ const items = arrayItemsOrNull(genesisData);
10027
+ if (items === null) {
10028
+ throw invalid("Token value payload is not PaymentAssetCollection: malformed CBOR array");
10029
+ }
10030
+ return items.some(isAssetShaped) ? { envelope: "bare_collection", value: null } : { envelope: "none_other", value: null };
10031
+ }
10032
+ function classifyValueEnvelope(genesisData) {
10033
+ if (genesisData === null) return { envelope: "none_absent", value: null };
10034
+ const major = majorTypeOf(genesisData);
10035
+ if (major === null) return { envelope: "none_absent", value: null };
10036
+ if (major === import_MajorType.MajorType.TAG) return classifyTagged(genesisData);
10037
+ if (major === import_MajorType.MajorType.ARRAY) return classifyArray(genesisData);
10038
+ return { envelope: "none_other", value: null };
10039
+ }
10040
+ function wrapToken(sdkToken) {
10041
+ const { envelope, value } = classifyValueEnvelope(sdkToken.genesis.data);
10042
+ const blob = {
10043
+ tokenId: import_HexConverter.HexConverter.encode(sdkToken.id.bytes),
10044
+ token: sdkToken.toCBOR()
10045
+ };
10046
+ return {
10047
+ sdkToken,
10048
+ blob,
10049
+ value,
10050
+ valueEnvelope: envelope,
10051
+ tokenType: import_HexConverter.HexConverter.encode(sdkToken.type.bytes)
10052
+ };
10053
+ }
10054
+ function assertMintableData(data) {
10055
+ try {
10056
+ classifyValueEnvelope(data);
10057
+ } catch (error) {
10058
+ throw invalid(
10059
+ `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.`
10060
+ );
10061
+ }
10062
+ }
10063
+
9923
10064
  // token-engine/SphereTokenEngine.ts
9924
10065
  var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
9925
10066
  var MAX_MINT_CONCURRENCY = 8;
@@ -9976,7 +10117,7 @@ var SphereTokenEngine = class {
9976
10117
  return sdkToken.latestTransaction.data;
9977
10118
  }
9978
10119
  const data = sdkToken.genesis.data;
9979
- if (data && this.isSpherePaymentData(data)) {
10120
+ if (data && classifyValueEnvelope(data).envelope === "sphere") {
9980
10121
  return SpherePaymentData.fromCBOR(data).memo;
9981
10122
  }
9982
10123
  return null;
@@ -10006,9 +10147,10 @@ var SphereTokenEngine = class {
10006
10147
  );
10007
10148
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10008
10149
  const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
10009
- return this.wrapToken(token);
10150
+ return wrapToken(token);
10010
10151
  }
10011
10152
  async mintDataToken(params, options) {
10153
+ assertMintableData(params.data);
10012
10154
  const recipient = import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey);
10013
10155
  const tokenType = params.tokenType ? new import_TokenType.TokenType(params.tokenType) : import_TokenType.TokenType.generate();
10014
10156
  const salt = params.salt ? import_TokenSalt.TokenSalt.fromBytes(params.salt) : import_TokenSalt.TokenSalt.generate();
@@ -10023,7 +10165,7 @@ var SphereTokenEngine = class {
10023
10165
  );
10024
10166
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10025
10167
  const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
10026
- return this.wrapToken(token);
10168
+ return wrapToken(token);
10027
10169
  }
10028
10170
  async transfer(params, options) {
10029
10171
  this.assertOwned(params.token);
@@ -10046,13 +10188,19 @@ var SphereTokenEngine = class {
10046
10188
  );
10047
10189
  const certified = await transferTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10048
10190
  const transferred = await params.token.sdkToken.transfer(certified, this.deps.verificationContext);
10049
- return this.wrapToken(transferred);
10191
+ return wrapToken(transferred);
10050
10192
  }
10051
10193
  async split(params, options) {
10052
10194
  this.assertOwned(params.token);
10053
10195
  if (params.outputs.length === 0) {
10054
10196
  throw new SphereError("Split requires at least one output", "VALIDATION_ERROR");
10055
10197
  }
10198
+ if (params.token.value === null) {
10199
+ throw new SphereError(
10200
+ `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.`,
10201
+ "VALIDATION_ERROR"
10202
+ );
10203
+ }
10056
10204
  const transferId = this.resolveTransferId(options);
10057
10205
  const requests = params.outputs.map(
10058
10206
  (o, i) => import_SplitTokenRequest.SplitTokenRequest.create(
@@ -10174,7 +10322,7 @@ var SphereTokenEngine = class {
10174
10322
  const proof = await this.submitSplitMintLeg(certData, mintTx, options);
10175
10323
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10176
10324
  const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
10177
- return this.wrapToken(token);
10325
+ return wrapToken(token);
10178
10326
  }
10179
10327
  /**
10180
10328
  * Submit one split-mint leg. A `TRANSACTION_HASH_MISMATCH` on a mint leg is NOT a foreign spend
@@ -10232,7 +10380,7 @@ var SphereTokenEngine = class {
10232
10380
  "VALIDATION_ERROR"
10233
10381
  );
10234
10382
  }
10235
- return this.wrapToken(sdkToken);
10383
+ return wrapToken(sdkToken);
10236
10384
  }
10237
10385
  // ── internals ────────────────────────────────────────────────────────────────
10238
10386
  /**
@@ -10344,34 +10492,6 @@ var SphereTokenEngine = class {
10344
10492
  throw new SphereError("Cannot transfer a token not owned by this engine identity", "VALIDATION_ERROR");
10345
10493
  }
10346
10494
  }
10347
- /** Wrap an SDK token into a SphereToken: cache its blob (incl. stable tokenId) + decoded value. */
10348
- wrapToken(sdkToken) {
10349
- const data = sdkToken.genesis.data;
10350
- let value = null;
10351
- if (data && this.isSpherePaymentData(data)) {
10352
- try {
10353
- value = SpherePaymentData.fromCBOR(data).toValue();
10354
- } catch (err) {
10355
- throw new SphereError(
10356
- `Failed to decode token payment data: ${err instanceof Error ? err.message : String(err)}`,
10357
- "VALIDATION_ERROR"
10358
- );
10359
- }
10360
- }
10361
- const blob = {
10362
- tokenId: import_HexConverter.HexConverter.encode(sdkToken.id.bytes),
10363
- token: sdkToken.toCBOR()
10364
- };
10365
- return { sdkToken, blob, value };
10366
- }
10367
- /** True if the bytes are a SpherePaymentData envelope (value token) vs a raw data token. */
10368
- isSpherePaymentData(data) {
10369
- try {
10370
- return import_CborDeserializer.CborDeserializer.decodeTag(data).tag === SpherePaymentData.CBOR_TAG;
10371
- } catch {
10372
- return false;
10373
- }
10374
- }
10375
10495
  /**
10376
10496
  * Terminate the verification worker pool, if this engine was configured with
10377
10497
  * one. Idempotent — the pool's dispose only touches workers it spawned.
@@ -11767,6 +11887,17 @@ function createMachineStores(kv) {
11767
11887
  };
11768
11888
  }
11769
11889
 
11890
+ // modules/payments-v2/machine/payload-view.ts
11891
+ function isWholeIntent(p) {
11892
+ return p.kind === "whole" || p.kind === "coinless";
11893
+ }
11894
+ function splitOf(p) {
11895
+ return p.kind === "coin" ? p.split : void 0;
11896
+ }
11897
+ function coinIdOf(p) {
11898
+ return p.kind === "coin" ? p.coinId : void 0;
11899
+ }
11900
+
11770
11901
  // modules/payments-v2/machine/TransferMachine.ts
11771
11902
  var CERTIFY_WIDTH = 20;
11772
11903
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
@@ -11969,13 +12100,14 @@ var TransferMachine = class {
11969
12100
  const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
11970
12101
  return { recipientBlob: finished.blob.token };
11971
12102
  }
11972
- const split = ctx.payload.split;
12103
+ const split = splitOf(ctx.payload);
12104
+ const splitCoinId = coinIdOf(ctx.payload);
11973
12105
  const { outputs } = await engine.split(
11974
12106
  {
11975
12107
  token: source,
11976
12108
  outputs: [
11977
- { recipientPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.splitAmount) },
11978
- { recipientPubkey: this.deps.ownPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.remainderAmount) }
12109
+ { recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
12110
+ { recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
11979
12111
  ]
11980
12112
  },
11981
12113
  opts
@@ -12097,7 +12229,9 @@ var TransferMachine = class {
12097
12229
  const entry = {
12098
12230
  transferId: r.transferId,
12099
12231
  remainingAmount: undelivered.toString(),
12100
- coinId: r.payload.coinId,
12232
+ // '' only on a token-addressed intent, which is one leg: it either lands or
12233
+ // conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
12234
+ coinId: coinIdOf(r.payload) ?? "",
12101
12235
  recipient: r.payload.recipient,
12102
12236
  committedTokenIds: committed.map((o) => o.op.sourceTokenId),
12103
12237
  createdAt: this.deps.now()
@@ -12123,8 +12257,16 @@ var TransferMachine = class {
12123
12257
  }
12124
12258
  async finishBookkeeping(engine, transferId, payload, phase, committed) {
12125
12259
  try {
12126
- const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
12127
- await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
12260
+ const coinId = coinIdOf(payload);
12261
+ const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
12262
+ const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
12263
+ await this.deps.recordHistory?.({
12264
+ transferId,
12265
+ payload,
12266
+ phase,
12267
+ committedAmount,
12268
+ ...committedAssets !== void 0 ? { committedAssets } : {}
12269
+ });
12128
12270
  } catch {
12129
12271
  }
12130
12272
  try {
@@ -12138,6 +12280,18 @@ var TransferMachine = class {
12138
12280
  * a journal-rehydrated resume leg has no source token, but every committed
12139
12281
  * leg carries its recipient blob, so the outcome-derived rule is uniform).
12140
12282
  */
12283
+ /** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
12284
+ async settledAssets(engine, committed) {
12285
+ const total = /* @__PURE__ */ new Map();
12286
+ for (const outcome of committed) {
12287
+ if (outcome.recipientBlob === void 0) continue;
12288
+ const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
12289
+ for (const asset of engine.readValue(token)?.assets ?? []) {
12290
+ total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
12291
+ }
12292
+ }
12293
+ return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
12294
+ }
12141
12295
  async settledAmount(engine, coinId, committed) {
12142
12296
  let total = 0n;
12143
12297
  for (const outcome of committed) {
@@ -12166,8 +12320,11 @@ var TransferMachine = class {
12166
12320
  }
12167
12321
  };
12168
12322
  function conflictAmount(engine, payload, op, source) {
12169
- if (op.kind === "split") return BigInt(payload.split.splitAmount);
12170
- return source !== void 0 ? engine.balanceOf(source, payload.coinId) : 0n;
12323
+ const split = splitOf(payload);
12324
+ if (op.kind === "split") return BigInt(split.splitAmount);
12325
+ const coinId = coinIdOf(payload);
12326
+ if (coinId === void 0) return 0n;
12327
+ return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
12171
12328
  }
12172
12329
 
12173
12330
  // modules/payments-v2/machine/resume.ts
@@ -12274,6 +12431,14 @@ async function runOne(ctx, job, report) {
12274
12431
  report.failed.push(job.transferId);
12275
12432
  return;
12276
12433
  }
12434
+ if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
12435
+ logger.warn(
12436
+ "PaymentsV2",
12437
+ `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
12438
+ );
12439
+ report.failed.push(job.transferId);
12440
+ return;
12441
+ }
12277
12442
  mine.set(op.sourceTokenId, token);
12278
12443
  }
12279
12444
  try {
@@ -12304,6 +12469,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
12304
12469
  }
12305
12470
  report.failed.push(transferId);
12306
12471
  }
12472
+ function validateCoinPayload(c) {
12473
+ if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
12474
+ throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
12475
+ }
12476
+ const s = c.split;
12477
+ if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
12478
+ throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
12479
+ }
12480
+ return { ...c, kind: "coin" };
12481
+ }
12482
+ function validateWholePayload(p) {
12483
+ if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
12484
+ throw new SphereError(
12485
+ "token intent payload must name exactly one source token",
12486
+ "VALIDATION_ERROR"
12487
+ );
12488
+ }
12489
+ if (p.split !== void 0) {
12490
+ throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
12491
+ }
12492
+ return { ...p, kind: "whole", direct: [p.direct[0]] };
12493
+ }
12307
12494
  function validatePayload(raw) {
12308
12495
  const p = raw !== null && typeof raw === "object" ? raw : null;
12309
12496
  if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
@@ -12312,14 +12499,17 @@ function validatePayload(raw) {
12312
12499
  "VALIDATION_ERROR"
12313
12500
  );
12314
12501
  }
12315
- if (typeof p.recipient !== "string" || typeof p.coinId !== "string" || typeof p.amount !== "string") {
12316
- throw new SphereError("intent payload is missing recipient/coinId/amount", "VALIDATION_ERROR");
12502
+ if (typeof p.recipient !== "string") {
12503
+ throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
12317
12504
  }
12318
- const s = p.split;
12319
- if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
12320
- throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
12505
+ const kind = p.kind ?? "coin";
12506
+ if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
12507
+ throw new SphereError(
12508
+ `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
12509
+ "VALIDATION_ERROR"
12510
+ );
12321
12511
  }
12322
- return p;
12512
+ return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
12323
12513
  }
12324
12514
 
12325
12515
  // modules/payments-v2/convergence.ts
@@ -12525,13 +12715,24 @@ async function openRow(entry, journal, decryptPayload) {
12525
12715
  transferId: entry.transferId,
12526
12716
  kind: "open",
12527
12717
  recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
12528
- coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
12529
- amount: typeof payload?.amount === "string" ? payload.amount : "",
12718
+ ...subject(payload),
12530
12719
  legs: { certified, total: Math.max(total, certified) },
12531
12720
  deliveryPending: journal.length > 0,
12532
12721
  createdAt: entry.createdAt
12533
12722
  };
12534
12723
  }
12724
+ function subject(payload) {
12725
+ const rawKind = payload?.kind;
12726
+ if (rawKind === "whole" || rawKind === "coinless") {
12727
+ const tokenId = payload?.direct?.[0];
12728
+ return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
12729
+ }
12730
+ const coin = payload;
12731
+ return {
12732
+ coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
12733
+ amount: typeof coin?.amount === "string" ? coin.amount : ""
12734
+ };
12735
+ }
12535
12736
  function journalOnlyRow(transferId, entries) {
12536
12737
  const certified = distinctOpCount(entries);
12537
12738
  return {
@@ -12561,6 +12762,263 @@ function distinctOpCount(entries) {
12561
12762
  return new Set(entries.map((entry) => entry.opIndex)).size;
12562
12763
  }
12563
12764
 
12765
+ // modules/payments-v2/inventory/token-data.ts
12766
+ async function readTokenData(deps, tokenId) {
12767
+ if (deps.view.stateHashOf(tokenId) === void 0) {
12768
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
12769
+ }
12770
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
12771
+ if (bytes === void 0) {
12772
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
12773
+ }
12774
+ return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
12775
+ }
12776
+
12777
+ // modules/payments-v2/machine/payload.ts
12778
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
12779
+ return {
12780
+ v: 2,
12781
+ kind: "coin",
12782
+ recipient: recipientPubkey,
12783
+ coinId: request.coinId,
12784
+ amount: request.amount,
12785
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
12786
+ direct: [...spend.plan.direct],
12787
+ ...spend.plan.split !== void 0 ? {
12788
+ split: {
12789
+ tokenId: spend.plan.split.tokenId,
12790
+ splitAmount: spend.plan.split.splitAmount.toString(),
12791
+ remainderAmount: spend.plan.split.remainderAmount.toString()
12792
+ }
12793
+ } : {},
12794
+ spentStates
12795
+ };
12796
+ }
12797
+ function messageOf(err) {
12798
+ return err instanceof Error ? err.message : String(err);
12799
+ }
12800
+ function buildWholePayload(recipientPubkey, request, spentStates) {
12801
+ return {
12802
+ v: 2,
12803
+ kind: "whole",
12804
+ recipient: recipientPubkey,
12805
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
12806
+ direct: [request.tokenId],
12807
+ spentStates
12808
+ };
12809
+ }
12810
+
12811
+ // modules/payments-v2/mint-params.ts
12812
+ function mintParams(recipientPubkey, coinId, amount) {
12813
+ return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12814
+ }
12815
+
12816
+ // modules/payments-v2/mint.ts
12817
+ function runMintUnderJournal(deps, input) {
12818
+ return mintUnderJournal(deps, input);
12819
+ }
12820
+ async function mintUnderJournal(deps, input) {
12821
+ const { mintId, coinId, amount } = input;
12822
+ const engine = deps.engine;
12823
+ const entry = {
12824
+ mintId,
12825
+ coinId,
12826
+ amount: amount.toString(),
12827
+ tokenId: "",
12828
+ createdAt: deps.now()
12829
+ };
12830
+ await deps.mintJournal.upsert(entry);
12831
+ let token;
12832
+ try {
12833
+ token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
12834
+ } catch (err) {
12835
+ deps.armHeartbeat();
12836
+ return { success: false, error: messageOf(err) };
12837
+ }
12838
+ if (token.blob.tokenId !== entry.tokenId) {
12839
+ await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
12840
+ }
12841
+ try {
12842
+ await finalizeMint(deps, mintId, token, coinId, amount.toString());
12843
+ } catch (err) {
12844
+ deps.armHeartbeat();
12845
+ return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
12846
+ }
12847
+ return { success: true, tokenId: token.blob.tokenId };
12848
+ }
12849
+ async function finalizeMint(deps, mintId, token, coinId, amount) {
12850
+ const engine = deps.engine;
12851
+ const bytes = token.blob.token;
12852
+ const digest = bytesToHex3(sha256(bytes));
12853
+ const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
12854
+ const key = keys.get(digest);
12855
+ if (key === void 0) {
12856
+ throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
12857
+ }
12858
+ await deps.storagePort.applyDelta({
12859
+ transferId: mintId,
12860
+ spent: [],
12861
+ added: [{ tokenId: token.blob.tokenId, key }]
12862
+ });
12863
+ await deps.recordMint({
12864
+ tokenId: token.blob.tokenId,
12865
+ assets: [{ coinId, amount }]
12866
+ });
12867
+ await deps.mintJournal.removeByKey(mintId);
12868
+ deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
12869
+ deps.refreshView();
12870
+ }
12871
+
12872
+ // modules/payments-v2/inventory/presentation.ts
12873
+ function toToken(tokenId, snapshot, asset, registry, flags) {
12874
+ const iconUrl = registry.getIconUrl(asset.coinId);
12875
+ return {
12876
+ id: tokenId,
12877
+ coinId: asset.coinId,
12878
+ symbol: registry.getSymbol(asset.coinId),
12879
+ name: registry.getName(asset.coinId),
12880
+ decimals: registry.getDecimals(asset.coinId),
12881
+ ...iconUrl !== null ? { iconUrl } : {},
12882
+ amount: asset.amount,
12883
+ status: flags.transferring ? "transferring" : "confirmed",
12884
+ createdAt: snapshot.createdAt,
12885
+ updatedAt: snapshot.updatedAt,
12886
+ lazy: true,
12887
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12888
+ };
12889
+ }
12890
+ function aggregateAssets(entries, isTransferring, registry) {
12891
+ const groups = /* @__PURE__ */ new Map();
12892
+ for (const [tokenId, entry] of entries) {
12893
+ const moving = isTransferring(tokenId);
12894
+ for (const asset of entry.assets) {
12895
+ let group = groups.get(asset.coinId);
12896
+ if (!group) {
12897
+ group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12898
+ groups.set(asset.coinId, group);
12899
+ }
12900
+ if (moving) {
12901
+ group.transferring += BigInt(asset.amount);
12902
+ group.transferringCount += 1;
12903
+ } else {
12904
+ group.total += BigInt(asset.amount);
12905
+ group.count += 1;
12906
+ }
12907
+ }
12908
+ }
12909
+ return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12910
+ }
12911
+ function toAsset(coinId, totals, registry) {
12912
+ const iconUrl = registry.getIconUrl(coinId);
12913
+ return {
12914
+ coinId,
12915
+ symbol: registry.getSymbol(coinId),
12916
+ name: registry.getName(coinId),
12917
+ decimals: registry.getDecimals(coinId),
12918
+ ...iconUrl !== null ? { iconUrl } : {},
12919
+ totalAmount: totals.total.toString(),
12920
+ tokenCount: totals.count,
12921
+ confirmedAmount: totals.total.toString(),
12922
+ unconfirmedAmount: "0",
12923
+ confirmedTokenCount: totals.count,
12924
+ unconfirmedTokenCount: 0,
12925
+ transferringTokenCount: totals.transferringCount,
12926
+ transferringAmount: totals.transferring.toString(),
12927
+ priceUsd: null,
12928
+ priceEur: null,
12929
+ change24h: null,
12930
+ fiatValueUsd: null,
12931
+ fiatValueEur: null
12932
+ };
12933
+ }
12934
+ function priceIdOf(registry, coinId) {
12935
+ return registry.getName(coinId).toLowerCase();
12936
+ }
12937
+ async function withPrices(raw, registry, price) {
12938
+ try {
12939
+ const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
12940
+ const quotes = await price.getPrices(ids);
12941
+ return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
12942
+ } catch {
12943
+ return raw;
12944
+ }
12945
+ }
12946
+ function priceOne(asset, quote) {
12947
+ if (!quote) return asset;
12948
+ const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
12949
+ return {
12950
+ ...asset,
12951
+ priceUsd: quote.priceUsd,
12952
+ priceEur: quote.priceEur ?? null,
12953
+ change24h: quote.change24h ?? null,
12954
+ fiatValueUsd: human * quote.priceUsd,
12955
+ fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12956
+ };
12957
+ }
12958
+ function transferringToken(tokenId, coinId, amount, registry, now) {
12959
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
12960
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12961
+ transferring: true,
12962
+ suspectedSpent: false
12963
+ });
12964
+ }
12965
+
12966
+ // modules/payments-v2/send-whole.ts
12967
+ async function materializeWholeSpend(deps, input) {
12968
+ const { engine } = deps;
12969
+ const tokenId = input.request.tokenId;
12970
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
12971
+ if (bytes === void 0) {
12972
+ throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
12973
+ }
12974
+ const token = await engine.decodeToken({ tokenId, token: bytes });
12975
+ if (token.valueEnvelope === "bare_collection") {
12976
+ throw new SphereError(
12977
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
12978
+ "VALIDATION_ERROR"
12979
+ );
12980
+ }
12981
+ if (input.requireCoinless && token.value !== null) {
12982
+ throw new SphereError(
12983
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
12984
+ "VALIDATION_ERROR"
12985
+ );
12986
+ }
12987
+ const keys = await engine.deliveryKeys(bytes);
12988
+ const payload = buildWholePayload(input.recipientPubkey, input.request, {
12989
+ [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
12990
+ });
12991
+ const plan = {
12992
+ transferId: input.transferId,
12993
+ recipientPubkey: input.recipientPubkey,
12994
+ payload,
12995
+ ops: buildOps(payload),
12996
+ sources: /* @__PURE__ */ new Map([[tokenId, token]]),
12997
+ ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
12998
+ };
12999
+ const sourceTokens = (token.value?.assets ?? []).map(
13000
+ (asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
13001
+ );
13002
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
13003
+ }
13004
+
13005
+ // modules/payments-v2/send-errors.ts
13006
+ function partialize(err, run) {
13007
+ if (run.delivered.length === 0) return err;
13008
+ return new PartialSendConflictError(
13009
+ "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.",
13010
+ run.firstPartialId ?? "",
13011
+ [...run.delivered],
13012
+ run.amount,
13013
+ err
13014
+ );
13015
+ }
13016
+ function stampTransferId(err, transferId) {
13017
+ if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
13018
+ err.transferId = transferId;
13019
+ }
13020
+ }
13021
+
12564
13022
  // modules/payments-v2/recipient.ts
12565
13023
  var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
12566
13024
  async function requireSameNetworkRecipient(deps, identifier) {
@@ -12609,11 +13067,6 @@ async function splitOpIndex(deps, envelope) {
12609
13067
  return Array.isArray(payload.direct) ? payload.direct.length : 0;
12610
13068
  }
12611
13069
 
12612
- // modules/payments-v2/mint-params.ts
12613
- function mintParams(recipientPubkey, coinId, amount) {
12614
- return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
12615
- }
12616
-
12617
13070
  // modules/payments-v2/prewarm-cache.ts
12618
13071
  var PrewarmCache = class _PrewarmCache {
12619
13072
  entries = /* @__PURE__ */ new Map();
@@ -12693,124 +13146,15 @@ async function takeSourceBlobs(deps, sourceIds) {
12693
13146
  return blobs;
12694
13147
  }
12695
13148
 
12696
- // modules/payments-v2/inventory/presentation.ts
12697
- function toToken(tokenId, snapshot, asset, registry, flags) {
12698
- const iconUrl = registry.getIconUrl(asset.coinId);
12699
- return {
12700
- id: tokenId,
12701
- coinId: asset.coinId,
12702
- symbol: registry.getSymbol(asset.coinId),
12703
- name: registry.getName(asset.coinId),
12704
- decimals: registry.getDecimals(asset.coinId),
12705
- ...iconUrl !== null ? { iconUrl } : {},
12706
- amount: asset.amount,
12707
- status: flags.transferring ? "transferring" : "confirmed",
12708
- createdAt: snapshot.createdAt,
12709
- updatedAt: snapshot.updatedAt,
12710
- lazy: true,
12711
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12712
- };
12713
- }
12714
- function aggregateAssets(entries, isTransferring, registry) {
12715
- const groups = /* @__PURE__ */ new Map();
12716
- for (const [tokenId, entry] of entries) {
12717
- const moving = isTransferring(tokenId);
12718
- for (const asset of entry.assets) {
12719
- let group = groups.get(asset.coinId);
12720
- if (!group) {
12721
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12722
- groups.set(asset.coinId, group);
12723
- }
12724
- if (moving) {
12725
- group.transferring += BigInt(asset.amount);
12726
- group.transferringCount += 1;
12727
- } else {
12728
- group.total += BigInt(asset.amount);
12729
- group.count += 1;
12730
- }
12731
- }
12732
- }
12733
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12734
- }
12735
- function toAsset(coinId, totals, registry) {
12736
- const iconUrl = registry.getIconUrl(coinId);
12737
- return {
12738
- coinId,
12739
- symbol: registry.getSymbol(coinId),
12740
- name: registry.getName(coinId),
12741
- decimals: registry.getDecimals(coinId),
12742
- ...iconUrl !== null ? { iconUrl } : {},
12743
- totalAmount: totals.total.toString(),
12744
- tokenCount: totals.count,
12745
- confirmedAmount: totals.total.toString(),
12746
- unconfirmedAmount: "0",
12747
- confirmedTokenCount: totals.count,
12748
- unconfirmedTokenCount: 0,
12749
- transferringTokenCount: totals.transferringCount,
12750
- transferringAmount: totals.transferring.toString(),
12751
- priceUsd: null,
12752
- priceEur: null,
12753
- change24h: null,
12754
- fiatValueUsd: null,
12755
- fiatValueEur: null
12756
- };
12757
- }
12758
- function priceIdOf(registry, coinId) {
12759
- return registry.getName(coinId).toLowerCase();
12760
- }
12761
- async function withPrices(raw, registry, price) {
12762
- try {
12763
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
12764
- const quotes = await price.getPrices(ids);
12765
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
12766
- } catch {
12767
- return raw;
12768
- }
12769
- }
12770
- function priceOne(asset, quote) {
12771
- if (!quote) return asset;
12772
- const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
12773
- return {
12774
- ...asset,
12775
- priceUsd: quote.priceUsd,
12776
- priceEur: quote.priceEur ?? null,
12777
- change24h: quote.change24h ?? null,
12778
- fiatValueUsd: human * quote.priceUsd,
12779
- fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12780
- };
12781
- }
12782
- function transferringToken(tokenId, coinId, amount, registry, now) {
12783
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
12784
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12785
- transferring: true,
12786
- suspectedSpent: false
12787
- });
12788
- }
12789
-
12790
- // modules/payments-v2/machine/payload.ts
12791
- function buildPayload(recipientPubkey, request, spend, spentStates) {
12792
- return {
12793
- v: 2,
12794
- recipient: recipientPubkey,
12795
- coinId: request.coinId,
12796
- amount: request.amount,
12797
- ...request.memo !== void 0 ? { memo: request.memo } : {},
12798
- direct: [...spend.plan.direct],
12799
- ...spend.plan.split !== void 0 ? {
12800
- split: {
12801
- tokenId: spend.plan.split.tokenId,
12802
- splitAmount: spend.plan.split.splitAmount.toString(),
12803
- remainderAmount: spend.plan.split.remainderAmount.toString()
12804
- }
12805
- } : {},
12806
- spentStates
12807
- };
13149
+ // modules/payments-v2/history/History.ts
13150
+ function wireAssets(assets) {
13151
+ return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
12808
13152
  }
12809
- function messageOf(err) {
12810
- return err instanceof Error ? err.message : String(err);
13153
+ function isPermanentReject(err) {
13154
+ const status = err?.status;
13155
+ if (typeof status !== "number") return false;
13156
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
12811
13157
  }
12812
-
12813
- // modules/payments-v2/history/History.ts
12814
13158
  var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
12815
13159
  var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
12816
13160
  var History = class {
@@ -12835,7 +13179,7 @@ var History = class {
12835
13179
  dedupKey: input.transferId,
12836
13180
  id: (this.deps.newId ?? randomUUID)(),
12837
13181
  type: "SENT",
12838
- assets: [{ coinId: input.coinId, amount: input.amount }],
13182
+ assets: wireAssets(input.assets),
12839
13183
  ts: this.ts(input.timestamp),
12840
13184
  transferId: input.transferId,
12841
13185
  ...this.wireTokenId(input.tokenId),
@@ -12848,7 +13192,7 @@ var History = class {
12848
13192
  dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
12849
13193
  id: (this.deps.newId ?? randomUUID)(),
12850
13194
  type: "RECEIVED",
12851
- assets: [{ coinId: input.coinId, amount: input.amount }],
13195
+ assets: wireAssets(input.assets),
12852
13196
  ts: this.ts(input.timestamp),
12853
13197
  ...input.transferId !== void 0 ? { transferId: input.transferId } : {},
12854
13198
  ...this.wireTokenId(input.tokenId),
@@ -12861,7 +13205,7 @@ var History = class {
12861
13205
  dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
12862
13206
  id: (this.deps.newId ?? randomUUID)(),
12863
13207
  type: "MINT",
12864
- assets: [{ coinId: input.coinId, amount: input.amount }],
13208
+ assets: wireAssets(input.assets),
12865
13209
  ts: this.ts(input.timestamp),
12866
13210
  ...this.wireTokenId(input.tokenId)
12867
13211
  }));
@@ -12872,7 +13216,10 @@ var History = class {
12872
13216
  try {
12873
13217
  await this.deps.client.postHistory([record]);
12874
13218
  } catch (err) {
12875
- this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
13219
+ this.deps.log?.(
13220
+ isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
13221
+ err
13222
+ );
12876
13223
  return;
12877
13224
  }
12878
13225
  this.deps.emit("history:updated", this.toEntry(record));
@@ -12939,6 +13286,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
12939
13286
  function stateKey(tokenId, stateHash) {
12940
13287
  return `${tokenId}:${stateHash}`;
12941
13288
  }
13289
+ function unchanged(prev, item, tokenType) {
13290
+ return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
13291
+ }
13292
+ function isCoinless(item, prev) {
13293
+ if (item.status !== "active") return prev?.coinless ?? false;
13294
+ return (item.assets?.length ?? 0) === 0;
13295
+ }
12942
13296
  var InventoryView = class {
12943
13297
  constructor(deps) {
12944
13298
  this.deps = deps;
@@ -13032,14 +13386,26 @@ var InventoryView = class {
13032
13386
  pool(coinId) {
13033
13387
  const out = [];
13034
13388
  for (const [tokenId, entry] of this.mirror) {
13035
- if (entry.status !== "active") continue;
13036
- if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
13037
- if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
13389
+ if (!this.isSpendable(tokenId, entry)) continue;
13038
13390
  const asset = entry.assets.find((a) => a.coinId === coinId);
13039
13391
  if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
13040
13392
  }
13041
13393
  return out;
13042
13394
  }
13395
+ /**
13396
+ * The eligibility gates every spend shares, coin or token — ONE definition, so a
13397
+ * single probe covers both verbs and neither can drift from the other.
13398
+ */
13399
+ isSpendable(tokenId, entry) {
13400
+ if (entry.status !== "active") return false;
13401
+ if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13402
+ return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13403
+ }
13404
+ /** Is this NAMED token spendable whole — coinless or valued (#777)? */
13405
+ spendableToken(tokenId) {
13406
+ const entry = this.mirror.get(tokenId);
13407
+ return entry !== void 0 && this.isSpendable(tokenId, entry);
13408
+ }
13043
13409
  /**
13044
13410
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
13045
13411
  * or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
@@ -13065,6 +13431,29 @@ var InventoryView = class {
13065
13431
  }
13066
13432
  return out;
13067
13433
  }
13434
+ /**
13435
+ * Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
13436
+ * exactly when it is not, so no mirror row can appear in both reads.
13437
+ */
13438
+ coinless(registry) {
13439
+ const out = [];
13440
+ for (const [tokenId, entry] of this.mirror) {
13441
+ if (entry.status !== "active" || !entry.coinless) continue;
13442
+ const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
13443
+ out.push({
13444
+ tokenId,
13445
+ ...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
13446
+ ...meta ? { name: meta.name } : {},
13447
+ ...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
13448
+ stateHash: entry.stateHash,
13449
+ transferring: this.held(tokenId),
13450
+ ...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
13451
+ createdAt: entry.createdAt,
13452
+ updatedAt: entry.updatedAt
13453
+ });
13454
+ }
13455
+ return out;
13456
+ }
13068
13457
  async assets(registry, price) {
13069
13458
  const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
13070
13459
  if (!price || raw.length === 0) return raw;
@@ -13136,14 +13525,19 @@ var InventoryView = class {
13136
13525
  applyOne(item, now) {
13137
13526
  const prev = this.mirror.get(item.tokenId);
13138
13527
  if (prev && item.seq < prev.seq) return false;
13139
- if (prev && prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash) {
13140
- return false;
13141
- }
13528
+ const tokenType = item.tokenType ?? prev?.tokenType;
13529
+ if (prev && unchanged(prev, item, tokenType)) return false;
13142
13530
  this.mirror.set(item.tokenId, {
13143
13531
  stateHash: item.stateHash,
13144
13532
  seq: item.seq,
13145
13533
  status: item.status,
13146
- assets: item.assets ?? prev?.assets ?? [],
13534
+ // ONLY a tombstone inherits: it omits assets for an unrelated reason and
13535
+ // recoverRemoved must know the amount it restores. An ACTIVE row that omits
13536
+ // them is stating coinlessness, and inheriting there would keep stale assets
13537
+ // in tokens() while `coinless` is true — the row in BOTH reads (§16).
13538
+ assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
13539
+ coinless: isCoinless(item, prev),
13540
+ ...tokenType !== void 0 ? { tokenType } : {},
13147
13541
  createdAt: prev?.createdAt ?? now,
13148
13542
  updatedAt: now
13149
13543
  });
@@ -13379,7 +13773,12 @@ async function screen(deps, engine, entry) {
13379
13773
  }
13380
13774
  return {
13381
13775
  kind: "accept",
13382
- record: { tokenId: keys.tokenId, stateHash: keys.stateHash, assets: toAssetAmounts(token) }
13776
+ record: {
13777
+ tokenId: keys.tokenId,
13778
+ stateHash: keys.stateHash,
13779
+ assets: toAssetAmounts(token),
13780
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
13781
+ }
13383
13782
  };
13384
13783
  }
13385
13784
  async function announce(deps, entry, record) {
@@ -13390,6 +13789,7 @@ async function announce(deps, entry, record) {
13390
13789
  tokenId: record.tokenId,
13391
13790
  stateHash: record.stateHash,
13392
13791
  assets: record.assets,
13792
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
13393
13793
  ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
13394
13794
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13395
13795
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
@@ -13403,6 +13803,19 @@ async function announce(deps, entry, record) {
13403
13803
  senderPubkey: entry.senderPubkey ?? "",
13404
13804
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13405
13805
  tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
13806
+ // #777: named here rather than mapped from assets, which announced an EMPTY list.
13807
+ ...record.tokenType !== void 0 ? {
13808
+ coinless: [
13809
+ {
13810
+ tokenId: record.tokenId,
13811
+ tokenType: record.tokenType,
13812
+ stateHash: record.stateHash,
13813
+ transferring: false,
13814
+ createdAt: receivedAt,
13815
+ updatedAt: receivedAt
13816
+ }
13817
+ ]
13818
+ } : {},
13406
13819
  ...entry.memo !== void 0 ? { memo: entry.memo } : {},
13407
13820
  receivedAt
13408
13821
  };
@@ -14068,6 +14481,34 @@ var SpendQueue = class {
14068
14481
  }
14069
14482
  return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
14070
14483
  }
14484
+ /**
14485
+ * #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
14486
+ * for. A coin plan waits because some OTHER combination may free up; a named
14487
+ * token is either free now or held by a reservation that only its own transfer
14488
+ * can release, so queueing would block until a timeout on a spend that cannot
14489
+ * become possible.
14490
+ */
14491
+ planWhole(reservationId, tokenId) {
14492
+ if (this.destroyed) {
14493
+ throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
14494
+ }
14495
+ const unproven = this.deps.ledger.unprovenReason();
14496
+ if (unproven !== null) {
14497
+ throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
14498
+ }
14499
+ if (this.deps.spendableToken?.(tokenId) !== true) {
14500
+ throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
14501
+ }
14502
+ const holder = this.deps.ledger.holderOf(tokenId);
14503
+ if (holder !== void 0) {
14504
+ throw new SphereError(
14505
+ `Token ${tokenId} is already reserved by transfer ${holder}`,
14506
+ "VALIDATION_ERROR"
14507
+ );
14508
+ }
14509
+ this.deps.ledger.reserve(reservationId, [{ tokenId }]);
14510
+ return { reservationId, plan: { direct: [tokenId] } };
14511
+ }
14071
14512
  /**
14072
14513
  * The sources `plan()` WOULD pick right now, reserving nothing and queueing
14073
14514
  * nothing — a read-only probe used to warm blobs while the user is still
@@ -14257,6 +14698,7 @@ function composeFacadeParts(deps, hooks) {
14257
14698
  const queue = new SpendQueue({
14258
14699
  ledger,
14259
14700
  getPool: (coinId) => view.pool(coinId),
14701
+ spendableToken: (tokenId) => view.spendableToken(tokenId),
14260
14702
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
14261
14703
  });
14262
14704
  const historyStore = new History({
@@ -14337,13 +14779,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
14337
14779
  ...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
14338
14780
  emit: deps.emit,
14339
14781
  now: deps.now ?? Date.now,
14340
- recordHistory: async ({ transferId, payload, committedAmount }) => {
14782
+ recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
14341
14783
  await historyStore.recordSent({
14342
14784
  transferId,
14343
- coinId: payload.coinId,
14344
- // §5.9: the SETTLED amount (machine-computed from the certified
14345
- // recipient blobs), never payload.amount — the plan.
14346
- amount: committedAmount,
14785
+ // §5.9: the SETTLED amount, never payload.amount — the plan. A token
14786
+ // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
14787
+ ...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
14347
14788
  recipientPubkey: payload.recipient,
14348
14789
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
14349
14790
  });
@@ -14378,12 +14819,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
14378
14819
  });
14379
14820
  }
14380
14821
  async function recordReceived(historyStore, record) {
14381
- const first = record.assets[0];
14382
14822
  await historyStore.recordReceived({
14383
14823
  tokenId: record.tokenId,
14384
14824
  stateHash: record.stateHash,
14385
- coinId: first?.coinId ?? "",
14386
- amount: first?.amount ?? "0",
14825
+ assets: record.assets,
14387
14826
  ...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
14388
14827
  ...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
14389
14828
  ...record.memo !== void 0 ? { memo: record.memo } : {},
@@ -14531,6 +14970,13 @@ var PaymentsFacade = class {
14531
14970
  tokens(filter) {
14532
14971
  return this.view.tokens(this.deps.registry, filter);
14533
14972
  }
14973
+ coinless() {
14974
+ return this.view.coinless(this.deps.registry);
14975
+ }
14976
+ tokenData(tokenId) {
14977
+ const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
14978
+ return readTokenData(deps, tokenId);
14979
+ }
14534
14980
  history(page) {
14535
14981
  return this.historyStore.page(page ?? {});
14536
14982
  }
@@ -14553,7 +14999,14 @@ var PaymentsFacade = class {
14553
14999
  return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
14554
15000
  }
14555
15001
  send(request) {
14556
- return this.track(this.sendOutcome(request));
15002
+ return this.track(this.runJob({ kind: "coin", request }, request.amount));
15003
+ }
15004
+ sendWholeToken(request) {
15005
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
15006
+ }
15007
+ /** NFT-scoped twin: refuses a valued source. */
15008
+ sendCoinless(request) {
15009
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
14557
15010
  }
14558
15011
  async receive() {
14559
15012
  const transfers = await this.track(this.receiveLoop.drainOnce());
@@ -14585,12 +15038,25 @@ var PaymentsFacade = class {
14585
15038
  nowMs() {
14586
15039
  return (this.deps.now ?? Date.now)();
14587
15040
  }
15041
+ mintDeps() {
15042
+ return {
15043
+ engine: this.engine(),
15044
+ mintJournal: this.machineStores.mintJournal,
15045
+ storagePort: this.deps.storagePort,
15046
+ recordMint: (input) => this.historyStore.recordMint(input),
15047
+ armHeartbeat: () => this.heartbeat.arm(),
15048
+ noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
15049
+ refreshView: () => this.trackTail(this.view.delta()),
15050
+ ownPubkeyBytes: this.ownPubkeyBytes,
15051
+ now: () => this.nowMs()
15052
+ };
15053
+ }
14588
15054
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
14589
15055
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
14590
15056
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
14591
- async sendOutcome(request) {
15057
+ async runJob(job, amount) {
14592
15058
  const run = {
14593
- amount: request.amount,
15059
+ amount,
14594
15060
  delivered: [],
14595
15061
  sentTokens: [],
14596
15062
  tokenTransfers: [],
@@ -14600,7 +15066,7 @@ var PaymentsFacade = class {
14600
15066
  lastTransferId: ""
14601
15067
  };
14602
15068
  try {
14603
- return await this.sendWithPolicy(request, run);
15069
+ return await this.sendWithPolicy(job, run);
14604
15070
  } catch (err) {
14605
15071
  this.emitCleanFailure(err, run.lastTransferId);
14606
15072
  throw err;
@@ -14620,19 +15086,19 @@ var PaymentsFacade = class {
14620
15086
  };
14621
15087
  this.deps.emit("transfer:updated", failed);
14622
15088
  }
14623
- async sendWithPolicy(request, run) {
14624
- const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
15089
+ async sendWithPolicy(job, run) {
15090
+ const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
14625
15091
  for (let attempt = 0; ; attempt++) {
14626
15092
  let ctx;
14627
15093
  try {
14628
- ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
15094
+ ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
14629
15095
  } catch (err) {
14630
- throw this.partialize(err, run);
15096
+ throw partialize(err, run);
14631
15097
  }
14632
15098
  const disposition = await this.runAttempt(ctx);
14633
- if (disposition.kind === "rethrow") throw this.partialize(disposition.error, run);
15099
+ if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
14634
15100
  if (disposition.kind === "retry-full") {
14635
- if (attempt >= MAX_RESELECT) throw this.partialize(disposition.error, run);
15101
+ if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
14636
15102
  continue;
14637
15103
  }
14638
15104
  if (disposition.kind === "success") {
@@ -14688,7 +15154,7 @@ var PaymentsFacade = class {
14688
15154
  async disposeFailedAttempt(ctx, err) {
14689
15155
  if (isPossiblyCommittedSendOutcome(err)) {
14690
15156
  this.settleKeepOpen(ctx);
14691
- this.stampTransferId(err, ctx.transferId);
15157
+ stampTransferId(err, ctx.transferId);
14692
15158
  return { kind: "rethrow", error: err };
14693
15159
  }
14694
15160
  const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
@@ -14722,7 +15188,7 @@ var PaymentsFacade = class {
14722
15188
  async disposeConvergeFailure(ctx, err) {
14723
15189
  if (isPossiblyCommittedSendOutcome(err)) {
14724
15190
  this.settleKeepOpen(ctx);
14725
- this.stampTransferId(err, ctx.transferId);
15191
+ stampTransferId(err, ctx.transferId);
14726
15192
  return { kind: "rethrow", error: err };
14727
15193
  }
14728
15194
  if (classifyError(err) === "conflict") {
@@ -14733,9 +15199,29 @@ var PaymentsFacade = class {
14733
15199
  }
14734
15200
  return { kind: "rethrow", error: err };
14735
15201
  }
14736
- async planAndMaterialize(recipientPubkey, request, run) {
15202
+ async planAndMaterialize(recipientPubkey, job, run) {
14737
15203
  const transferId = this.newId();
14738
15204
  run.lastTransferId = transferId;
15205
+ if (job.kind === "whole") {
15206
+ const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
15207
+ const sourceIds2 = this.markPlanned(transferId, "", spend2);
15208
+ try {
15209
+ return await materializeWholeSpend(
15210
+ { engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
15211
+ {
15212
+ transferId,
15213
+ recipientPubkey,
15214
+ request: job.request,
15215
+ sourceIds: sourceIds2,
15216
+ requireCoinless: job.requireCoinless
15217
+ }
15218
+ );
15219
+ } catch (err) {
15220
+ this.settleFailure(transferId, "", sourceIds2);
15221
+ throw err;
15222
+ }
15223
+ }
15224
+ const request = { ...job.request, amount: run.amount };
14739
15225
  const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
14740
15226
  const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
14741
15227
  const sourceIds = this.markPlanned(transferId, request.coinId, spend);
@@ -14869,22 +15355,6 @@ var PaymentsFacade = class {
14869
15355
  return result;
14870
15356
  }
14871
15357
  /** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
14872
- partialize(err, run) {
14873
- if (run.delivered.length === 0) return err;
14874
- return new PartialSendConflictError(
14875
- "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.",
14876
- run.firstPartialId ?? "",
14877
- run.delivered,
14878
- run.amount,
14879
- err
14880
- );
14881
- }
14882
- /** #441: possibly-committed errors must carry the transferId for the settling journal. */
14883
- stampTransferId(err, transferId) {
14884
- if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
14885
- err.transferId = transferId;
14886
- }
14887
- }
14888
15358
  async softAbort(transferId) {
14889
15359
  try {
14890
15360
  await this.deps.client.abortIntent(transferId);
@@ -14900,57 +15370,11 @@ var PaymentsFacade = class {
14900
15370
  const mintId = this.newId();
14901
15371
  this.activeMoneyOps.add(mintId);
14902
15372
  try {
14903
- return await this.mintUnderJournal(mintId, coinId, amount);
15373
+ return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
14904
15374
  } finally {
14905
15375
  this.activeMoneyOps.delete(mintId);
14906
15376
  }
14907
15377
  }
14908
- async mintUnderJournal(mintId, coinId, amount) {
14909
- const engine = this.engine();
14910
- const entry = {
14911
- mintId,
14912
- coinId,
14913
- amount: amount.toString(),
14914
- tokenId: "",
14915
- createdAt: this.nowMs()
14916
- };
14917
- await this.machineStores.mintJournal.upsert(entry);
14918
- let token;
14919
- try {
14920
- token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
14921
- } catch (err) {
14922
- this.heartbeat.arm();
14923
- return { success: false, error: messageOf(err) };
14924
- }
14925
- if (token.blob.tokenId !== entry.tokenId) {
14926
- await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
14927
- }
14928
- try {
14929
- await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
14930
- } catch (err) {
14931
- this.heartbeat.arm();
14932
- return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
14933
- }
14934
- return { success: true, tokenId: token.blob.tokenId };
14935
- }
14936
- async finalizeMint(engine, mintId, token, coinId, amount) {
14937
- const bytes = token.blob.token;
14938
- const digest = bytesToHex3(sha256(bytes));
14939
- const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
14940
- const key = keys.get(digest);
14941
- if (key === void 0) {
14942
- throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
14943
- }
14944
- await this.deps.storagePort.applyDelta({
14945
- transferId: mintId,
14946
- spent: [],
14947
- added: [{ tokenId: token.blob.tokenId, key }]
14948
- });
14949
- await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
14950
- await this.machineStores.mintJournal.removeByKey(mintId);
14951
- this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
14952
- this.trackTail(this.view.delta());
14953
- }
14954
15378
  /** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
14955
15379
  async replayMints() {
14956
15380
  let resolved = 0;
@@ -14982,7 +15406,7 @@ var PaymentsFacade = class {
14982
15406
  if (token.blob.tokenId !== entry.tokenId) {
14983
15407
  await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
14984
15408
  }
14985
- await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
15409
+ await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
14986
15410
  return true;
14987
15411
  }
14988
15412
  async tokenInServerInventory(tokenId) {