@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
@@ -1695,6 +1695,33 @@ function decryptField(key, envelope) {
1695
1695
  return new TextDecoder().decode(decryptFieldBytes(key, envelope));
1696
1696
  }
1697
1697
 
1698
+ // token-engine/claims.ts
1699
+ var CoinClaims = class _CoinClaims {
1700
+ issuers = /* @__PURE__ */ new Map();
1701
+ policies = [];
1702
+ static fromPlugins(plugins) {
1703
+ const claims = new _CoinClaims();
1704
+ for (const plugin of plugins ?? []) {
1705
+ for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
1706
+ }
1707
+ return claims;
1708
+ }
1709
+ add(policy) {
1710
+ const tokenType = HexConverter.encode(policy.tokenType.bytes);
1711
+ const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
1712
+ const taken = coinIds.find((coinId) => this.issuers.has(coinId));
1713
+ if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
1714
+ for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
1715
+ this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
1716
+ }
1717
+ fingerprint() {
1718
+ return [...this.policies].sort().join("\n");
1719
+ }
1720
+ issuerOf(coinId) {
1721
+ return this.issuers.get(coinId.toLowerCase()) ?? null;
1722
+ }
1723
+ };
1724
+
1698
1725
  // modules/payments-v2/machine/payload-view.ts
1699
1726
  function isWholeIntent(p) {
1700
1727
  return p.kind === "whole" || p.kind === "coinless";
@@ -1972,6 +1999,9 @@ function createScopedKV(storage, network, chainPubkey) {
1972
1999
  }
1973
2000
  };
1974
2001
  }
2002
+ function isLiveBurn(entry) {
2003
+ return !entry.settled && entry.failure === void 0;
2004
+ }
1975
2005
  var STORE_KEYS = {
1976
2006
  intentBackstop: "intents",
1977
2007
  checkpointCache: "checkpoints",
@@ -1986,7 +2016,8 @@ var STORE_KEYS = {
1986
2016
  epochLatch: "epoch-latch",
1987
2017
  // §5.2 InventoryView durable overlays (#625/#679).
1988
2018
  suspectedSpent: "suspected-spent",
1989
- knownSpends: "known-spends"
2019
+ knownSpends: "known-spends",
2020
+ verifiedTokens: "verified-tokens"
1990
2021
  };
1991
2022
 
1992
2023
  // modules/payments-v2/inventory/presentation.ts
@@ -2004,18 +2035,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
2004
2035
  createdAt: snapshot.createdAt,
2005
2036
  updatedAt: snapshot.updatedAt,
2006
2037
  lazy: true,
2007
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
2038
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {},
2039
+ ...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
2008
2040
  };
2009
2041
  }
