@unicitylabs/sphere-sdk 0.18.0-dev.2 → 0.18.0-dev.3

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 (58) hide show
  1. package/dist/connect/chunks/{chunk-OAAY2JEA.js → chunk-7BZL7HE2.js} +3 -3
  2. package/dist/connect/chunks/{chunk-MQQ3TMHM.js → chunk-CJ4D6M6W.js} +2 -2
  3. package/dist/connect/chunks/{chunk-FGQ6CKKK.js → chunk-LDEOQNG2.js} +2 -2
  4. package/dist/connect/chunks/{chunk-FGQ6CKKK.js.map → chunk-LDEOQNG2.js.map} +1 -1
  5. package/dist/connect/chunks/{chunk-3CYK5SIU.js → chunk-WCKTUB4V.js} +1 -1
  6. package/dist/connect/chunks/chunk-WCKTUB4V.js.map +1 -0
  7. package/dist/connect/chunks/{chunk-JYBI36GK.js → chunk-WL6VYP7G.js} +2 -2
  8. package/dist/connect/index.cjs +1 -1
  9. package/dist/connect/index.cjs.map +1 -1
  10. package/dist/connect/index.js +5 -5
  11. package/dist/connect/internal/host.js +3 -3
  12. package/dist/connect/internal/nft-wire.js +2 -2
  13. package/dist/core/index.cjs +831 -343
  14. package/dist/core/index.cjs.map +1 -1
  15. package/dist/core/index.d.cts +30 -1
  16. package/dist/core/index.d.ts +30 -1
  17. package/dist/core/index.js +831 -343
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/impl/browser/connect/index.cjs +1 -1
  20. package/dist/impl/browser/connect/index.cjs.map +1 -1
  21. package/dist/impl/browser/connect/index.js +2 -2
  22. package/dist/impl/browser/index.cjs.map +1 -1
  23. package/dist/impl/browser/index.js.map +1 -1
  24. package/dist/impl/nodejs/index.cjs.map +1 -1
  25. package/dist/impl/nodejs/index.d.cts +1 -0
  26. package/dist/impl/nodejs/index.d.ts +1 -0
  27. package/dist/impl/nodejs/index.js.map +1 -1
  28. package/dist/impl/shared/wallet-api/index.d.cts +1 -0
  29. package/dist/impl/shared/wallet-api/index.d.ts +1 -0
  30. package/dist/impl/wallet-api-v2/index.cjs +2 -1
  31. package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
  32. package/dist/impl/wallet-api-v2/index.d.cts +3 -0
  33. package/dist/impl/wallet-api-v2/index.d.ts +3 -0
  34. package/dist/impl/wallet-api-v2/index.js +2 -1
  35. package/dist/impl/wallet-api-v2/index.js.map +1 -1
  36. package/dist/index.cjs +833 -343
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +32 -2
  39. package/dist/index.d.ts +32 -2
  40. package/dist/index.js +832 -343
  41. package/dist/index.js.map +1 -1
  42. package/dist/modules/payments-v2/index.cjs +658 -259
  43. package/dist/modules/payments-v2/index.cjs.map +1 -1
  44. package/dist/modules/payments-v2/index.d.cts +65 -2
  45. package/dist/modules/payments-v2/index.d.ts +65 -2
  46. package/dist/modules/payments-v2/index.js +656 -259
  47. package/dist/modules/payments-v2/index.js.map +1 -1
  48. package/dist/token-engine/index.cjs +185 -65
  49. package/dist/token-engine/index.cjs.map +1 -1
  50. package/dist/token-engine/index.d.cts +20 -3
  51. package/dist/token-engine/index.d.ts +20 -3
  52. package/dist/token-engine/index.js +184 -65
  53. package/dist/token-engine/index.js.map +1 -1
  54. package/package.json +1 -1
  55. package/dist/connect/chunks/chunk-3CYK5SIU.js.map +0 -1
  56. /package/dist/connect/chunks/{chunk-OAAY2JEA.js.map → chunk-7BZL7HE2.js.map} +0 -0
  57. /package/dist/connect/chunks/{chunk-MQQ3TMHM.js.map → chunk-CJ4D6M6W.js.map} +0 -0
  58. /package/dist/connect/chunks/{chunk-JYBI36GK.js.map → chunk-WL6VYP7G.js.map} +0 -0
@@ -33,6 +33,7 @@ __export(payments_v2_exports, {
33
33
  ATTENTION_MINT_UNRESOLVED: () => ATTENTION_MINT_UNRESOLVED,
34
34
  ATTENTION_RECIPIENT_NETWORK_UNVERIFIED: () => ATTENTION_RECIPIENT_NETWORK_UNVERIFIED,
35
35
  ATTENTION_RESEED_REJECTED: () => ATTENTION_RESEED_REJECTED,
36
+ CUSTOM_MINT_MAX_PAYLOAD_BYTES: () => CUSTOM_MINT_MAX_PAYLOAD_BYTES,
36
37
  HEARTBEAT_CAP_MS: () => HEARTBEAT_CAP_MS,
37
38
  HEARTBEAT_SEED_MS: () => HEARTBEAT_SEED_MS,
38
39
  MAX_RESELECT: () => MAX_RESELECT,
@@ -46,6 +47,7 @@ __export(payments_v2_exports, {
46
47
  STORE_KEYS: () => STORE_KEYS,
47
48
  createScopedKV: () => createScopedKV,
48
49
  encodeNftContent: () => encodeNftContent,
50
+ isLiveBurn: () => isLiveBurn,
49
51
  isNftDocumentLink: () => isNftDocumentLink,
50
52
  isRetryableAckError: () => isRetryableAckError,
51
53
  parseNftDocument: () => parseNftDocument,
@@ -1753,6 +1755,33 @@ function decryptField(key, envelope) {
1753
1755
  return new TextDecoder().decode(decryptFieldBytes(key, envelope));
1754
1756
  }
1755
1757
 
1758
+ // token-engine/claims.ts
1759
+ var CoinClaims = class _CoinClaims {
1760
+ issuers = /* @__PURE__ */ new Map();
1761
+ policies = [];
1762
+ static fromPlugins(plugins) {
1763
+ const claims = new _CoinClaims();
1764
+ for (const plugin of plugins ?? []) {
1765
+ for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
1766
+ }
1767
+ return claims;
1768
+ }
1769
+ add(policy) {
1770
+ const tokenType = import_HexConverter.HexConverter.encode(policy.tokenType.bytes);
1771
+ const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
1772
+ const taken = coinIds.find((coinId) => this.issuers.has(coinId));
1773
+ if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
1774
+ for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
1775
+ this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
1776
+ }
1777
+ fingerprint() {
1778
+ return [...this.policies].sort().join("\n");
1779
+ }
1780
+ issuerOf(coinId) {
1781
+ return this.issuers.get(coinId.toLowerCase()) ?? null;
1782
+ }
1783
+ };
1784
+
1756
1785
  // modules/payments-v2/machine/payload-view.ts
1757
1786
  function isWholeIntent(p) {
1758
1787
  return p.kind === "whole" || p.kind === "coinless";
@@ -2030,6 +2059,9 @@ function createScopedKV(storage, network, chainPubkey) {
2030
2059
  }
2031
2060
  };
2032
2061
  }
2062
+ function isLiveBurn(entry) {
2063
+ return !entry.settled && entry.failure === void 0;
2064
+ }
2033
2065
  var STORE_KEYS = {
2034
2066
  intentBackstop: "intents",
2035
2067
  checkpointCache: "checkpoints",
@@ -2044,7 +2076,8 @@ var STORE_KEYS = {
2044
2076
  epochLatch: "epoch-latch",
2045
2077
  // §5.2 InventoryView durable overlays (#625/#679).
2046
2078
  suspectedSpent: "suspected-spent",
2047
- knownSpends: "known-spends"
2079
+ knownSpends: "known-spends",
2080
+ verifiedTokens: "verified-tokens"
2048
2081
  };
2049
2082
 
2050
2083
  // modules/payments-v2/inventory/presentation.ts
@@ -2062,18 +2095,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
2062
2095
  createdAt: snapshot.createdAt,
2063
2096
  updatedAt: snapshot.updatedAt,
2064
2097
  lazy: true,
2065
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
2098
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {},
2099
+ ...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
2066
2100
  };
2067
2101
  }
2068
- function aggregateAssets(entries, isTransferring, registry) {
2102
+ function aggregateAssets(entries, reading, registry) {
2069
2103
  const groups = /* @__PURE__ */ new Map();
2070
2104
  for (const [tokenId, entry] of entries) {
2071
- const moving = isTransferring(tokenId);
2105
+ const moving = reading.isTransferring(tokenId);
2072
2106
  for (const asset of entry.assets) {
2073
- let group = groups.get(asset.coinId);
2107
+ const standing = reading.standing(tokenId, asset.coinId);
2108
+ const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
2109
+ let group = groups.get(key);
2074
2110
  if (!group) {
2075
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
2076
- groups.set(asset.coinId, group);
2111
+ group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
2112
+ groups.set(key, group);
2077
2113
  }
2078
2114
  if (moving) {
2079
2115
  group.transferring += BigInt(asset.amount);
@@ -2084,9 +2120,10 @@ function aggregateAssets(entries, isTransferring, registry) {
2084
2120
  }
2085
2121
  }
2086
2122
  }
2087
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
2123
+ return [...groups.values()].map((totals) => toAsset(totals, registry));
2088
2124
  }
2089
- function toAsset(coinId, totals, registry) {
2125
+ function toAsset(totals, registry) {
2126
+ const coinId = totals.coinId;
2090
2127
  const iconUrl = registry.getIconUrl(coinId);
2091
2128
  return {
2092
2129
  coinId,
@@ -2106,7 +2143,8 @@ function toAsset(coinId, totals, registry) {
2106
2143
  priceEur: null,
2107
2144
  change24h: null,
2108
2145
  fiatValueUsd: null,
2109
- fiatValueEur: null
2146
+ fiatValueEur: null,
2147
+ ...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
2110
2148
  };
2111
2149
  }
2112
2150
  function priceIdOf(registry, coinId) {
@@ -2114,9 +2152,9 @@ function priceIdOf(registry, coinId) {
2114
2152
  }
2115
2153
  async function withPrices(raw, registry, price) {
2116
2154
  try {
2117
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
2155
+ const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
2118
2156
  const quotes = await price.getPrices(ids);
2119
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
2157
+ return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
2120
2158
  } catch {
2121
2159
  return raw;
2122
2160
  }
@@ -2137,11 +2175,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
2137
2175
  const snapshot = { assets: [], createdAt: now, updatedAt: now };
2138
2176
  return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
2139
2177
  transferring: true,
2140
- suspectedSpent: false
2178
+ suspectedSpent: false,
2179
+ standing: "trusted"
2141
2180
  });
2142
2181
  }
2143
2182
 
2144
2183
  // modules/payments-v2/inventory/InventoryView.ts
2184
+ var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
2145
2185
  var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
2146
2186
  var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
2147
2187
  var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
@@ -2235,6 +2275,12 @@ var InventoryView = class {
2235
2275
  for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
2236
2276
  if (changed) this.deps.emit("inventory:updated");
2237
2277
  }
2278
+ get trust() {
2279
+ return this.deps.trust ?? TRUST_ALL;
2280
+ }
2281
+ trusts(tokenId, coinId) {
2282
+ return this.trust.standing(tokenId, coinId) === "trusted";
2283
+ }
2238
2284
  pinned(tokenId) {
2239
2285
  return this.deps.isPinned?.(tokenId) ?? false;
2240
2286
  }
@@ -2261,6 +2307,7 @@ var InventoryView = class {
2261
2307
  isSpendable(tokenId, entry) {
2262
2308
  if (entry.status !== "active") return false;
2263
2309
  if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
2310
+ if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
2264
2311
  return !this.suspected.has(stateKey(tokenId, entry.stateHash));
2265
2312
  }
2266
2313
  /** Is this NAMED token spendable whole — coinless or valued (#777)? */
@@ -2287,7 +2334,8 @@ var InventoryView = class {
2287
2334
  out.push(
2288
2335
  toToken(tokenId, entry, asset, registry, {
2289
2336
  transferring: this.held(tokenId),
2290
- suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
2337
+ suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
2338
+ standing: this.trust.standing(tokenId, asset.coinId)
2291
2339
  })
2292
2340
  );
2293
2341
  }
@@ -2317,10 +2365,21 @@ var InventoryView = class {
2317
2365
  return out;
2318
2366
  }
2319
2367
  async assets(registry, price) {
2320
- const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
2368
+ const reading = {
2369
+ isTransferring: (tokenId) => this.held(tokenId),
2370
+ standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
2371
+ };
2372
+ const raw = aggregateAssets(this.activeEntries(), reading, registry);
2321
2373
  if (!price || raw.length === 0) return raw;
2322
2374
  return withPrices(raw, registry, price);
2323
2375
  }
2376
+ claimHolders() {
2377
+ const out = [];
2378
+ for (const [tokenId, entry] of this.activeEntries()) {
2379
+ if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
2380
+ }
2381
+ return out;
2382
+ }
2324
2383
  *activeEntries() {
2325
2384
  for (const [tokenId, entry] of this.mirror) {
2326
2385
  if (entry.status === "active") yield [tokenId, entry];
@@ -2490,6 +2549,171 @@ var InventoryView = class {
2490
2549
  }
2491
2550
  };
2492
2551
 
2552
+ // modules/payments-v2/inventory/verdicts.ts
2553
+ var VERDICT_RETRY_MS = 3e4;
2554
+ var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
2555
+ function honoursClaims(claims, token) {
2556
+ const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
2557
+ return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
2558
+ }
2559
+ var TokenVerdicts = class {
2560
+ constructor(deps) {
2561
+ this.deps = deps;
2562
+ }
2563
+ verified = /* @__PURE__ */ new Set();
2564
+ refused = /* @__PURE__ */ new Set();
2565
+ retries = /* @__PURE__ */ new Map();
2566
+ writes = new SerialChain();
2567
+ hydration = null;
2568
+ queued = null;
2569
+ running = null;
2570
+ held = [];
2571
+ timer = null;
2572
+ live = false;
2573
+ isClaimed(coinId) {
2574
+ return this.deps.claims.issuerOf(coinId) !== null;
2575
+ }
2576
+ standing(tokenId, coinId) {
2577
+ if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
2578
+ return this.refused.has(tokenId) ? "refused" : "pending";
2579
+ }
2580
+ trusts(tokenId, coinId) {
2581
+ return this.standing(tokenId, coinId) === "trusted";
2582
+ }
2583
+ hydrate() {
2584
+ this.hydration ??= this.load().catch((error) => {
2585
+ this.hydration = null;
2586
+ throw error;
2587
+ });
2588
+ return this.hydration;
2589
+ }
2590
+ start() {
2591
+ this.live = true;
2592
+ return this.hydrate();
2593
+ }
2594
+ stop() {
2595
+ this.live = false;
2596
+ if (this.timer !== null) clearTimeout(this.timer);
2597
+ this.timer = null;
2598
+ }
2599
+ async accept(token) {
2600
+ if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
2601
+ }
2602
+ async recordSplit(sourceTokenId, outputTokenId) {
2603
+ await this.hydrate();
2604
+ if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
2605
+ }
2606
+ review(holders) {
2607
+ this.queued = holders;
2608
+ this.running ??= this.drain();
2609
+ return this.running;
2610
+ }
2611
+ async drain() {
2612
+ try {
2613
+ while (this.queued !== null) {
2614
+ const holders = this.queued;
2615
+ this.queued = null;
2616
+ await this.reviewOnce(holders);
2617
+ }
2618
+ } finally {
2619
+ this.running = null;
2620
+ }
2621
+ }
2622
+ async reviewOnce(holders) {
2623
+ await this.hydrate();
2624
+ this.held = holders;
2625
+ const due = holders.filter((tokenId) => this.isDue(tokenId));
2626
+ const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
2627
+ for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
2628
+ if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
2629
+ await this.persist();
2630
+ this.deps.changed();
2631
+ }
2632
+ this.scheduleRetry();
2633
+ }
2634
+ isDue(tokenId) {
2635
+ if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
2636
+ return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
2637
+ }
2638
+ async check(due) {
2639
+ const verdicts = /* @__PURE__ */ new Map();
2640
+ let blobs;
2641
+ try {
2642
+ blobs = await this.deps.getBlobs([...due]);
2643
+ } catch {
2644
+ return verdicts;
2645
+ }
2646
+ for (const tokenId of due) {
2647
+ const bytes = blobs.get(tokenId);
2648
+ if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
2649
+ }
2650
+ return verdicts;
2651
+ }
2652
+ async verdictOf(tokenId, bytes) {
2653
+ let engine;
2654
+ try {
2655
+ engine = this.deps.engine();
2656
+ } catch {
2657
+ return "retry";
2658
+ }
2659
+ let token;
2660
+ try {
2661
+ token = await engine.decodeToken({ tokenId, token: bytes });
2662
+ } catch {
2663
+ return "refused";
2664
+ }
2665
+ if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
2666
+ try {
2667
+ return (await engine.verify(token)).ok ? "verified" : "refused";
2668
+ } catch {
2669
+ return "retry";
2670
+ }
2671
+ }
2672
+ settle(tokenId, verdict) {
2673
+ if (verdict === "retry") {
2674
+ this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
2675
+ return;
2676
+ }
2677
+ this.retries.delete(tokenId);
2678
+ (verdict === "verified" ? this.verified : this.refused).add(tokenId);
2679
+ }
2680
+ nextRetry(previous) {
2681
+ const attempts = (previous?.attempts ?? 0) + 1;
2682
+ const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
2683
+ return { attempts, atMs: this.now() + delay };
2684
+ }
2685
+ scheduleRetry() {
2686
+ if (!this.live || this.timer !== null) return;
2687
+ const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
2688
+ if (due.length === 0) return;
2689
+ const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
2690
+ this.timer = setTimeout(() => {
2691
+ this.timer = null;
2692
+ void this.review(this.held).catch(() => void 0);
2693
+ }, delay);
2694
+ }
2695
+ async remember(tokenId) {
2696
+ await this.hydrate();
2697
+ if (this.verified.has(tokenId)) return;
2698
+ this.verified.add(tokenId);
2699
+ this.refused.delete(tokenId);
2700
+ this.retries.delete(tokenId);
2701
+ await this.persist();
2702
+ }
2703
+ persist() {
2704
+ const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
2705
+ return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
2706
+ }
2707
+ async load() {
2708
+ const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
2709
+ if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
2710
+ for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
2711
+ }
2712
+ now() {
2713
+ return (this.deps.now ?? Date.now)();
2714
+ }
2715
+ };
2716
+
2493
2717
  // token-engine/value-envelope.ts
2494
2718
  function isCoinlessEnvelope(envelope) {
2495
2719
  return envelope.startsWith("none_");
@@ -2504,6 +2728,8 @@ function isRetryableAckError(err) {
2504
2728
  var ACK_BATCH_SIZE = 200;
2505
2729
  var REFRESH_INTERVAL_MS = 2500;
2506
2730
  var POLL_INTERVAL_MS = 3e4;
2731
+ var RECHECK_BASE_MS = POLL_INTERVAL_MS;
2732
+ var RECHECK_MAX_MS = 60 * 60 * 1e3;
2507
2733
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
2508
2734
  function receivedDedupKey(tokenId, stateHash) {
2509
2735
  return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
@@ -2520,6 +2746,7 @@ var Receive = class {
2520
2746
  pollTimer = null;
2521
2747
  unsubscribeWake = null;
2522
2748
  lastRefreshAt = 0;
2749
+ rechecks = /* @__PURE__ */ new Map();
2523
2750
  drainOnce() {
2524
2751
  return this.drainFlight.run(() => this.doDrain());
2525
2752
  }
@@ -2578,6 +2805,7 @@ var Receive = class {
2578
2805
  const pending = [];
2579
2806
  const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
2580
2807
  const claimable = { n: 0 };
2808
+ const deferred = { any: false };
2581
2809
  const finish = () => {
2582
2810
  if (claimable.n > 0) deps.refreshView?.();
2583
2811
  return stored;
@@ -2586,7 +2814,7 @@ var Receive = class {
2586
2814
  const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
2587
2815
  let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
2588
2816
  for (let pass = 0; pass < 2; pass++) {
2589
- await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
2817
+ await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
2590
2818
  const served = deps.delivery.incomingEpoch();
2591
2819
  if (basis === null || served === null || served === basis.syncEpoch) break;
2592
2820
  await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
@@ -2603,20 +2831,41 @@ var Receive = class {
2603
2831
  return finish();
2604
2832
  }
2605
2833
  async processEntry(ctx, entry) {
2606
- const { deps, engine, pending, stored, claimable } = ctx;
2607
- const screened = await screen(deps, engine, entry);
2834
+ const { deps, engine, stored, claimable } = ctx;
2835
+ const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
2836
+ this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
2837
+ if (screened.kind === "defer") {
2838
+ ctx.deferred.any = true;
2839
+ return;
2840
+ }
2608
2841
  if (screened.kind === "ack") {
2609
- pending.push(screened.ack);
2842
+ queueAck(ctx, screened.ack);
2610
2843
  if (screened.ack.disposition === "claimed") claimable.n += 1;
2611
2844
  return;
2612
2845
  }
2846
+ await deps.accepted?.(screened.token);
2613
2847
  await deps.view.store(screened.record);
2614
- pending.push(claimAck(entry));
2848
+ queueAck(ctx, claimAck(entry));
2615
2849
  claimable.n += 1;
2616
2850
  const transfer = await announce(deps, entry, screened.record);
2617
2851
  stored.push(transfer);
2618
2852
  deps.emit("transfer:incoming", transfer);
2619
2853
  }
2854
+ recheckDue(deliveryId) {
2855
+ const recheck = this.rechecks.get(deliveryId);
2856
+ return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
2857
+ }
2858
+ scheduleRecheck(deliveryId, deferred) {
2859
+ if (!deferred) {
2860
+ this.rechecks.delete(deliveryId);
2861
+ return;
2862
+ }
2863
+ const previous = this.rechecks.get(deliveryId);
2864
+ if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
2865
+ const attempts = (previous?.attempts ?? 0) + 1;
2866
+ const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
2867
+ this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
2868
+ }
2620
2869
  };
2621
2870
  async function screen(deps, engine, entry) {
2622
2871
  const blobBytes = await entry.fetchBlob();
@@ -2627,7 +2876,8 @@ async function screen(deps, engine, entry) {
2627
2876
  logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
2628
2877
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
2629
2878
  }
2630
- const verdict = await engine.verify(token);
2879
+ const verdict = await verdictOf(engine, token);
2880
+ if (verdict === null) return { kind: "defer" };
2631
2881
  if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
2632
2882
  if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
2633
2883
  return { kind: "ack", ack: rejectAck(entry, "not-owned") };
@@ -2638,38 +2888,43 @@ async function screen(deps, engine, entry) {
2638
2888
  if (held !== null && await engine.isSpent(token)) {
2639
2889
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
2640
2890
  }
2891
+ const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
2641
2892
  return {
2642
2893
  kind: "accept",
2894
+ token,
2643
2895
  record: {
2644
2896
  tokenId: keys.tokenId,
2645
2897
  stateHash: keys.stateHash,
2646
2898
  assets: toAssetAmounts(token),
2647
- ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
2899
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
2900
+ ...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
2648
2901
  }
2649
2902
  };
2650
2903
  }
2651
- async function announce(deps, entry, record) {
2652
- const receivedAt = (deps.now ?? Date.now)();
2904
+ function unverifiedCoinsOf(deps, token) {
2905
+ const claims = deps.claims;
2906
+ if (claims === void 0 || honoursClaims(claims, token)) return [];
2907
+ return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
2908
+ }
2909
+ async function verdictOf(engine, token) {
2653
2910
  try {
2654
- await deps.recordReceived({
2655
- dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
2656
- tokenId: record.tokenId,
2657
- stateHash: record.stateHash,
2658
- assets: record.assets,
2659
- ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
2660
- ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
2661
- ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
2662
- ...entry.memo !== void 0 ? { memo: entry.memo } : {},
2663
- receivedAt
2664
- });
2911
+ return await engine.verify(token);
2665
2912
  } catch (err) {
2666
- logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
2913
+ if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
2914
+ throw err;
2667
2915
  }
2916
+ }
2917
+ async function announce(deps, entry, record) {
2918
+ const receivedAt = (deps.now ?? Date.now)();
2919
+ await recordArrival(deps, entry, record, receivedAt);
2920
+ const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
2921
+ const counted = record.assets.filter((asset) => !refused2.includes(asset));
2668
2922
  return {
2669
2923
  id: record.tokenId,
2670
2924
  senderPubkey: entry.senderPubkey ?? "",
2671
2925
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
2672
- tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
2926
+ tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
2927
+ ...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
2673
2928
  // #777: named here rather than mapped from assets, which announced an EMPTY list.
2674
2929
  ...record.tokenType !== void 0 ? {
2675
2930
  coinless: [
@@ -2687,6 +2942,25 @@ async function announce(deps, entry, record) {
2687
2942
  receivedAt
2688
2943
  };
2689
2944
  }
2945
+ async function recordArrival(deps, entry, record, receivedAt) {
2946
+ const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
2947
+ if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
2948
+ try {
2949
+ await deps.recordReceived({
2950
+ dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
2951
+ tokenId: record.tokenId,
2952
+ stateHash: record.stateHash,
2953
+ assets,
2954
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
2955
+ ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
2956
+ ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
2957
+ ...entry.memo !== void 0 ? { memo: entry.memo } : {},
2958
+ receivedAt
2959
+ });
2960
+ } catch (err) {
2961
+ logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
2962
+ }
2963
+ }
2690
2964
  async function flushAcks(deps, pending, epochOf) {
2691
2965
  if (pending.length === 0) return { progressed: true };
2692
2966
  const before = pending.length;
@@ -2694,7 +2968,7 @@ async function flushAcks(deps, pending, epochOf) {
2694
2968
  try {
2695
2969
  const settled = await settleAcks(deps, pending);
2696
2970
  let i = 0;
2697
- while (i < pending.length && settled.has(pending[i].deliveryId)) {
2971
+ while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
2698
2972
  lastAcked = pending[i].cursor;
2699
2973
  i += 1;
2700
2974
  }
@@ -2770,6 +3044,9 @@ async function ackOne(deps, ack) {
2770
3044
  await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
2771
3045
  }
2772
3046
  }
3047
+ function queueAck(ctx, ack) {
3048
+ ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
3049
+ }
2773
3050
  function claimAck(entry) {
2774
3051
  return {
2775
3052
  deliveryId: entry.deliveryId,
@@ -3670,6 +3947,7 @@ var TransferMachine = class {
3670
3947
  const key = keys.get(digest);
3671
3948
  if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
3672
3949
  const { tokenId } = await engine.deliveryKeys(o.changeBlob);
3950
+ await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
3673
3951
  added.push({ tokenId, key });
3674
3952
  }
3675
3953
  return added;
@@ -4065,7 +4343,7 @@ var Converger = class {
4065
4343
  this.deps.stores.burnJournal.list()
4066
4344
  ]);
4067
4345
  const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
4068
- const burnsOpen = burns.some((e) => !e.settled);
4346
+ const burnsOpen = burns.some(isLiveBurn);
4069
4347
  return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
4070
4348
  }
4071
4349
  async resumePass() {
@@ -4152,6 +4430,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
4152
4430
  const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
4153
4431
  holds.set(entry.transferId, [...direct, ...split]);
4154
4432
  }
4433
+ for (const burn of await stores.burnJournal.list()) {
4434
+ if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
4435
+ }
4155
4436
  return { open: holds, complete };
4156
4437
  }
4157
4438
  function groupJournal(journal) {
@@ -4641,6 +4922,176 @@ function parseAmount(raw) {
4641
4922
  return amount;
4642
4923
  }
4643
4924
 
4925
+ // modules/payments-v2/burn.ts
4926
+ var HEX32 = /^[0-9a-f]{64}$/;
4927
+ async function heldToken(deps, tokenId) {
4928
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
4929
+ if (bytes === void 0) {
4930
+ throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
4931
+ }
4932
+ return deps.engine.decodeToken({ tokenId, token: bytes });
4933
+ }
4934
+ function assertBurnable(engine, token, tokenId) {
4935
+ if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
4936
+ throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
4937
+ }
4938
+ if (token.valueEnvelope === "bare_collection") {
4939
+ throw new SphereError(
4940
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
4941
+ "VALIDATION_ERROR"
4942
+ );
4943
+ }
4944
+ }
4945
+ function assetsOf(token) {
4946
+ return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
4947
+ }
4948
+ async function runBurnUnderJournal(deps, input) {
4949
+ const { burnId } = input;
4950
+ const { tokenId, reasonBytes } = input.request;
4951
+ if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
4952
+ return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
4953
+ }
4954
+ let token;
4955
+ try {
4956
+ token = await heldToken(deps, tokenId);
4957
+ assertBurnable(deps.engine, token, tokenId);
4958
+ } catch (err) {
4959
+ return refused(burnId, tokenId, err);
4960
+ }
4961
+ const entry = {
4962
+ burnId,
4963
+ tokenId,
4964
+ reasonHex: bytesToHex(reasonBytes),
4965
+ burnedTokenHex: null,
4966
+ settled: false,
4967
+ assets: assetsOf(token),
4968
+ createdAt: deps.now()
4969
+ };
4970
+ await deps.burnJournal.upsert(entry);
4971
+ try {
4972
+ const settled = await burnJournaled(deps, entry, token);
4973
+ return { success: true, burnId, tokenId, burnedToken: hexToBytes(settled.burnedTokenHex ?? "") };
4974
+ } catch (err) {
4975
+ const failure = failureOf(err);
4976
+ if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
4977
+ else deps.armHeartbeat();
4978
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
4979
+ }
4980
+ }
4981
+ async function replayBurns(deps) {
4982
+ let progressed = 0;
4983
+ for (const entry of await deps.burnJournal.list()) {
4984
+ if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
4985
+ try {
4986
+ const token = entry.burnedTokenHex === null ? await heldToken(deps, entry.tokenId) : null;
4987
+ await burnJournaled(deps, entry, token);
4988
+ progressed += 1;
4989
+ } catch (err) {
4990
+ if (!isTerminal(err)) continue;
4991
+ await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
4992
+ progressed += 1;
4993
+ }
4994
+ }
4995
+ return progressed;
4996
+ }
4997
+ async function pendingBurns(deps) {
4998
+ return (await deps.burnJournal.list()).map((entry) => ({
4999
+ burnId: entry.burnId,
5000
+ tokenId: entry.tokenId,
5001
+ reasonBytes: hexToBytes(entry.reasonHex),
5002
+ burnedToken: entry.burnedTokenHex === null ? null : hexToBytes(entry.burnedTokenHex),
5003
+ settled: entry.settled,
5004
+ createdAt: entry.createdAt,
5005
+ ...entry.failure !== void 0 ? { failure: entry.failure } : {}
5006
+ }));
5007
+ }
5008
+ async function acknowledgeBurn(deps, burnId) {
5009
+ const entry = await deps.burnJournal.getByKey(burnId);
5010
+ if (entry === void 0) return;
5011
+ if (isLiveBurn(entry)) {
5012
+ throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
5013
+ }
5014
+ await deps.burnJournal.removeByKey(burnId);
5015
+ }
5016
+ function refused(burnId, tokenId, err) {
5017
+ const failure = failureOf(err);
5018
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
5019
+ }
5020
+ function isTerminal(err) {
5021
+ return classifyError(err) === "conflict";
5022
+ }
5023
+ function failureOf(err) {
5024
+ const code = err !== null && typeof err === "object" ? err.code : void 0;
5025
+ return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
5026
+ }
5027
+ async function burnJournaled(deps, entry, token) {
5028
+ let current = entry;
5029
+ if (current.burnedTokenHex === null) {
5030
+ if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
5031
+ const burned = await deps.engine.burn(
5032
+ { token, reasonBytes: hexToBytes(current.reasonHex) },
5033
+ { transferId: current.burnId }
5034
+ );
5035
+ if (burned.blob.tokenId !== current.tokenId) {
5036
+ throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
5037
+ }
5038
+ current = { ...current, burnedTokenHex: bytesToHex(burned.blob.token) };
5039
+ await deps.burnJournal.upsert(current);
5040
+ }
5041
+ await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
5042
+ await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
5043
+ current = { ...current, settled: true };
5044
+ await deps.burnJournal.upsert(current);
5045
+ deps.refreshView();
5046
+ return current;
5047
+ }
5048
+
5049
+ // modules/payments-v2/burn-hold.ts
5050
+ var BurnHold = class {
5051
+ constructor(deps) {
5052
+ this.deps = deps;
5053
+ }
5054
+ async run(burnId, tokenId, burn) {
5055
+ try {
5056
+ this.deps.queue.planWhole(burnId, tokenId);
5057
+ } catch (err) {
5058
+ return refused(burnId, tokenId, err);
5059
+ }
5060
+ this.deps.view.markInFlightMany([tokenId]);
5061
+ let burned = false;
5062
+ try {
5063
+ const result = await burn();
5064
+ burned = result.success;
5065
+ return result;
5066
+ } finally {
5067
+ await this.settle(burnId, tokenId, burned);
5068
+ }
5069
+ }
5070
+ async settle(burnId, tokenId, burned) {
5071
+ if (burned) {
5072
+ this.deps.ledger.commit(burnId);
5073
+ this.deps.track(this.refreshThenRelease(tokenId));
5074
+ return;
5075
+ }
5076
+ const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
5077
+ if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
5078
+ this.deps.ledger.cancel(burnId);
5079
+ this.deps.view.releaseMany([tokenId]);
5080
+ this.deps.queue.notifyChange("");
5081
+ return;
5082
+ }
5083
+ this.deps.pins.adopt(burnId);
5084
+ }
5085
+ async refreshThenRelease(tokenId) {
5086
+ try {
5087
+ await this.deps.view.delta();
5088
+ this.deps.view.releaseMany([tokenId]);
5089
+ } finally {
5090
+ this.deps.queue.notifyChange("");
5091
+ }
5092
+ }
5093
+ };
5094
+
4644
5095
  // modules/payments-v2/compose.ts
4645
5096
  function supportsDeterministicMint(engine) {
4646
5097
  return engine.deterministicMint === true;
@@ -4648,16 +5099,8 @@ function supportsDeterministicMint(engine) {
4648
5099
  function composeFacadeParts(deps, hooks) {
4649
5100
  const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
4650
5101
  const ledger = new ReservationLedger();
4651
- const view = new InventoryView({
4652
- port: deps.storagePort,
4653
- kv: deps.kv,
4654
- emit: (event) => deps.emit(event, {}),
4655
- // #737: a reserved token is unselectable, so it is never confirmed balance.
4656
- // #738: while the held-set is unproven, EVERY token reads pinned — the report
4657
- // must not call a token spendable that the queue is about to refuse to spend.
4658
- isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
4659
- ...deps.now !== void 0 ? { now: deps.now } : {}
4660
- });
5102
+ const verdicts = buildVerdicts(deps, hooks);
5103
+ const view = buildView(deps, hooks, ledger, verdicts);
4661
5104
  const queue = new SpendQueue({
4662
5105
  ledger,
4663
5106
  getPool: (coinId) => view.pool(coinId),
@@ -4672,27 +5115,57 @@ function composeFacadeParts(deps, hooks) {
4672
5115
  ...deps.now !== void 0 ? { now: deps.now } : {},
4673
5116
  ...deps.newId !== void 0 ? { newId: deps.newId } : {}
4674
5117
  });
4675
- const machineDeps = buildMachineDeps(deps, hooks, historyStore);
5118
+ const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
4676
5119
  const machineStores = createMachineStores(deps.kv);
4677
5120
  const heldStates = /* @__PURE__ */ new Map();
4678
5121
  const refreshView = coalesced(() => view.delta(), hooks.track);
5122
+ const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
4679
5123
  return {
4680
5124
  ownPubkeyBytes,
4681
5125
  view,
5126
+ verdicts,
4682
5127
  refreshView,
4683
5128
  ledger,
4684
- pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
5129
+ pins,
4685
5130
  queue,
5131
+ burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
4686
5132
  historyStore,
4687
5133
  machineStores,
4688
5134
  machineDeps,
4689
5135
  machine: new TransferMachine(machineDeps),
4690
5136
  heldStates,
4691
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
5137
+ receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
4692
5138
  requests: buildRequests(deps, hooks),
4693
5139
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
4694
5140
  };
4695
5141
  }
5142
+ function buildView(deps, hooks, ledger, verdicts) {
5143
+ const view = new InventoryView({
5144
+ port: deps.storagePort,
5145
+ kv: deps.kv,
5146
+ trust: verdicts,
5147
+ emit: (event) => {
5148
+ deps.emit(event, {});
5149
+ hooks.track(verdicts.review(view.claimHolders()));
5150
+ },
5151
+ // #737: a reserved token is unselectable, so it is never confirmed balance.
5152
+ // #738: while the held-set is unproven, EVERY token reads pinned — the report
5153
+ // must not call a token spendable that the queue is about to refuse to spend.
5154
+ isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
5155
+ ...deps.now !== void 0 ? { now: deps.now } : {}
5156
+ });
5157
+ return view;
5158
+ }
5159
+ function buildVerdicts(deps, hooks) {
5160
+ return new TokenVerdicts({
5161
+ kv: deps.kv,
5162
+ claims: deps.claims ?? new CoinClaims(),
5163
+ engine: () => hooks.engine(),
5164
+ getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
5165
+ changed: () => deps.emit("inventory:updated", {}),
5166
+ ...deps.now !== void 0 ? { now: deps.now } : {}
5167
+ });
5168
+ }
4696
5169
  function buildPins(deps, hooks, parts) {
4697
5170
  return new IntentPins({
4698
5171
  ledger: parts.ledger,
@@ -4720,8 +5193,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
4720
5193
  }
4721
5194
  };
4722
5195
  }
4723
- function buildMachineDeps(deps, hooks, historyStore) {
5196
+ function buildMachineDeps(deps, hooks, historyStore, verdicts) {
4724
5197
  return {
5198
+ recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
4725
5199
  engine: () => hooks.engine(),
4726
5200
  storage: deps.storagePort,
4727
5201
  delivery: deps.deliveryPort,
@@ -4754,10 +5228,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
4754
5228
  }
4755
5229
  };
4756
5230
  }
4757
- function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
5231
+ function buildReceive(deps, hooks, parts, refreshView) {
5232
+ const { historyStore, heldStates, verdicts } = parts;
4758
5233
  return new Receive({
4759
5234
  delivery: deps.deliveryPort,
4760
5235
  engine: () => hooks.engine(),
5236
+ claims: deps.claims ?? new CoinClaims(),
5237
+ accepted: (token) => verdicts.accept(token),
4761
5238
  view: {
4762
5239
  heldState: (tokenId) => heldStates.get(tokenId) ?? null,
4763
5240
  store: async (entry) => {
@@ -4876,6 +5353,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
4876
5353
  }
4877
5354
  return false;
4878
5355
  }
5356
+ async function seedHeldStates(storagePort, heldStates) {
5357
+ let page = await storagePort.listInventory();
5358
+ for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
5359
+ for (const item of page.items) {
5360
+ if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
5361
+ }
5362
+ if (!page.more) return;
5363
+ page = await storagePort.listInventory(page.cursor);
5364
+ }
5365
+ }
4879
5366
  async function replayCoinMints(deps) {
4880
5367
  let resolved = 0;
4881
5368
  for (const entry of await deps.mintJournal.list()) {
@@ -4979,98 +5466,9 @@ async function mintJournaled(deps, entry) {
4979
5466
  await finalizeMint(deps, { mintId: entry.mintId, token, assets: [], journal: deps.nftMintJournal });
4980
5467
  }
4981
5468
 
4982
- // modules/payments-v2/inventory/nft-read.ts
4983
- var NFT_CACHE_MAX = 256;
4984
- var NftCache = class {
4985
- constructor(max = NFT_CACHE_MAX) {
4986
- this.max = max;
4987
- }
4988
- entries = /* @__PURE__ */ new Map();
4989
- /** `undefined` = not cached. A hit becomes the most recently used entry. */
4990
- get(tokenId) {
4991
- const view = this.entries.get(tokenId);
4992
- if (view === void 0) return void 0;
4993
- this.entries.delete(tokenId);
4994
- this.entries.set(tokenId, view);
4995
- return view;
4996
- }
4997
- set(tokenId, view) {
4998
- this.entries.delete(tokenId);
4999
- this.entries.set(tokenId, view);
5000
- const oldest = this.entries.keys().next().value;
5001
- if (this.entries.size > this.max && oldest !== void 0) this.entries.delete(oldest);
5002
- }
5003
- };
5004
- async function readNft(deps, tokenId) {
5005
- if (deps.view.stateHashOf(tokenId) === void 0) {
5006
- throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
5007
- }
5008
- const cached = deps.cache.get(tokenId);
5009
- if (cached !== void 0) return cached;
5010
- const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
5011
- if (bytes === void 0) {
5012
- throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
5013
- }
5014
- return decodeView(deps, tokenId, bytes);
5015
- }
5016
- async function readNfts(deps, tokenIds) {
5017
- const held = [...new Set(tokenIds)].filter((tokenId) => deps.view.stateHashOf(tokenId) !== void 0);
5018
- const views = /* @__PURE__ */ new Map();
5019
- const misses = [];
5020
- for (const tokenId of held) {
5021
- const cached = deps.cache.get(tokenId);
5022
- if (cached === void 0) misses.push(tokenId);
5023
- else views.set(tokenId, cached);
5024
- }
5025
- const blobs = misses.length > 0 ? await deps.storagePort.getBlobs(misses) : /* @__PURE__ */ new Map();
5026
- for (const tokenId of misses) {
5027
- const bytes = blobs.get(tokenId);
5028
- if (bytes !== void 0) views.set(tokenId, await decodeView(deps, tokenId, bytes).catch(() => null));
5029
- }
5030
- const out = /* @__PURE__ */ new Map();
5031
- for (const tokenId of held) {
5032
- const view = views.get(tokenId);
5033
- if (view) out.set(tokenId, view);
5034
- }
5035
- return out;
5036
- }
5037
- async function decodeView(deps, tokenId, bytes) {
5038
- let view;
5039
- try {
5040
- const token = await deps.engine.decodeToken({ tokenId, token: bytes });
5041
- if (token.blob.tokenId !== tokenId) {
5042
- throw new SphereError(`the blob served for ${tokenId} decodes as ${token.blob.tokenId}`, "STORAGE_ERROR");
5043
- }
5044
- const reading = await deps.engine.readNft(token);
5045
- view = reading === null ? null : { tokenId, ...reading };
5046
- } catch (err) {
5047
- logger.warn("PaymentsV2", `NFT read: token ${tokenId} did not decode \u2014 ${String(err)}`);
5048
- throw err;
5049
- }
5050
- deps.cache.set(tokenId, view);
5051
- return view;
5052
- }
5053
-
5054
- // modules/payments-v2/inventory/token-data.ts
5055
- async function readTokenData(deps, tokenId) {
5056
- return deps.engine.readTokenData(await heldToken(deps, tokenId));
5057
- }
5058
- async function readTokenJustification(deps, tokenId) {
5059
- return deps.engine.readTokenJustification(await heldToken(deps, tokenId));
5060
- }
5061
- async function heldToken(deps, tokenId) {
5062
- if (deps.view.stateHashOf(tokenId) === void 0) {
5063
- throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
5064
- }
5065
- const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
5066
- if (bytes === void 0) {
5067
- throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
5068
- }
5069
- return deps.engine.decodeToken({ tokenId, token: bytes });
5070
- }
5071
-
5072
5469
  // modules/payments-v2/mint-custom.ts
5073
- var HEX32 = /^[0-9a-f]{64}$/;
5470
+ var HEX322 = /^[0-9a-f]{64}$/;
5471
+ var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
5074
5472
  function assertRequest(request) {
5075
5473
  if (request.tokenType.length !== 32) {
5076
5474
  throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
@@ -5081,19 +5479,27 @@ function assertRequest(request) {
5081
5479
  if (request.data.length === 0) {
5082
5480
  throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
5083
5481
  }
5482
+ if (request.justification !== void 0 && request.justification.length === 0) {
5483
+ throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
5484
+ }
5485
+ if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
5486
+ throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
5487
+ }
5084
5488
  for (const asset of request.assets) {
5085
- if (!HEX32.test(asset.coinId) || asset.amount <= 0n) {
5489
+ if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
5086
5490
  throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
5087
5491
  }
5088
5492
  }
5089
5493
  }
5090
5494
  async function runCustomMintUnderJournal(deps, input) {
5495
+ let entry;
5091
5496
  try {
5092
5497
  assertRequest(input.request);
5498
+ entry = journalEntryOf2(input.mintId, input.request, deps.now());
5499
+ deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
5093
5500
  } catch (err) {
5094
5501
  return { success: false, error: messageOf(err) };
5095
5502
  }
5096
- const entry = journalEntryOf2(input.mintId, input.request, deps.now());
5097
5503
  await deps.customMintJournal.upsert(entry);
5098
5504
  let tokenId;
5099
5505
  try {
@@ -5133,24 +5539,25 @@ function journalEntryOf2(mintId, request, createdAt) {
5133
5539
  createdAt
5134
5540
  };
5135
5541
  }
5542
+ function mintParamsOf(deps, entry, mintJustificationVerifiers) {
5543
+ return {
5544
+ recipientPubkey: deps.ownPubkeyBytes,
5545
+ data: hexToBytes(entry.dataHex),
5546
+ tokenType: hexToBytes(entry.tokenTypeHex),
5547
+ salt: hexToBytes(entry.saltHex),
5548
+ ...entry.justificationHex !== null ? { justification: hexToBytes(entry.justificationHex) } : {},
5549
+ ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
5550
+ };
5551
+ }
5136
5552
  async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
5137
- const token = await deps.engine.mintDataToken(
5138
- {
5139
- recipientPubkey: deps.ownPubkeyBytes,
5140
- data: hexToBytes(entry.dataHex),
5141
- tokenType: hexToBytes(entry.tokenTypeHex),
5142
- salt: hexToBytes(entry.saltHex),
5143
- ...entry.justificationHex !== null ? { justification: hexToBytes(entry.justificationHex) } : {},
5144
- ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
5145
- },
5146
- { transferId: entry.mintId }
5147
- );
5553
+ const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
5148
5554
  if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
5149
5555
  throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
5150
5556
  }
5151
5557
  if (entry.tokenId === "") {
5152
5558
  await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
5153
5559
  }
5560
+ if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
5154
5561
  await finalizeMint(deps, {
5155
5562
  mintId: entry.mintId,
5156
5563
  token,
@@ -5160,106 +5567,94 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
5160
5567
  return token.blob.tokenId;
5161
5568
  }
5162
5569
 
5163
- // modules/payments-v2/burn.ts
5164
- var HEX322 = /^[0-9a-f]{64}$/;
5165
- async function heldToken2(deps, tokenId) {
5570
+ // modules/payments-v2/inventory/nft-read.ts
5571
+ var NFT_CACHE_MAX = 256;
5572
+ var NftCache = class {
5573
+ constructor(max = NFT_CACHE_MAX) {
5574
+ this.max = max;
5575
+ }
5576
+ entries = /* @__PURE__ */ new Map();
5577
+ /** `undefined` = not cached. A hit becomes the most recently used entry. */
5578
+ get(tokenId) {
5579
+ const view = this.entries.get(tokenId);
5580
+ if (view === void 0) return void 0;
5581
+ this.entries.delete(tokenId);
5582
+ this.entries.set(tokenId, view);
5583
+ return view;
5584
+ }
5585
+ set(tokenId, view) {
5586
+ this.entries.delete(tokenId);
5587
+ this.entries.set(tokenId, view);
5588
+ const oldest = this.entries.keys().next().value;
5589
+ if (this.entries.size > this.max && oldest !== void 0) this.entries.delete(oldest);
5590
+ }
5591
+ };
5592
+ async function readNft(deps, tokenId) {
5593
+ if (deps.view.stateHashOf(tokenId) === void 0) {
5594
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
5595
+ }
5596
+ const cached = deps.cache.get(tokenId);
5597
+ if (cached !== void 0) return cached;
5166
5598
  const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
5167
5599
  if (bytes === void 0) {
5168
- throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
5600
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
5169
5601
  }
5170
- return deps.engine.decodeToken({ tokenId, token: bytes });
5171
- }
5172
- function assetsOf(token) {
5173
- return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
5602
+ return decodeView(deps, tokenId, bytes);
5174
5603
  }
5175
- async function runBurnUnderJournal(deps, input) {
5176
- const { burnId } = input;
5177
- const { tokenId, reasonBytes } = input.request;
5178
- if (!HEX322.test(tokenId) || reasonBytes.length === 0) {
5179
- return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty" };
5604
+ async function readNfts(deps, tokenIds) {
5605
+ const held = [...new Set(tokenIds)].filter((tokenId) => deps.view.stateHashOf(tokenId) !== void 0);
5606
+ const views = /* @__PURE__ */ new Map();
5607
+ const misses = [];
5608
+ for (const tokenId of held) {
5609
+ const cached = deps.cache.get(tokenId);
5610
+ if (cached === void 0) misses.push(tokenId);
5611
+ else views.set(tokenId, cached);
5180
5612
  }
5181
- let token;
5182
- try {
5183
- token = await heldToken2(deps, tokenId);
5184
- if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
5185
- throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
5186
- }
5187
- } catch (err) {
5188
- return { success: false, burnId, tokenId, error: messageOf(err) };
5613
+ const blobs = misses.length > 0 ? await deps.storagePort.getBlobs(misses) : /* @__PURE__ */ new Map();
5614
+ for (const tokenId of misses) {
5615
+ const bytes = blobs.get(tokenId);
5616
+ if (bytes !== void 0) views.set(tokenId, await decodeView(deps, tokenId, bytes).catch(() => null));
5189
5617
  }
5190
- const entry = {
5191
- burnId,
5192
- tokenId,
5193
- reasonHex: bytesToHex(reasonBytes),
5194
- burnedTokenHex: null,
5195
- settled: false,
5196
- assets: assetsOf(token),
5197
- createdAt: deps.now()
5198
- };
5199
- await deps.burnJournal.upsert(entry);
5200
- try {
5201
- const settled = await burnJournaled(deps, entry, token, { applied: false });
5202
- return { success: true, burnId, tokenId, burnedToken: hexToBytes(settled.burnedTokenHex ?? "") };
5203
- } catch (err) {
5204
- deps.armHeartbeat();
5205
- return { success: false, burnId, tokenId, error: messageOf(err) };
5618
+ const out = /* @__PURE__ */ new Map();
5619
+ for (const tokenId of held) {
5620
+ const view = views.get(tokenId);
5621
+ if (view) out.set(tokenId, view);
5206
5622
  }
5623
+ return out;
5207
5624
  }
5208
- async function replayBurns(deps) {
5209
- let progressed = 0;
5210
- for (const entry of await deps.burnJournal.list()) {
5211
- if (entry.settled || deps.isActiveOp(entry.burnId)) continue;
5212
- try {
5213
- const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
5214
- const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
5215
- await burnJournaled(deps, entry, token, { applied });
5216
- progressed += 1;
5217
- } catch {
5218
- continue;
5625
+ async function decodeView(deps, tokenId, bytes) {
5626
+ let view;
5627
+ try {
5628
+ const token = await deps.engine.decodeToken({ tokenId, token: bytes });
5629
+ if (token.blob.tokenId !== tokenId) {
5630
+ throw new SphereError(`the blob served for ${tokenId} decodes as ${token.blob.tokenId}`, "STORAGE_ERROR");
5219
5631
  }
5632
+ const reading = await deps.engine.readNft(token);
5633
+ view = reading === null ? null : { tokenId, ...reading };
5634
+ } catch (err) {
5635
+ logger.warn("PaymentsV2", `NFT read: token ${tokenId} did not decode \u2014 ${String(err)}`);
5636
+ throw err;
5220
5637
  }
5221
- return progressed;
5638
+ deps.cache.set(tokenId, view);
5639
+ return view;
5222
5640
  }
5223
- async function pendingBurns(deps) {
5224
- return (await deps.burnJournal.list()).map((entry) => ({
5225
- burnId: entry.burnId,
5226
- tokenId: entry.tokenId,
5227
- reasonBytes: hexToBytes(entry.reasonHex),
5228
- burnedToken: entry.burnedTokenHex === null ? null : hexToBytes(entry.burnedTokenHex),
5229
- settled: entry.settled,
5230
- createdAt: entry.createdAt
5231
- }));
5641
+
5642
+ // modules/payments-v2/inventory/token-data.ts
5643
+ async function readTokenData(deps, tokenId) {
5644
+ return deps.engine.readTokenData(await heldToken2(deps, tokenId));
5232
5645
  }
5233
- async function acknowledgeBurn(deps, burnId) {
5234
- const entry = await deps.burnJournal.getByKey(burnId);
5235
- if (entry === void 0) return;
5236
- if (!entry.settled) {
5237
- throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
5238
- }
5239
- await deps.burnJournal.removeByKey(burnId);
5646
+ async function readTokenJustification(deps, tokenId) {
5647
+ return deps.engine.readTokenJustification(await heldToken2(deps, tokenId));
5240
5648
  }
5241
- async function burnJournaled(deps, entry, token, state) {
5242
- let current = entry;
5243
- if (current.burnedTokenHex === null) {
5244
- if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
5245
- const burned = await deps.engine.burn(
5246
- { token, reasonBytes: hexToBytes(current.reasonHex) },
5247
- { transferId: current.burnId }
5248
- );
5249
- if (burned.blob.tokenId !== current.tokenId) {
5250
- throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
5251
- }
5252
- current = { ...current, burnedTokenHex: bytesToHex(burned.blob.token) };
5253
- await deps.burnJournal.upsert(current);
5649
+ async function heldToken2(deps, tokenId) {
5650
+ if (deps.view.stateHashOf(tokenId) === void 0) {
5651
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
5254
5652
  }
5255
- if (!state.applied) {
5256
- await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [] });
5653
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
5654
+ if (bytes === void 0) {
5655
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
5257
5656
  }
5258
- await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
5259
- current = { ...current, settled: true };
5260
- await deps.burnJournal.upsert(current);
5261
- deps.refreshView();
5262
- return current;
5657
+ return deps.engine.decodeToken({ tokenId, token: bytes });
5263
5658
  }
5264
5659
 
5265
5660
  // modules/payments-v2/send-whole.ts
@@ -5457,6 +5852,7 @@ var PaymentsFacade = class {
5457
5852
  track: (op) => this.trackTail(op)
5458
5853
  });
5459
5854
  this.view = parts.view;
5855
+ this.verdicts = parts.verdicts;
5460
5856
  this.wakeRefresh = parts.refreshView;
5461
5857
  this.ledger = parts.ledger;
5462
5858
  this.queue = parts.queue;
@@ -5470,6 +5866,7 @@ var PaymentsFacade = class {
5470
5866
  this.restoreDeps = parts.restoreDeps;
5471
5867
  this.requests = parts.requests;
5472
5868
  this.pins = parts.pins;
5869
+ this.burnHold = parts.burnHold;
5473
5870
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
5474
5871
  this.heartbeat = new ConvergenceHeartbeat({
5475
5872
  now: () => this.nowMs(),
@@ -5493,6 +5890,7 @@ var PaymentsFacade = class {
5493
5890
  wakeRefresh;
5494
5891
  prewarmed = new PrewarmCache();
5495
5892
  view;
5893
+ verdicts;
5496
5894
  ledger;
5497
5895
  queue;
5498
5896
  machine;
@@ -5519,6 +5917,7 @@ var PaymentsFacade = class {
5519
5917
  activeMoneyOps = /* @__PURE__ */ new Set();
5520
5918
  /** #737: the ledger holds exactly the sources of the still-open intents. */
5521
5919
  pins;
5920
+ burnHold;
5522
5921
  /** Per facade, so per address: an NFT reading is never served to another wallet. */
5523
5922
  nftCache = new NftCache();
5524
5923
  // ── lifecycle ──────────────────────────────────────────────────────────────
@@ -5543,7 +5942,8 @@ var PaymentsFacade = class {
5543
5942
  await this.deps.session.start();
5544
5943
  await this.pins.sync().catch(() => void 0);
5545
5944
  this.trackTail(this.runConvergencePass());
5546
- this.trackTail(this.seedHeldStates());
5945
+ this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
5946
+ await this.verdicts.start().catch(() => void 0);
5547
5947
  await this.view.fullPull().catch(() => void 0);
5548
5948
  this.receiveLoop.start(this.deps.receivePollMs);
5549
5949
  }
@@ -5552,6 +5952,7 @@ var PaymentsFacade = class {
5552
5952
  this.started = false;
5553
5953
  this.heartbeat.stop();
5554
5954
  this.receiveLoop.stop();
5955
+ this.verdicts.stop();
5555
5956
  for (const unsubscribe of this.unsubscribers) unsubscribe();
5556
5957
  this.unsubscribers = [];
5557
5958
  await this.deps.session.stop();
@@ -5568,12 +5969,17 @@ var PaymentsFacade = class {
5568
5969
  }
5569
5970
  // ── reads ──────────────────────────────────────────────────────────────────
5570
5971
  async assets(coinId) {
5972
+ return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
5973
+ }
5974
+ unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
5975
+ async heldAssets(coinId) {
5571
5976
  const all = await this.view.assets(this.deps.registry, this.deps.price);
5572
5977
  return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
5573
5978
  }
5574
5979
  tokens(filter) {
5575
- return this.view.tokens(this.deps.registry, filter);
5980
+ return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
5576
5981
  }
5982
+ unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
5577
5983
  coinless() {
5578
5984
  return this.view.coinless(this.deps.registry);
5579
5985
  }
@@ -5634,9 +6040,9 @@ var PaymentsFacade = class {
5634
6040
  return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
5635
6041
  }
5636
6042
  mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
5637
- burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
5638
- pendingBurns = () => this.track(pendingBurns(this.mintDeps()));
5639
- acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.mintDeps(), burnId));
6043
+ burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
6044
+ pendingBurns = () => this.track(pendingBurns(this.machineStores));
6045
+ acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
5640
6046
  // §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
5641
6047
  async resumeNow() {
5642
6048
  if (!this.started) return;
@@ -5679,7 +6085,8 @@ var PaymentsFacade = class {
5679
6085
  isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
5680
6086
  tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
5681
6087
  emit: this.deps.emit,
5682
- now: () => this.nowMs()
6088
+ now: () => this.nowMs(),
6089
+ accepted: (token) => this.verdicts.accept(token)
5683
6090
  };
5684
6091
  }
5685
6092
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
@@ -6017,16 +6424,6 @@ var PaymentsFacade = class {
6017
6424
  newId() {
6018
6425
  return (this.deps.newId ?? randomUUID)();
6019
6426
  }
6020
- async seedHeldStates() {
6021
- let page = await this.deps.storagePort.listInventory();
6022
- for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
6023
- for (const item of page.items) {
6024
- if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
6025
- }
6026
- if (!page.more) return;
6027
- page = await this.deps.storagePort.listInventory(page.cursor);
6028
- }
6029
- }
6030
6427
  track(op) {
6031
6428
  const settled = op.then(() => void 0, () => void 0);
6032
6429
  this.pendingOps.add(settled);
@@ -6042,6 +6439,7 @@ var PaymentsFacade = class {
6042
6439
  ATTENTION_MINT_UNRESOLVED,
6043
6440
  ATTENTION_RECIPIENT_NETWORK_UNVERIFIED,
6044
6441
  ATTENTION_RESEED_REJECTED,
6442
+ CUSTOM_MINT_MAX_PAYLOAD_BYTES,
6045
6443
  HEARTBEAT_CAP_MS,
6046
6444
  HEARTBEAT_SEED_MS,
6047
6445
  MAX_RESELECT,
@@ -6055,6 +6453,7 @@ var PaymentsFacade = class {
6055
6453
  STORE_KEYS,
6056
6454
  createScopedKV,
6057
6455
  encodeNftContent,
6456
+ isLiveBurn,
6058
6457
  isNftDocumentLink,
6059
6458
  isRetryableAckError,
6060
6459
  parseNftDocument,