2010
- function aggregateAssets(entries, isTransferring, registry) {
2042
+ function aggregateAssets(entries, reading, registry) {
2011
2043
  const groups = /* @__PURE__ */ new Map();
2012
2044
  for (const [tokenId, entry] of entries) {
2013
- const moving = isTransferring(tokenId);
2045
+ const moving = reading.isTransferring(tokenId);
2014
2046
  for (const asset of entry.assets) {
2015
- let group = groups.get(asset.coinId);
2047
+ const standing = reading.standing(tokenId, asset.coinId);
2048
+ const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
2049
+ let group = groups.get(key);
2016
2050
  if (!group) {
2017
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
2018
- groups.set(asset.coinId, group);
2051
+ group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
2052
+ groups.set(key, group);
2019
2053
  }
2020
2054
  if (moving) {
2021
2055
  group.transferring += BigInt(asset.amount);
@@ -2026,9 +2060,10 @@ function aggregateAssets(entries, isTransferring, registry) {
2026
2060
  }
2027
2061
  }
2028
2062
  }
2029
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
2063
+ return [...groups.values()].map((totals) => toAsset(totals, registry));
2030
2064
  }
2031
- function toAsset(coinId, totals, registry) {
2065
+ function toAsset(totals, registry) {
2066
+ const coinId = totals.coinId;
2032
2067
  const iconUrl = registry.getIconUrl(coinId);
2033
2068
  return {
2034
2069
  coinId,
@@ -2048,7 +2083,8 @@ function toAsset(coinId, totals, registry) {
2048
2083
  priceEur: null,
2049
2084
  change24h: null,
2050
2085
  fiatValueUsd: null,
2051
- fiatValueEur: null
2086
+ fiatValueEur: null,
2087
+ ...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
2052
2088
  };
2053
2089
  }
2054
2090
  function priceIdOf(registry, coinId) {
@@ -2056,9 +2092,9 @@ function priceIdOf(registry, coinId) {
2056
2092
  }
2057
2093
  async function withPrices(raw, registry, price) {
2058
2094
  try {
2059
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
2095
+ const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
2060
2096
  const quotes = await price.getPrices(ids);
2061
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
2097
+ return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
2062
2098
  } catch {
2063
2099
  return raw;
2064
2100
  }
@@ -2079,11 +2115,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
2079
2115
  const snapshot = { assets: [], createdAt: now, updatedAt: now };
2080
2116
  return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
2081
2117
  transferring: true,
2082
- suspectedSpent: false
2118
+ suspectedSpent: false,
2119
+ standing: "trusted"
2083
2120
  });
2084
2121
  }
2085
2122
 
2086
2123
  // modules/payments-v2/inventory/InventoryView.ts
2124
+ var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
2087
2125
  var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
2088
2126
  var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
2089
2127
  var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
@@ -2177,6 +2215,12 @@ var InventoryView = class {
2177
2215
  for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
2178
2216
  if (changed) this.deps.emit("inventory:updated");
2179
2217
  }
2218
+ get trust() {
2219
+ return this.deps.trust ?? TRUST_ALL;
2220
+ }
2221
+ trusts(tokenId, coinId) {
2222
+ return this.trust.standing(tokenId, coinId) === "trusted";
2223
+ }
2180
2224
  pinned(tokenId) {
2181
2225
  return this.deps.isPinned?.(tokenId) ?? false;
2182
2226
  }
@@ -2203,6 +2247,7 @@ var InventoryView = class {
2203
2247
  isSpendable(tokenId, entry) {
2204
2248
  if (entry.status !== "active") return false;
2205
2249
  if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
2250
+ if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
2206
2251
  return !this.suspected.has(stateKey(tokenId, entry.stateHash));
2207
2252
  }
2208
2253
  /** Is this NAMED token spendable whole — coinless or valued (#777)? */
@@ -2229,7 +2274,8 @@ var InventoryView = class {
2229
2274
  out.push(
2230
2275
  toToken(tokenId, entry, asset, registry, {
2231
2276
  transferring: this.held(tokenId),
2232
- suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
2277
+ suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
2278
+ standing: this.trust.standing(tokenId, asset.coinId)
2233
2279
  })
2234
2280
  );
2235
2281
  }
@@ -2259,10 +2305,21 @@ var InventoryView = class {
2259
2305
  return out;
2260
2306
  }
2261
2307
  async assets(registry, price) {
2262
- const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
2308
+ const reading = {
2309
+ isTransferring: (tokenId) => this.held(tokenId),
2310
+ standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
2311
+ };
2312
+ const raw = aggregateAssets(this.activeEntries(), reading, registry);
2263
2313
  if (!price || raw.length === 0) return raw;
2264
2314
  return withPrices(raw, registry, price);
2265
2315
  }
2316
+ claimHolders() {
2317
+ const out = [];
2318
+ for (const [tokenId, entry] of this.activeEntries()) {
2319
+ if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
2320
+ }
2321
+ return out;
2322
+ }
2266
2323
  *activeEntries() {
2267
2324
  for (const [tokenId, entry] of this.mirror) {
2268
2325
  if (entry.status === "active") yield [tokenId, entry];
@@ -2432,6 +2489,171 @@ var InventoryView = class {
2432
2489
  }
2433
2490
  };
2434
2491
 
2492
+ // modules/payments-v2/inventory/verdicts.ts
2493
+ var VERDICT_RETRY_MS = 3e4;
2494
+ var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
2495
+ function honoursClaims(claims, token) {
2496
+ const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
2497
+ return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
2498
+ }
2499
+ var TokenVerdicts = class {
2500
+ constructor(deps) {
2501
+ this.deps = deps;
2502
+ }
2503
+ verified = /* @__PURE__ */ new Set();
2504
+ refused = /* @__PURE__ */ new Set();
2505
+ retries = /* @__PURE__ */ new Map();
2506
+ writes = new SerialChain();
2507
+ hydration = null;
2508
+ queued = null;
2509
+ running = null;
2510
+ held = [];
2511
+ timer = null;
2512
+ live = false;
2513
+ isClaimed(coinId) {
2514
+ return this.deps.claims.issuerOf(coinId) !== null;
2515
+ }
2516
+ standing(tokenId, coinId) {
2517
+ if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
2518
+ return this.refused.has(tokenId) ? "refused" : "pending";
2519
+ }
2520
+ trusts(tokenId, coinId) {
2521
+ return this.standing(tokenId, coinId) === "trusted";
2522
+ }
2523
+ hydrate() {
2524
+ this.hydration ??= this.load().catch((error) => {
2525
+ this.hydration = null;
2526
+ throw error;
2527
+ });
2528
+ return this.hydration;
2529
+ }
2530
+ start() {
2531
+ this.live = true;
2532
+ return this.hydrate();
2533
+ }
2534
+ stop() {
2535
+ this.live = false;
2536
+ if (this.timer !== null) clearTimeout(this.timer);
2537
+ this.timer = null;
2538
+ }
2539
+ async accept(token) {
2540
+ if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
2541
+ }
2542
+ async recordSplit(sourceTokenId, outputTokenId) {
2543
+ await this.hydrate();
2544
+ if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
2545
+ }
2546
+ review(holders) {
2547
+ this.queued = holders;
2548
+ this.running ??= this.drain();
2549
+ return this.running;
2550
+ }
2551
+ async drain() {
2552
+ try {
2553
+ while (this.queued !== null) {
2554
+ const holders = this.queued;
2555
+ this.queued = null;
2556
+ await this.reviewOnce(holders);
2557
+ }
2558
+ } finally {
2559
+ this.running = null;
2560
+ }
2561
+ }
2562
+ async reviewOnce(holders) {
2563
+ await this.hydrate();
2564
+ this.held = holders;
2565
+ const due = holders.filter((tokenId) => this.isDue(tokenId));
2566
+ const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
2567
+ for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
2568
+ if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
2569
+ await this.persist();
2570
+ this.deps.changed();
2571
+ }
2572
+ this.scheduleRetry();
2573
+ }
2574
+ isDue(tokenId) {
2575
+ if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
2576
+ return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
2577
+ }
2578
+ async check(due) {
2579
+ const verdicts = /* @__PURE__ */ new Map();
2580
+ let blobs;
2581
+ try {
2582
+ blobs = await this.deps.getBlobs([...due]);
2583
+ } catch {
2584
+ return verdicts;
2585
+ }
2586
+ for (const tokenId of due) {
2587
+ const bytes = blobs.get(tokenId);
2588
+ if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
2589
+ }
2590
+ return verdicts;
2591
+ }
2592
+ async verdictOf(tokenId, bytes) {
2593
+ let engine;
2594
+ try {
2595
+ engine = this.deps.engine();
2596
+ } catch {
2597
+ return "retry";
2598
+ }
2599
+ let token;
2600
+ try {
2601
+ token = await engine.decodeToken({ tokenId, token: bytes });
2602
+ } catch {
2603
+ return "refused";
2604
+ }
2605
+ if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
2606
+ try {
2607
+ return (await engine.verify(token)).ok ? "verified" : "refused";
2608
+ } catch {
2609
+ return "retry";
2610
+ }
2611
+ }
2612
+ settle(tokenId, verdict) {
2613
+ if (verdict === "retry") {
2614
+ this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
2615
+ return;
2616
+ }
2617
+ this.retries.delete(tokenId);
2618
+ (verdict === "verified" ? this.verified : this.refused).add(tokenId);
2619
+ }
2620
+ nextRetry(previous) {
2621
+ const attempts = (previous?.attempts ?? 0) + 1;
2622
+ const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
2623
+ return { attempts, atMs: this.now() + delay };
2624
+ }
2625
+ scheduleRetry() {
2626
+ if (!this.live || this.timer !== null) return;
2627
+ const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
2628
+ if (due.length === 0) return;
2629
+ const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
2630
+ this.timer = setTimeout(() => {
2631
+ this.timer = null;
2632
+ void this.review(this.held).catch(() => void 0);
2633
+ }, delay);
2634
+ }
2635
+ async remember(tokenId) {
2636
+ await this.hydrate();
2637
+ if (this.verified.has(tokenId)) return;
2638
+ this.verified.add(tokenId);
2639
+ this.refused.delete(tokenId);
2640
+ this.retries.delete(tokenId);
2641
+ await this.persist();
2642
+ }
2643
+ persist() {
2644
+ const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
2645
+ return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
2646
+ }
2647
+ async load() {
2648
+ const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
2649
+ if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
2650
+ for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
2651
+ }
2652
+ now() {
2653
+ return (this.deps.now ?? Date.now)();
2654
+ }
2655
+ };
2656
+
2435
2657
  // token-engine/value-envelope.ts
2436
2658
  function isCoinlessEnvelope(envelope) {
2437
2659
  return envelope.startsWith("none_");
@@ -2446,6 +2668,8 @@ function isRetryableAckError(err) {
2446
2668
  var ACK_BATCH_SIZE = 200;
2447
2669
  var REFRESH_INTERVAL_MS = 2500;
2448
2670
  var POLL_INTERVAL_MS = 3e4;
2671
+ var RECHECK_BASE_MS = POLL_INTERVAL_MS;
2672
+ var RECHECK_MAX_MS = 60 * 60 * 1e3;
2449
2673
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
2450
2674
  function receivedDedupKey(tokenId, stateHash) {
2451
2675
  return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
@@ -2462,6 +2686,7 @@ var Receive = class {
2462
2686
  pollTimer = null;
2463
2687
  unsubscribeWake = null;
2464
2688
  lastRefreshAt = 0;
2689
+ rechecks = /* @__PURE__ */ new Map();
2465
2690
  drainOnce() {
2466
2691
  return this.drainFlight.run(() => this.doDrain());
2467
2692
  }
@@ -2520,6 +2745,7 @@ var Receive = class {
2520
2745
  const pending = [];
2521
2746
  const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
2522
2747
  const claimable = { n: 0 };
2748
+ const deferred = { any: false };
2523
2749
  const finish = () => {
2524
2750
  if (claimable.n > 0) deps.refreshView?.();
2525
2751
  return stored;
@@ -2528,7 +2754,7 @@ var Receive = class {
2528
2754
  const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
2529
2755
  let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
2530
2756
  for (let pass = 0; pass < 2; pass++) {
2531
- await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
2757
+ await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
2532
2758
  const served = deps.delivery.incomingEpoch();
2533
2759
  if (basis === null || served === null || served === basis.syncEpoch) break;
2534
2760
  await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
@@ -2545,20 +2771,41 @@ var Receive = class {
2545
2771
  return finish();
2546
2772
  }
2547
2773
  async processEntry(ctx, entry) {
2548
- const { deps, engine, pending, stored, claimable } = ctx;
2549
- const screened = await screen(deps, engine, entry);
2774
+ const { deps, engine, stored, claimable } = ctx;
2775
+ const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
2776
+ this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
2777
+ if (screened.kind === "defer") {
2778
+ ctx.deferred.any = true;
2779
+ return;
2780
+ }
2550
2781
  if (screened.kind === "ack") {
2551
- pending.push(screened.ack);
2782
+ queueAck(ctx, screened.ack);
2552
2783
  if (screened.ack.disposition === "claimed") claimable.n += 1;
2553
2784
  return;
2554
2785
  }
2786
+ await deps.accepted?.(screened.token);
2555
2787
  await deps.view.store(screened.record);
2556
- pending.push(claimAck(entry));
2788
+ queueAck(ctx, claimAck(entry));
2557
2789
  claimable.n += 1;
2558
2790
  const transfer = await announce(deps, entry, screened.record);
2559
2791
  stored.push(transfer);
2560
2792
  deps.emit("transfer:incoming", transfer);
2561
2793
  }
2794
+ recheckDue(deliveryId) {
2795
+ const recheck = this.rechecks.get(deliveryId);
2796
+ return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
2797
+ }
2798
+ scheduleRecheck(deliveryId, deferred) {
2799
+ if (!deferred) {
2800
+ this.rechecks.delete(deliveryId);
2801
+ return;
2802
+ }
2803
+ const previous = this.rechecks.get(deliveryId);
2804
+ if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
2805
+ const attempts = (previous?.attempts ?? 0) + 1;
2806
+ const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
2807
+ this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
2808
+ }
2562
2809
  };
2563
2810
  async function screen(deps, engine, entry) {
2564
2811
  const blobBytes = await entry.fetchBlob();
@@ -2569,7 +2816,8 @@ async function screen(deps, engine, entry) {
2569
2816
  logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
2570
2817
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
2571
2818
  }
2572
- const verdict = await engine.verify(token);
2819
+ const verdict = await verdictOf(engine, token);
2820
+ if (verdict === null) return { kind: "defer" };
2573
2821
  if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
2574
2822
  if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
2575
2823
  return { kind: "ack", ack: rejectAck(entry, "not-owned") };
@@ -2580,38 +2828,43 @@ async function screen(deps, engine, entry) {
2580
2828
  if (held !== null && await engine.isSpent(token)) {
2581
2829
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
2582
2830
  }
2831
+ const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
2583
2832
  return {
2584
2833
  kind: "accept",
2834
+ token,
2585
2835
  record: {
2586
2836
  tokenId: keys.tokenId,
2587
2837
  stateHash: keys.stateHash,
2588
2838
  assets: toAssetAmounts(token),
2589
- ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
2839
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
2840
+ ...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
2590
2841
  }
2591
2842
  };
2592
2843
  }
2593
- async function announce(deps, entry, record) {
2594
- const receivedAt = (deps.now ?? Date.now)();
2844
+ function unverifiedCoinsOf(deps, token) {
2845
+ const claims = deps.claims;
2846
+ if (claims === void 0 || honoursClaims(claims, token)) return [];
2847
+ return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
2848
+ }
2849
+ async function verdictOf(engine, token) {
2595
2850
  try {
2596
- await deps.recordReceived({
2597
- dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
2598
- tokenId: record.tokenId,
2599
- stateHash: record.stateHash,
2600
- assets: record.assets,
2601
- ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
2602
- ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
2603
- ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
2604
- ...entry.memo !== void 0 ? { memo: entry.memo } : {},
2605
- receivedAt
2606
- });
2851
+ return await engine.verify(token);
2607
2852
  } catch (err) {
2608
- logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
2853
+ if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
2854
+ throw err;
2609
2855
  }
2856
+ }
2857
+ async function announce(deps, entry, record) {
2858
+ const receivedAt = (deps.now ?? Date.now)();
2859
+ await recordArrival(deps, entry, record, receivedAt);
2860
+ const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
2861
+ const counted = record.assets.filter((asset) => !refused2.includes(asset));
2610
2862
  return {
2611
2863
  id: record.tokenId,
2612
2864
  senderPubkey: entry.senderPubkey ?? "",
2613
2865
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
2614
- tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
2866
+ tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
2867
+ ...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
2615
2868
  // #777: named here rather than mapped from assets, which announced an EMPTY list.
2616
2869
  ...record.tokenType !== void 0 ? {
2617
2870
  coinless: [
@@ -2629,6 +2882,25 @@ async function announce(deps, entry, record) {
2629
2882
  receivedAt
2630
2883
  };
2631
2884
  }
2885
+ async function recordArrival(deps, entry, record, receivedAt) {
2886
+ const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
2887
+ if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
2888
+ try {
2889
+ await deps.recordReceived({
2890
+ dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
2891
+ tokenId: record.tokenId,
2892
+ stateHash: record.stateHash,
2893
+ assets,
2894
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
2895
+ ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
2896
+ ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
2897
+ ...entry.memo !== void 0 ? { memo: entry.memo } : {},
2898
+ receivedAt
2899
+ });
2900
+ } catch (err) {
2901
+ logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
2902
+ }
2903
+ }
2632
2904
  async function flushAcks(deps, pending, epochOf) {
2633
2905
  if (pending.length === 0) return { progressed: true };
2634
2906
  const before = pending.length;
@@ -2636,7 +2908,7 @@ async function flushAcks(deps, pending, epochOf) {
2636
2908
  try {
2637
2909
  const settled = await settleAcks(deps, pending);
2638
2910
  let i = 0;
2639
- while (i < pending.length && settled.has(pending[i].deliveryId)) {
2911
+ while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
2640
2912
  lastAcked = pending[i].cursor;
2641
2913
  i += 1;
2642
2914
  }
@@ -2712,6 +2984,9 @@ async function ackOne(deps, ack) {
2712
2984
  await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
2713
2985
  }
2714
2986
  }
2987
+ function queueAck(ctx, ack) {
2988
+ ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
2989
+ }
2715
2990
  function claimAck(entry) {
2716
2991
  return {
2717
2992
  deliveryId: entry.deliveryId,
@@ -3612,6 +3887,7 @@ var TransferMachine = class {
3612
3887
  const key = keys.get(digest);
3613
3888
  if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
3614
3889
  const { tokenId } = await engine.deliveryKeys(o.changeBlob);
3890
+ await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
3615
3891
  added.push({ tokenId, key });
3616
3892
  }
3617
3893
  return added;
@@ -4007,7 +4283,7 @@ var Converger = class {
4007
4283
  this.deps.stores.burnJournal.list()
4008
4284
  ]);
4009
4285
  const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
4010
- const burnsOpen = burns.some((e) => !e.settled);
4286
+ const burnsOpen = burns.some(isLiveBurn);
4011
4287
  return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
4012
4288
  }
4013
4289
  async resumePass() {
@@ -4094,6 +4370,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
4094
4370
  const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
4095
4371
  holds.set(entry.transferId, [...direct, ...split]);
4096
4372
  }
4373
+ for (const burn of await stores.burnJournal.list()) {
4374
+ if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
4375
+ }
4097
4376
  return { open: holds, complete };
4098
4377
  }
4099
4378
  function groupJournal(journal) {
@@ -4583,6 +4862,176 @@ function parseAmount(raw) {
4583
4862
  return amount;
4584
4863
  }
4585
4864
 
4865
+ // modules/payments-v2/burn.ts
4866
+ var HEX32 = /^[0-9a-f]{64}$/;
4867
+ async function heldToken(deps, tokenId) {
4868
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
4869
+ if (bytes === void 0) {
4870
+ throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
4871
+ }
4872
+ return deps.engine.decodeToken({ tokenId, token: bytes });
4873
+ }
4874
+ function assertBurnable(engine, token, tokenId) {
4875
+ if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
4876
+ throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
4877
+ }
4878
+ if (token.valueEnvelope === "bare_collection") {
4879
+ throw new SphereError(
4880
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
4881
+ "VALIDATION_ERROR"
4882
+ );
4883
+ }
4884
+ }
4885
+ function assetsOf(token) {
4886
+ return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
4887
+ }
4888
+ async function runBurnUnderJournal(deps, input) {
4889
+ const { burnId } = input;
4890
+ const { tokenId, reasonBytes } = input.request;
4891
+ if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
4892
+ return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
4893
+ }
4894
+ let token;
4895
+ try {
4896
+ token = await heldToken(deps, tokenId);
4897
+ assertBurnable(deps.engine, token, tokenId);
4898
+ } catch (err) {
4899
+ return refused(burnId, tokenId, err);
4900
+ }
4901
+ const entry = {
4902
+ burnId,
4903
+ tokenId,
4904
+ reasonHex: bytesToHex(reasonBytes),
4905
+ burnedTokenHex: null,
4906
+ settled: false,
4907
+ assets: assetsOf(token),
4908
+ createdAt: deps.now()
4909
+ };
4910
+ await deps.burnJournal.upsert(entry);
4911
+ try {
4912
+ const settled = await burnJournaled(deps, entry, token);
4913
+ return { success: true, burnId, tokenId, burnedToken: hexToBytes(settled.burnedTokenHex ?? "") };
4914
+ } catch (err) {
4915
+ const failure = failureOf(err);
4916
+ if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
4917
+ else deps.armHeartbeat();
4918
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
4919
+ }
4920
+ }
4921
+ async function replayBurns(deps) {
4922
+ let progressed = 0;
4923
+ for (const entry of await deps.burnJournal.list()) {
4924
+ if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
4925
+ try {
4926
+ const token = entry.burnedTokenHex === null ? await heldToken(deps, entry.tokenId) : null;
4927
+ await burnJournaled(deps, entry, token);
4928
+ progressed += 1;
4929
+ } catch (err) {
4930
+ if (!isTerminal(err)) continue;
4931
+ await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
4932
+ progressed += 1;
4933
+ }
4934
+ }
4935
+ return progressed;
4936
+ }
4937
+ async function pendingBurns(deps) {
4938
+ return (await deps.burnJournal.list()).map((entry) => ({
4939
+ burnId: entry.burnId,
4940
+ tokenId: entry.tokenId,
4941
+ reasonBytes: hexToBytes(entry.reasonHex),
4942
+ burnedToken: entry.burnedTokenHex === null ? null : hexToBytes(entry.burnedTokenHex),
4943
+ settled: entry.settled,
4944
+ createdAt: entry.createdAt,
4945
+ ...entry.failure !== void 0 ? { failure: entry.failure } : {}
4946
+ }));
4947
+ }
4948
+ async function acknowledgeBurn(deps, burnId) {
4949
+ const entry = await deps.burnJournal.getByKey(burnId);
4950
+ if (entry === void 0) return;
4951
+ if (isLiveBurn(entry)) {
4952
+ throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
4953
+ }
4954
+ await deps.burnJournal.removeByKey(burnId);
4955
+ }
4956
+ function refused(burnId, tokenId, err) {
4957
+ const failure = failureOf(err);
4958
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
4959
+ }
4960
+ function isTerminal(err) {
4961
+ return classifyError(err) === "conflict";
4962
+ }
4963
+ function failureOf(err) {
4964
+ const code = err !== null && typeof err === "object" ? err.code : void 0;
4965
+ return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
4966
+ }
4967
+ async function burnJournaled(deps, entry, token) {
4968
+ let current = entry;
4969
+ if (current.burnedTokenHex === null) {
4970
+ if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
4971
+ const burned = await deps.engine.burn(
4972
+ { token, reasonBytes: hexToBytes(current.reasonHex) },
4973
+ { transferId: current.burnId }
4974
+ );
4975
+ if (burned.blob.tokenId !== current.tokenId) {
4976
+ throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
4977
+ }
4978
+ current = { ...current, burnedTokenHex: bytesToHex(burned.blob.token) };
4979
+ await deps.burnJournal.upsert(current);
4980
+ }
4981
+ await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
4982
+ await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
4983
+ current = { ...current, settled: true };
4984
+ await deps.burnJournal.upsert(current);
4985
+ deps.refreshView();
4986
+ return current;
4987
+ }
4988
+
4989
+ // modules/payments-v2/burn-hold.ts
4990
+ var BurnHold = class {
4991
+ constructor(deps) {
4992
+ this.deps = deps;
4993
+ }
4994
+ async run(burnId, tokenId, burn) {
4995
+ try {
4996
+ this.deps.queue.planWhole(burnId, tokenId);
4997
+ } catch (err) {
4998
+ return refused(burnId, tokenId, err);
4999
+ }
5000
+ this.deps.view.markInFlightMany([tokenId]);
5001
+ let burned = false;
5002
+ try {
5003
+ const result = await burn();
5004
+ burned = result.success;
5005
+ return result;
5006
+ } finally {
5007
+ await this.settle(burnId, tokenId, burned);
5008
+ }
5009
+ }
5010
+ async settle(burnId, tokenId, burned) {
5011
+ if (burned) {
5012
+ this.deps.ledger.commit(burnId);
5013
+ this.deps.track(this.refreshThenRelease(tokenId));
5014
+ return;
5015
+ }
5016
+ const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
5017
+ if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
5018
+ this.deps.ledger.cancel(burnId);
5019
+ this.deps.view.releaseMany([tokenId]);
5020
+ this.deps.queue.notifyChange("");
5021
+ return;
5022
+ }
5023
+ this.deps.pins.adopt(burnId);
5024
+ }
5025
+ async refreshThenRelease(tokenId) {
5026
+ try {
5027
+ await this.deps.view.delta();
5028
+ this.deps.view.releaseMany([tokenId]);
5029
+ } finally {
5030
+ this.deps.queue.notifyChange("");
5031
+ }
5032
+ }
5033
+ };
5034
+
4586
5035
  // modules/payments-v2/compose.ts
4587
5036
  function supportsDeterministicMint(engine) {
4588
5037
  return engine.deterministicMint === true;
@@ -4590,16 +5039,8 @@ function supportsDeterministicMint(engine) {
4590
5039
  function composeFacadeParts(deps, hooks) {
4591
5040
  const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
4592
5041
  const ledger = new ReservationLedger();
4593
- const view = new InventoryView({
4594
- port: deps.storagePort,
4595
- kv: deps.kv,
4596
- emit: (event) => deps.emit(event, {}),
4597
- // #737: a reserved token is unselectable, so it is never confirmed balance.
4598
- // #738: while the held-set is unproven, EVERY token reads pinned — the report
4599
- // must not call a token spendable that the queue is about to refuse to spend.
4600
- isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
4601
- ...deps.now !== void 0 ? { now: deps.now } : {}
4602
- });
5042
+ const verdicts = buildVerdicts(deps, hooks);
5043
+ const view = buildView(deps, hooks, ledger, verdicts);
4603
5044
  const queue = new SpendQueue({
4604
5045
  ledger,
4605
5046
  getPool: (coinId) => view.pool(coinId),
@@ -4614,27 +5055,57 @@ function composeFacadeParts(deps, hooks) {
4614
5055
  ...deps.now !== void 0 ? { now: deps.now } : {},
4615
5056
  ...deps.newId !== void 0 ? { newId: deps.newId } : {}
4616
5057
  });
4617
- const machineDeps = buildMachineDeps(deps, hooks, historyStore);
5058
+ const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
4618
5059
  const machineStores = createMachineStores(deps.kv);
4619
5060
  const heldStates = /* @__PURE__ */ new Map();
4620
5061
  const refreshView = coalesced(() => view.delta(), hooks.track);
5062
+ const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
4621
5063
  return {
4622
5064
  ownPubkeyBytes,
4623
5065
  view,
5066
+ verdicts,
4624
5067
  refreshView,
4625
5068
  ledger,
4626
- pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
5069
+ pins,
4627
5070
  queue,
5071
+ burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
4628
5072
  historyStore,
4629
5073
  machineStores,
4630
5074
  machineDeps,
4631
5075
  machine: new TransferMachine(machineDeps),
4632
5076
  heldStates,
4633
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
5077
+ receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
4634
5078
  requests: buildRequests(deps, hooks),
4635
5079
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
4636
5080
  };
4637
5081
  }
5082
+ function buildView(deps, hooks, ledger, verdicts) {
5083
+ const view = new InventoryView({
5084
+ port: deps.storagePort,
5085
+ kv: deps.kv,
5086
+ trust: verdicts,
5087
+ emit: (event) => {
5088
+ deps.emit(event, {});
5089
+ hooks.track(verdicts.review(view.claimHolders()));
5090
+ },
5091
+ // #737: a reserved token is unselectable, so it is never confirmed balance.
5092
+ // #738: while the held-set is unproven, EVERY token reads pinned — the report
5093
+ // must not call a token spendable that the queue is about to refuse to spend.
5094
+ isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
5095
+ ...deps.now !== void 0 ? { now: deps.now } : {}
5096
+ });
5097
+ return view;
5098
+ }
5099
+ function buildVerdicts(deps, hooks) {
5100
+ return new TokenVerdicts({
5101
+ kv: deps.kv,
5102
+ claims: deps.claims ?? new CoinClaims(),
5103
+ engine: () => hooks.engine(),
5104
+ getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
5105
+ changed: () => deps.emit("inventory:updated", {}),
5106
+ ...deps.now !== void 0 ? { now: deps.now } : {}
5107
+ });
5108
+ }
4638
5109
  function buildPins(deps, hooks, parts) {
4639
5110
  return new IntentPins({
4640
5111
  ledger: parts.ledger,
@@ -4662,8 +5133,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
4662
5133
  }
4663
5134
  };
4664
5135
  }
4665
- function buildMachineDeps(deps, hooks, historyStore) {
5136
+ function buildMachineDeps(deps, hooks, historyStore, verdicts) {
4666
5137
  return {
5138
+ recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
4667
5139
  engine: () => hooks.engine(),
4668
5140
  storage: deps.storagePort,
4669
5141
  delivery: deps.deliveryPort,
@@ -4696,10 +5168,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
4696
5168
  }
4697
5169
  };
4698
5170
  }
4699
- function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
5171
+ function buildReceive(deps, hooks, parts, refreshView) {
5172
+ const { historyStore, heldStates, verdicts } = parts;
4700
5173
  return new Receive({
4701
5174
  delivery: deps.deliveryPort,
4702
5175
  engine: () => hooks.engine(),
5176
+ claims: deps.claims ?? new CoinClaims(),
5177
+ accepted: (token) => verdicts.accept(token),
4703
5178
  view: {
4704
5179
  heldState: (tokenId) => heldStates.get(tokenId) ?? null,
4705
5180
  store: async (entry) => {
@@ -4818,6 +5293,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
4818
5293
  }
4819
5294
  return false;
4820
5295
  }
5296
+ async function seedHeldStates(storagePort, heldStates) {
5297
+ let page = await storagePort.listInventory();
5298
+ for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
5299
+ for (const item of page.items) {
5300
+ if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
5301
+ }
5302
+ if (!page.more) return;
5303
+ page = await storagePort.listInventory(page.cursor);
5304
+ }
5305
+ }
4821
5306
  async function replayCoinMints(deps) {
4822
5307
  let resolved = 0;
4823
5308
  for (const entry of await deps.mintJournal.list()) {
@@ -4921,98 +5406,9 @@ async function mintJournaled(deps, entry) {
4921
5406
  await finalizeMint(deps, { mintId: entry.mintId, token, assets: [], journal: deps.nftMintJournal });
4922
5407
  }
4923
5408
 
4924
- // modules/payments-v2/inventory/nft-read.ts
4925
- var NFT_CACHE_MAX = 256;
4926
- var NftCache = class {
4927
- constructor(max = NFT_CACHE_MAX) {
4928
- this.max = max;
4929
- }
4930
- entries = /* @__PURE__ */ new Map();
4931
- /** `undefined` = not cached. A hit becomes the most recently used entry. */
4932
- get(tokenId) {
4933
- const view = this.entries.get(tokenId);
4934
- if (view === void 0) return void 0;
4935
- this.entries.delete(tokenId);
4936
- this.entries.set(tokenId, view);
4937
- return view;
4938
- }
4939
- set(tokenId, view) {
4940
- this.entries.delete(tokenId);
4941
- this.entries.set(tokenId, view);
4942
- const oldest = this.entries.keys().next().value;
4943
- if (this.entries.size > this.max && oldest !== void 0) this.entries.delete(oldest);
4944
- }
4945
- };
4946
- async function readNft(deps, tokenId) {
4947
- if (deps.view.stateHashOf(tokenId) === void 0) {
4948
- throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
4949
- }
4950
- const cached = deps.cache.get(tokenId);
4951
- if (cached !== void 0) return cached;
4952
- const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
4953
- if (bytes === void 0) {
4954
- throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
4955
- }
4956
- return decodeView(deps, tokenId, bytes);
4957
- }
4958
- async function readNfts(deps, tokenIds) {
4959
- const held = [...new Set(tokenIds)].filter((tokenId) => deps.view.stateHashOf(tokenId) !== void 0);
4960
- const views = /* @__PURE__ */ new Map();
4961
- const misses = [];
4962
- for (const tokenId of held) {
4963
- const cached = deps.cache.get(tokenId);
4964
- if (cached === void 0) misses.push(tokenId);
4965
- else views.set(tokenId, cached);
4966
- }
4967
- const blobs = misses.length > 0 ? await deps.storagePort.getBlobs(misses) : /* @__PURE__ */ new Map();
4968
- for (const tokenId of misses) {
4969
- const bytes = blobs.get(tokenId);
4970
- if (bytes !== void 0) views.set(tokenId, await decodeView(deps, tokenId, bytes).catch(() => null));
4971
- }
4972
- const out = /* @__PURE__ */ new Map();
4973
- for (const tokenId of held) {
4974
- const view = views.get(tokenId);
4975
- if (view) out.set(tokenId, view);
4976
- }
4977
- return out;
4978
- }
4979
- async function decodeView(deps, tokenId, bytes) {
4980
- let view;
4981
- try {
4982
- const token = await deps.engine.decodeToken({ tokenId, token: bytes });
4983
- if (token.blob.tokenId !== tokenId) {
4984
- throw new SphereError(`the blob served for ${tokenId} decodes as ${token.blob.tokenId}`, "STORAGE_ERROR");
4985
- }
4986
- const reading = await deps.engine.readNft(token);
4987
- view = reading === null ? null : { tokenId, ...reading };
4988
- } catch (err) {
4989
- logger.warn("PaymentsV2", `NFT read: token ${tokenId} did not decode \u2014 ${String(err)}`);
4990
- throw err;
4991
- }
4992
- deps.cache.set(tokenId, view);
4993
- return view;
4994
- }
4995
-
4996
- // modules/payments-v2/inventory/token-data.ts
4997
- async function readTokenData(deps, tokenId) {
4998
- return deps.engine.readTokenData(await heldToken(deps, tokenId));
4999
- }
5000
- async function readTokenJustification(deps, tokenId) {
5001
- return deps.engine.readTokenJustification(await heldToken(deps, tokenId));
5002
- }
5003
- async function heldToken(deps, tokenId) {
5004
- if (deps.view.stateHashOf(tokenId) === void 0) {
5005
- throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
5006
- }
5007
- const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
5008
- if (bytes === void 0) {
5009
- throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
5010
- }
5011
- return deps.engine.decodeToken({ tokenId, token: bytes });
5012
- }
5013
-
5014
5409
  // modules/payments-v2/mint-custom.ts
5015
- var HEX32 = /^[0-9a-f]{64}$/;
5410
+ var HEX322 = /^[0-9a-f]{64}$/;
5411
+ var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
5016
5412
  function assertRequest(request) {
5017
5413
  if (request.tokenType.length !== 32) {
5018
5414
  throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
@@ -5023,19 +5419,27 @@ function assertRequest(request) {
5023
5419
  if (request.data.length === 0) {
5024
5420
  throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
5025
5421
  }
5422
+ if (request.justification !== void 0 && request.justification.length === 0) {
5423
+ throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
5424
+ }
5425
+ if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
5426
+ throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
5427
+ }
5026
5428
  for (const asset of request.assets) {
5027
- if (!HEX32.test(asset.coinId) || asset.amount <= 0n) {
5429
+ if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
5028
5430
  throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
5029
5431
  }
5030
5432
  }
5031
5433
  }
5032
5434
  async function runCustomMintUnderJournal(deps, input) {
5435
+ let entry;
5033
5436
  try {
5034
5437
  assertRequest(input.request);
5438
+ entry = journalEntryOf2(input.mintId, input.request, deps.now());
5439
+ deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
5035
5440
  } catch (err) {
5036
5441
  return { success: false, error: messageOf(err) };
5037
5442
  }
5038
- const entry = journalEntryOf2(input.mintId, input.request, deps.now());
5039
5443
  await deps.customMintJournal.upsert(entry);
5040
5444
  let tokenId;
5041
5445
  try {
@@ -5075,24 +5479,25 @@ function journalEntryOf2(mintId, request, createdAt) {
5075
5479
  createdAt
5076
5480
  };
5077
5481
  }
5482
+ function mintParamsOf(deps, entry, mintJustificationVerifiers) {
5483
+ return {
5484
+ recipientPubkey: deps.ownPubkeyBytes,
5485
+ data: hexToBytes(entry.dataHex),
5486
+ tokenType: hexToBytes(entry.tokenTypeHex),
5487
+ salt: hexToBytes(entry.saltHex),
5488
+ ...entry.justificationHex !== null ? { justification: hexToBytes(entry.justificationHex) } : {},
5489
+ ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
5490
+ };
5491
+ }
5078
5492
  async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
5079
- const token = await deps.engine.mintDataToken(
5080
- {
5081
- recipientPubkey: deps.ownPubkeyBytes,
5082
- data: hexToBytes(entry.dataHex),
5083
- tokenType: hexToBytes(entry.tokenTypeHex),
5084
- salt: hexToBytes(entry.saltHex),
5085
- ...entry.justificationHex !== null ? { justification: hexToBytes(entry.justificationHex) } : {},
5086
- ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
5087
- },
5088
- { transferId: entry.mintId }
5089
- );
5493
+ const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
5090
5494
  if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
5091
5495
  throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
5092
5496
  }
5093
5497
  if (entry.tokenId === "") {
5094
5498
  await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
5095
5499
  }
5500
+ if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
5096
5501
  await finalizeMint(deps, {
5097
5502
  mintId: entry.mintId,
5098
5503
  token,
@@ -5102,106 +5507,94 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
5102
5507
  return token.blob.tokenId;
5103
5508
  }
5104
5509
 
5105
- // modules/payments-v2/burn.ts
5106
- var HEX322 = /^[0-9a-f]{64}$/;
5107
- async function heldToken2(deps, tokenId) {
5510
+ // modules/payments-v2/inventory/nft-read.ts
5511
+ var NFT_CACHE_MAX = 256;
5512
+ var NftCache = class {
5513
+ constructor(max = NFT_CACHE_MAX) {
5514
+ this.max = max;
5515
+ }
5516
+ entries = /* @__PURE__ */ new Map();
5517
+ /** `undefined` = not cached. A hit becomes the most recently used entry. */
5518
+ get(tokenId) {
5519
+ const view = this.entries.get(tokenId);
5520
+ if (view === void 0) return void 0;
5521
+ this.entries.delete(tokenId);
5522
+ this.entries.set(tokenId, view);
5523
+ return view;
5524
+ }
5525
+ set(tokenId, view) {
5526
+ this.entries.delete(tokenId);
5527
+ this.entries.set(tokenId, view);
5528
+ const oldest = this.entries.keys().next().value;
5529
+ if (this.entries.size > this.max && oldest !== void 0) this.entries.delete(oldest);
5530
+ }
5531
+ };
5532
+ async function readNft(deps, tokenId) {
5533
+ if (deps.view.stateHashOf(tokenId) === void 0) {
5534
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
5535
+ }
5536
+ const cached = deps.cache.get(tokenId);
5537
+ if (cached !== void 0) return cached;
5108
5538
  const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
5109
5539
  if (bytes === void 0) {
5110
- throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
5540
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
5111
5541
  }
5112
- return deps.engine.decodeToken({ tokenId, token: bytes });
5113
- }
5114
- function assetsOf(token) {
5115
- return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
5542
+ return decodeView(deps, tokenId, bytes);
5116
5543
  }
5117
- async function runBurnUnderJournal(deps, input) {
5118
- const { burnId } = input;
5119
- const { tokenId, reasonBytes } = input.request;
5120
- if (!HEX322.test(tokenId) || reasonBytes.length === 0) {
5121
- return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty" };
5544
+ async function readNfts(deps, tokenIds) {
5545
+ const held = [...new Set(tokenIds)].filter((tokenId) => deps.view.stateHashOf(tokenId) !== void 0);
5546
+ const views = /* @__PURE__ */ new Map();
5547
+ const misses = [];
5548
+ for (const tokenId of held) {
5549
+ const cached = deps.cache.get(tokenId);
5550
+ if (cached === void 0) misses.push(tokenId);
5551
+ else views.set(tokenId, cached);
5122
5552
  }
5123
- let token;
5124
- try {
5125
- token = await heldToken2(deps, tokenId);
5126
- if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
5127
- throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
5128
- }
5129
- } catch (err) {
5130
- return { success: false, burnId, tokenId, error: messageOf(err) };
5553
+ const blobs = misses.length > 0 ? await deps.storagePort.getBlobs(misses) : /* @__PURE__ */ new Map();
5554
+ for (const tokenId of misses) {
5555
+ const bytes = blobs.get(tokenId);
5556
+ if (bytes !== void 0) views.set(tokenId, await decodeView(deps, tokenId, bytes).catch(() => null));
5131
5557
  }
5132
- const entry = {
5133
- burnId,
5134
- tokenId,
5135
- reasonHex: bytesToHex(reasonBytes),
5136
- burnedTokenHex: null,
5137
- settled: false,
5138
- assets: assetsOf(token),
5139
- createdAt: deps.now()
5140
- };
5141
- await deps.burnJournal.upsert(entry);
5142
- try {
5143
- const settled = await burnJournaled(deps, entry, token, { applied: false });
5144
- return { success: true, burnId, tokenId, burnedToken: hexToBytes(settled.burnedTokenHex ?? "") };
5145
- } catch (err) {
5146
- deps.armHeartbeat();
5147
- return { success: false, burnId, tokenId, error: messageOf(err) };
5558
+ const out = /* @__PURE__ */ new Map();
5559
+ for (const tokenId of held) {
5560
+ const view = views.get(tokenId);
5561
+ if (view) out.set(tokenId, view);
5148
5562
  }
5563
+ return out;
5149
5564
  }
5150
- async function replayBurns(deps) {
5151
- let progressed = 0;
5152
- for (const entry of await deps.burnJournal.list()) {
5153
- if (entry.settled || deps.isActiveOp(entry.burnId)) continue;
5154
- try {
5155
- const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
5156
- const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
5157
- await burnJournaled(deps, entry, token, { applied });
5158
- progressed += 1;
5159
- } catch {
5160
- continue;
5565
+ async function decodeView(deps, tokenId, bytes) {
5566
+ let view;
5567
+ try {
5568
+ const token = await deps.engine.decodeToken({ tokenId, token: bytes });
5569
+ if (token.blob.tokenId !== tokenId) {
5570
+ throw new SphereError(`the blob served for ${tokenId} decodes as ${token.blob.tokenId}`, "STORAGE_ERROR");
5161
5571
  }
5572
+ const reading = await deps.engine.readNft(token);
5573
+ view = reading === null ? null : { tokenId, ...reading };
5574
+ } catch (err) {
5575
+ logger.warn("PaymentsV2", `NFT read: token ${tokenId} did not decode \u2014 ${String(err)}`);
5576
+ throw err;
5162
5577
  }
5163
- return progressed;
5578
+ deps.cache.set(tokenId, view);
5579
+ return view;
5164
5580
  }
5165
- async function pendingBurns(deps) {
5166
- return (await deps.burnJournal.list()).map((entry) => ({
5167
- burnId: entry.burnId,
5168
- tokenId: entry.tokenId,
5169
- reasonBytes: hexToBytes(entry.reasonHex),
5170
- burnedToken: entry.burnedTokenHex === null ? null : hexToBytes(entry.burnedTokenHex),
5171
- settled: entry.settled,
5172
- createdAt: entry.createdAt
5173
- }));
5581
+
5582
+ // modules/payments-v2/inventory/token-data.ts
5583
+ async function readTokenData(deps, tokenId) {
5584
+ return deps.engine.readTokenData(await heldToken2(deps, tokenId));
5174
5585
  }
5175
- async function acknowledgeBurn(deps, burnId) {
5176
- const entry = await deps.burnJournal.getByKey(burnId);
5177
- if (entry === void 0) return;
5178
- if (!entry.settled) {
5179
- throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
5180
- }
5181
- await deps.burnJournal.removeByKey(burnId);
5586
+ async function readTokenJustification(deps, tokenId) {
5587
+ return deps.engine.readTokenJustification(await heldToken2(deps, tokenId));
5182
5588
  }
5183
- async function burnJournaled(deps, entry, token, state) {
5184
- let current = entry;
5185
- if (current.burnedTokenHex === null) {
5186
- if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
5187
- const burned = await deps.engine.burn(
5188
- { token, reasonBytes: hexToBytes(current.reasonHex) },
5189
- { transferId: current.burnId }
5190
- );
5191
- if (burned.blob.tokenId !== current.tokenId) {
5192
- throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
5193
- }
5194
- current = { ...current, burnedTokenHex: bytesToHex(burned.blob.token) };
5195
- await deps.burnJournal.upsert(current);
5589
+ async function heldToken2(deps, tokenId) {
5590
+ if (deps.view.stateHashOf(tokenId) === void 0) {
5591
+ throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
5196
5592
  }
5197
- if (!state.applied) {
5198
- await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [] });
5593
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
5594
+ if (bytes === void 0) {
5595
+ throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
5199
5596
  }
5200
- await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
5201
- current = { ...current, settled: true };
5202
- await deps.burnJournal.upsert(current);
5203
- deps.refreshView();
5204
- return current;
5597
+ return deps.engine.decodeToken({ tokenId, token: bytes });
5205
5598
  }
5206
5599
 
5207
5600
  // modules/payments-v2/send-whole.ts
@@ -5399,6 +5792,7 @@ var PaymentsFacade = class {
5399
5792
  track: (op) => this.trackTail(op)
5400
5793
  });
5401
5794
  this.view = parts.view;
5795
+ this.verdicts = parts.verdicts;
5402
5796
  this.wakeRefresh = parts.refreshView;
5403
5797
  this.ledger = parts.ledger;
5404
5798
  this.queue = parts.queue;
@@ -5412,6 +5806,7 @@ var PaymentsFacade = class {
5412
5806
  this.restoreDeps = parts.restoreDeps;
5413
5807
  this.requests = parts.requests;
5414
5808
  this.pins = parts.pins;
5809
+ this.burnHold = parts.burnHold;
5415
5810
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
5416
5811
  this.heartbeat = new ConvergenceHeartbeat({
5417
5812
  now: () => this.nowMs(),
@@ -5435,6 +5830,7 @@ var PaymentsFacade = class {
5435
5830
  wakeRefresh;
5436
5831
  prewarmed = new PrewarmCache();
5437
5832
  view;
5833
+ verdicts;
5438
5834
  ledger;
5439
5835
  queue;
5440
5836
  machine;
@@ -5461,6 +5857,7 @@ var PaymentsFacade = class {
5461
5857
  activeMoneyOps = /* @__PURE__ */ new Set();
5462
5858
  /** #737: the ledger holds exactly the sources of the still-open intents. */
5463
5859
  pins;
5860
+ burnHold;
5464
5861
  /** Per facade, so per address: an NFT reading is never served to another wallet. */
5465
5862
  nftCache = new NftCache();
5466
5863
  // ── lifecycle ──────────────────────────────────────────────────────────────
@@ -5485,7 +5882,8 @@ var PaymentsFacade = class {
5485
5882
  await this.deps.session.start();
5486
5883
  await this.pins.sync().catch(() => void 0);
5487
5884
  this.trackTail(this.runConvergencePass());
5488
- this.trackTail(this.seedHeldStates());
5885
+ this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
5886
+ await this.verdicts.start().catch(() => void 0);
5489
5887
  await this.view.fullPull().catch(() => void 0);
5490
5888
  this.receiveLoop.start(this.deps.receivePollMs);
5491
5889
  }
@@ -5494,6 +5892,7 @@ var PaymentsFacade = class {
5494
5892
  this.started = false;
5495
5893
  this.heartbeat.stop();
5496
5894
  this.receiveLoop.stop();
5895
+ this.verdicts.stop();
5497
5896
  for (const unsubscribe of this.unsubscribers) unsubscribe();
5498
5897
  this.unsubscribers = [];
5499
5898
  await this.deps.session.stop();
@@ -5510,12 +5909,17 @@ var PaymentsFacade = class {
5510
5909
  }
5511
5910
  // ── reads ──────────────────────────────────────────────────────────────────
5512
5911
  async assets(coinId) {
5912
+ return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
5913
+ }
5914
+ unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
5915
+ async heldAssets(coinId) {
5513
5916
  const all = await this.view.assets(this.deps.registry, this.deps.price);
5514
5917
  return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
5515
5918
  }
5516
5919
  tokens(filter) {
5517
- return this.view.tokens(this.deps.registry, filter);
5920
+ return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
5518
5921
  }
5922
+ unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
5519
5923
  coinless() {
5520
5924
  return this.view.coinless(this.deps.registry);
5521
5925
  }
@@ -5576,9 +5980,9 @@ var PaymentsFacade = class {
5576
5980
  return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
5577
5981
  }
5578
5982
  mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
5579
- burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
5580
- pendingBurns = () => this.track(pendingBurns(this.mintDeps()));
5581
- acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.mintDeps(), burnId));
5983
+ burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
5984
+ pendingBurns = () => this.track(pendingBurns(this.machineStores));
5985
+ acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
5582
5986
  // §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
5583
5987
  async resumeNow() {
5584
5988
  if (!this.started) return;
@@ -5621,7 +6025,8 @@ var PaymentsFacade = class {
5621
6025
  isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
5622
6026
  tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
5623
6027
  emit: this.deps.emit,
5624
- now: () => this.nowMs()
6028
+ now: () => this.nowMs(),
6029
+ accepted: (token) => this.verdicts.accept(token)
5625
6030
  };
5626
6031
  }
5627
6032
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
@@ -5959,16 +6364,6 @@ var PaymentsFacade = class {
5959
6364
  newId() {
5960
6365
  return (this.deps.newId ?? randomUUID)();
5961
6366
  }
5962
- async seedHeldStates() {
5963
- let page = await this.deps.storagePort.listInventory();
5964
- for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
5965
- for (const item of page.items) {
5966
- if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
5967
- }
5968
- if (!page.more) return;
5969
- page = await this.deps.storagePort.listInventory(page.cursor);
5970
- }
5971
- }
5972
6367
  track(op) {
5973
6368
  const settled = op.then(() => void 0, () => void 0);
5974
6369
  this.pendingOps.add(settled);
@@ -5983,6 +6378,7 @@ export {
5983
6378
  ATTENTION_MINT_UNRESOLVED,
5984
6379
  ATTENTION_RECIPIENT_NETWORK_UNVERIFIED,
5985
6380
  ATTENTION_RESEED_REJECTED,
6381
+ CUSTOM_MINT_MAX_PAYLOAD_BYTES,
5986
6382
  HEARTBEAT_CAP_MS,
5987
6383
  HEARTBEAT_SEED_MS,
5988
6384
  MAX_RESELECT,
@@ -5996,6 +6392,7 @@ export {
5996
6392
  STORE_KEYS,
5997
6393
  createScopedKV,
5998
6394
  encodeNftContent,
6395
+ isLiveBurn,
5999
6396
  isNftDocumentLink,
6000
6397
  isRetryableAckError,
6001
6398
  parseNftDocument,