@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
@@ -9511,6 +9511,150 @@ async function deriveDirectAddress(publicKey) {
9511
9511
  return `DIRECT://${HexConverter.encode(imprint)}${HexConverter.encode(checksum)}`;
9512
9512
  }
9513
9513
 
9514
+ // token-engine/claims.ts
9515
+ var CoinClaims = class _CoinClaims {
9516
+ issuers = /* @__PURE__ */ new Map();
9517
+ policies = [];
9518
+ static fromPlugins(plugins) {
9519
+ const claims = new _CoinClaims();
9520
+ for (const plugin of plugins ?? []) {
9521
+ for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
9522
+ }
9523
+ return claims;
9524
+ }
9525
+ add(policy) {
9526
+ const tokenType = HexConverter.encode(policy.tokenType.bytes);
9527
+ const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
9528
+ const taken = coinIds.find((coinId) => this.issuers.has(coinId));
9529
+ if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
9530
+ for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
9531
+ this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
9532
+ }
9533
+ fingerprint() {
9534
+ return [...this.policies].sort().join("\n");
9535
+ }
9536
+ issuerOf(coinId) {
9537
+ return this.issuers.get(coinId.toLowerCase()) ?? null;
9538
+ }
9539
+ };
9540
+
9541
+ // token-engine/errors.ts
9542
+ var TransferConflictError = class extends SphereError {
9543
+ /**
9544
+ * #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
9545
+ * Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
9546
+ */
9547
+ conflictedSourceId;
9548
+ constructor(message, cause) {
9549
+ super(message, "TRANSFER_CONFLICT", cause);
9550
+ this.name = "TransferConflictError";
9551
+ }
9552
+ };
9553
+ var MintReasonUnverifiableError = class extends SphereError {
9554
+ constructor(message, cause) {
9555
+ super(message, "MINT_REASON_UNVERIFIABLE", cause);
9556
+ this.name = "MintReasonUnverifiableError";
9557
+ }
9558
+ };
9559
+ var ProofUnconfirmedError = class extends SphereError {
9560
+ mayHaveCertified = true;
9561
+ constructor(message, cause) {
9562
+ super(message, "CERTIFICATION_UNCONFIRMED", cause);
9563
+ this.name = "ProofUnconfirmedError";
9564
+ }
9565
+ };
9566
+ var CheckpointPersistFailedError = class extends SphereError {
9567
+ mayHaveCertified = true;
9568
+ constructor(message, cause) {
9569
+ super(message, "CHECKPOINT_PERSIST_FAILED", cause);
9570
+ this.name = "CheckpointPersistFailedError";
9571
+ }
9572
+ };
9573
+ var SplitCheckpointLostError = class extends SphereError {
9574
+ constructor(message, cause) {
9575
+ super(message, "SPLIT_CHECKPOINT_LOST", cause);
9576
+ this.name = "SplitCheckpointLostError";
9577
+ }
9578
+ };
9579
+ var CheckpointTrustbaseMismatchError = class extends SphereError {
9580
+ constructor(message, cause) {
9581
+ super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
9582
+ this.name = "CheckpointTrustbaseMismatchError";
9583
+ }
9584
+ };
9585
+ function isKeepOpenSplitError(err) {
9586
+ return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
9587
+ }
9588
+
9589
+ // token-engine/mint-reasons.ts
9590
+ var RULE = "MintJustificationVerification";
9591
+ var MintReasonRegistry = class _MintReasonRegistry extends MintJustificationVerifierService {
9592
+ byTag = /* @__PURE__ */ new Map();
9593
+ pluginTags = /* @__PURE__ */ new Set();
9594
+ register(verifier) {
9595
+ super.register(verifier);
9596
+ this.byTag.set(verifier.tag, verifier);
9597
+ return this;
9598
+ }
9599
+ registerPlugin(verifier) {
9600
+ this.register(verifier);
9601
+ this.pluginTags.add(verifier.tag);
9602
+ return this;
9603
+ }
9604
+ async verify(transaction, nestedTokenCollector) {
9605
+ const tag = transaction.justification ? reasonTag(transaction.justification) : null;
9606
+ if (tag === null) return super.verify(transaction, nestedTokenCollector);
9607
+ const verifier = this.byTag.get(tag);
9608
+ if (verifier === void 0) throw new MintReasonUnverifiableError(`No verifier is registered for mint-reason tag ${tag}`);
9609
+ if (!this.pluginTags.has(tag)) return super.verify(transaction, nestedTokenCollector);
9610
+ return verifyPlugin(verifier, transaction, nestedTokenCollector);
9611
+ }
9612
+ knows(tag) {
9613
+ return this.byTag.has(tag);
9614
+ }
9615
+ /** `verifiers` replace the registered ones of their tags; a tag this registry lacks is refused. */
9616
+ overlaid(verifiers) {
9617
+ for (const verifier of verifiers) assertKnown(this, verifier.tag);
9618
+ const replaced = new Map(verifiers.map((verifier) => [verifier.tag, verifier]));
9619
+ const overlay = new _MintReasonRegistry();
9620
+ for (const [tag, verifier] of this.byTag) {
9621
+ const perCall = replaced.get(tag);
9622
+ if (perCall !== void 0 || this.pluginTags.has(tag)) overlay.registerPlugin(perCall ?? verifier);
9623
+ else overlay.register(verifier);
9624
+ }
9625
+ return overlay;
9626
+ }
9627
+ assertVerifiable(justification) {
9628
+ const tag = reasonTag(justification);
9629
+ if (tag === null) throw new SphereError("The mint reason is not tagged CBOR", "VALIDATION_ERROR");
9630
+ assertKnown(this, tag);
9631
+ }
9632
+ };
9633
+ function reasonTag(justification) {
9634
+ try {
9635
+ return BigInt(CborDeserializer.decodeTag(justification).tag);
9636
+ } catch {
9637
+ return null;
9638
+ }
9639
+ }
9640
+ async function verifyPlugin(verifier, transaction, nestedTokenCollector) {
9641
+ let result;
9642
+ try {
9643
+ result = await verifier.verify(transaction, nestedTokenCollector);
9644
+ } catch (err) {
9645
+ const why = err instanceof Error ? err.message : String(err);
9646
+ throw new MintReasonUnverifiableError(`Mint-reason tag ${verifier.tag} cannot be verified yet: ${why}`, err);
9647
+ }
9648
+ return result.status === VerificationStatus.OK ? new VerificationResult(RULE, VerificationStatus.OK, "", [result]) : new VerificationResult(RULE, VerificationStatus.FAIL, `Verification failed for tag ${verifier.tag}.`, [result]);
9649
+ }
9650
+ function assertKnown(registry, tag) {
9651
+ if (registry.knows(tag)) return;
9652
+ throw new SphereError(
9653
+ `No verifier is registered on the engine for mint-reason tag ${tag}, so a replay of this mint could never verify`,
9654
+ "VALIDATION_ERROR"
9655
+ );
9656
+ }
9657
+
9514
9658
  // token-engine/SpherePaymentData.ts
9515
9659
  var COIN_ID_PATTERN = /^([0-9a-f]{2})+$/;
9516
9660
  function assertAsset(coinId, amount) {
@@ -9687,45 +9831,6 @@ async function awaitProofBounded(deps, transaction, signal) {
9687
9831
  return value;
9688
9832
  }
9689
9833
 
9690
- // token-engine/errors.ts
9691
- var TransferConflictError = class extends SphereError {
9692
- /**
9693
- * #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
9694
- * Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
9695
- */
9696
- conflictedSourceId;
9697
- constructor(message, cause) {
9698
- super(message, "TRANSFER_CONFLICT", cause);
9699
- this.name = "TransferConflictError";
9700
- }
9701
- };
9702
- var ProofUnconfirmedError = class extends SphereError {
9703
- mayHaveCertified = true;
9704
- constructor(message, cause) {
9705
- super(message, "CERTIFICATION_UNCONFIRMED", cause);
9706
- this.name = "ProofUnconfirmedError";
9707
- }
9708
- };
9709
- var CheckpointPersistFailedError = class extends SphereError {
9710
- mayHaveCertified = true;
9711
- constructor(message, cause) {
9712
- super(message, "CHECKPOINT_PERSIST_FAILED", cause);
9713
- this.name = "CheckpointPersistFailedError";
9714
- }
9715
- };
9716
- var SplitCheckpointLostError = class extends SphereError {
9717
- constructor(message, cause) {
9718
- super(message, "SPLIT_CHECKPOINT_LOST", cause);
9719
- this.name = "SplitCheckpointLostError";
9720
- }
9721
- };
9722
- var CheckpointTrustbaseMismatchError = class extends SphereError {
9723
- constructor(message, cause) {
9724
- super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
9725
- this.name = "CheckpointTrustbaseMismatchError";
9726
- }
9727
- };
9728
-
9729
9834
  // token-engine/certification-outcome.ts
9730
9835
  function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
9731
9836
  if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
@@ -10288,15 +10393,17 @@ async function readTokenNft(token) {
10288
10393
  }
10289
10394
 
10290
10395
  // token-engine/plugin-ops.ts
10291
- function contextWithMintVerifiers(base, verifiers) {
10292
- const registry = new MintJustificationVerifierService();
10293
- for (const verifier of verifiers) registry.register(verifier);
10396
+ function mintContext(deps, params) {
10397
+ assertMintableData(params.data);
10398
+ if (params.justification !== void 0) deps.mintJustificationVerifier.assertVerifiable(params.justification);
10399
+ const verifiers = params.mintJustificationVerifiers;
10400
+ if (verifiers === void 0) return deps.verificationContext;
10294
10401
  return new VerificationContext(
10295
- base.trustBase,
10296
- base.predicateVerifier,
10297
- base.unicityCertificateVerifier,
10298
- registry,
10299
- new TokenIssuanceVerifierService(false)
10402
+ deps.trustBase,
10403
+ deps.predicateVerifier,
10404
+ deps.unicityCertificateVerifier,
10405
+ deps.mintJustificationVerifier.overlaid(verifiers),
10406
+ deps.verificationContext.tokenIssuanceVerifier
10300
10407
  );
10301
10408
  }
10302
10409
 
@@ -10384,9 +10491,6 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
10384
10491
  // token-engine/SphereTokenEngine.ts
10385
10492
  var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
10386
10493
  var MAX_MINT_CONCURRENCY = 8;
10387
- function isKeepOpenSplitError(err) {
10388
- return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
10389
- }
10390
10494
  var TRANSFER_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
10391
10495
  var CLEAN_REJECT_STATUSES = /* @__PURE__ */ new Set([
10392
10496
  CertificationStatus.STATE_ID_MISMATCH,
@@ -10476,8 +10580,9 @@ var SphereTokenEngine = class {
10476
10580
  const token = await Token.mint(certified, this.deps.verificationContext);
10477
10581
  return wrapToken(token);
10478
10582
  }
10583
+ assertMintable = (params) => void mintContext(this.deps, params);
10479
10584
  async mintDataToken(params, options) {
10480
- assertMintableData(params.data);
10585
+ const context = mintContext(this.deps, params);
10481
10586
  const recipient = SignaturePredicate.create(params.recipientPubkey);
10482
10587
  const tokenType = params.tokenType ? new TokenType(params.tokenType) : TokenType.generate();
10483
10588
  const salt = params.salt ? TokenSalt.fromBytes(params.salt) : TokenSalt.generate();
@@ -10491,8 +10596,7 @@ var SphereTokenEngine = class {
10491
10596
  options
10492
10597
  );
10493
10598
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10494
- const verifiers = params.mintJustificationVerifiers;
10495
- return wrapToken(await Token.mint(certified, verifiers ? contextWithMintVerifiers(this.deps, verifiers) : this.deps.verificationContext));
10599
+ return wrapToken(await Token.mint(certified, context));
10496
10600
  }
10497
10601
  buildNftMint(params) {
10498
10602
  return planNftMint(this.deps, params);
@@ -10829,20 +10933,30 @@ var SphereTokenEngine = class {
10829
10933
 
10830
10934
  // token-engine/factory.ts
10831
10935
  var DEFAULT_VERIFICATION_POOL_SIZE = 4;
10832
- function registerPluginVerifiers(registry, plugins) {
10936
+ function registerPlugins(registries, plugins) {
10833
10937
  for (const plugin of plugins ?? []) {
10834
10938
  for (const verifier of plugin.mintJustificationVerifiers ?? []) {
10835
- try {
10836
- registry.register(verifier);
10837
- } catch (err) {
10838
- throw new SphereError(
10839
- `Token plugin '${plugin.id}' registers mint-reason tag ${verifier.tag} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
10840
- "INVALID_CONFIG"
10841
- );
10842
- }
10939
+ claim(plugin, `mint-reason tag ${verifier.tag}`, () => registries.reasons.registerPlugin(verifier));
10940
+ }
10941
+ for (const policy of plugin.tokenIssuancePolicies ?? []) {
10942
+ const tokenType = HexConverter.encode(policy.tokenType.bytes);
10943
+ claim(plugin, `an issuance policy for token type ${tokenType}`, () => {
10944
+ registries.claims.add(policy);
10945
+ registries.policies.register(policy);
10946
+ });
10843
10947
  }
10844
10948
  }
10845
10949
  }
10950
+ function claim(plugin, what, register) {
10951
+ try {
10952
+ register();
10953
+ } catch (err) {
10954
+ throw new SphereError(
10955
+ `Token plugin '${plugin.id}' registers ${what} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
10956
+ "INVALID_CONFIG"
10957
+ );
10958
+ }
10959
+ }
10846
10960
  function disposedError() {
10847
10961
  return new SphereError(
10848
10962
  "Verification worker pool disposed \u2014 the in-flight verification was cancelled",
@@ -10920,11 +11034,15 @@ async function createSphereTokenEngine(config) {
10920
11034
  const unicityCertificateVerifier = new UnicityCertificateVerifier(
10921
11035
  new UnicitySealQuorumSignaturesVerificationRule(new Secp256k1SignatureVerifier(), new VerifiedSealCache(256))
10922
11036
  );
10923
- const mintJustificationVerifier = new MintJustificationVerifierService();
11037
+ const mintJustificationVerifier = new MintReasonRegistry();
10924
11038
  mintJustificationVerifier.register(
10925
11039
  new SplitMintJustificationVerifier(decodeSpherePaymentData)
10926
11040
  );
10927
- registerPluginVerifiers(mintJustificationVerifier, config.plugins);
11041
+ const tokenIssuanceVerifier = new TokenIssuanceVerifierService(false);
11042
+ registerPlugins(
11043
+ { reasons: mintJustificationVerifier, policies: tokenIssuanceVerifier, claims: new CoinClaims() },
11044
+ config.plugins
11045
+ );
10928
11046
  const deps = {
10929
11047
  client: new StateTransitionClient(new AggregatorClient(config.aggregatorUrl, config.apiKey ?? null)),
10930
11048
  trustBase,
@@ -10936,7 +11054,7 @@ async function createSphereTokenEngine(config) {
10936
11054
  predicateVerifier,
10937
11055
  unicityCertificateVerifier,
10938
11056
  mintJustificationVerifier,
10939
- new TokenIssuanceVerifierService(false)
11057
+ tokenIssuanceVerifier
10940
11058
  ),
10941
11059
  signingService: new SigningService(config.privateKey),
10942
11060
  // Also the HKDF ikm for deterministic realization (Part E.1) — the
@@ -12036,6 +12154,9 @@ function createScopedKV(storage, network, chainPubkey) {
12036
12154
  }
12037
12155
  };
12038
12156
  }
12157
+ function isLiveBurn(entry) {
12158
+ return !entry.settled && entry.failure === void 0;
12159
+ }
12039
12160
  var STORE_KEYS = {
12040
12161
  intentBackstop: "intents",
12041
12162
  checkpointCache: "checkpoints",
@@ -12050,7 +12171,8 @@ var STORE_KEYS = {
12050
12171
  epochLatch: "epoch-latch",
12051
12172
  // §5.2 InventoryView durable overlays (#625/#679).
12052
12173
  suspectedSpent: "suspected-spent",
12053
- knownSpends: "known-spends"
12174
+ knownSpends: "known-spends",
12175
+ verifiedTokens: "verified-tokens"
12054
12176
  };
12055
12177
 
12056
12178
  // modules/payments-v2/machine/journal.ts
@@ -12567,6 +12689,7 @@ var TransferMachine = class {
12567
12689
  const key = keys.get(digest);
12568
12690
  if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
12569
12691
  const { tokenId } = await engine.deliveryKeys(o.changeBlob);
12692
+ await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
12570
12693
  added.push({ tokenId, key });
12571
12694
  }
12572
12695
  return added;
@@ -12962,7 +13085,7 @@ var Converger = class {
12962
13085
  this.deps.stores.burnJournal.list()
12963
13086
  ]);
12964
13087
  const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
12965
- const burnsOpen = burns.some((e) => !e.settled);
13088
+ const burnsOpen = burns.some(isLiveBurn);
12966
13089
  return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
12967
13090
  }
12968
13091
  async resumePass() {
@@ -13049,6 +13172,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
13049
13172
  const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
13050
13173
  holds.set(entry.transferId, [...direct, ...split]);
13051
13174
  }
13175
+ for (const burn of await stores.burnJournal.list()) {
13176
+ if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
13177
+ }
13052
13178
  return { open: holds, complete };
13053
13179
  }
13054
13180
  function groupJournal(journal) {
@@ -13359,18 +13485,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
13359
13485
  createdAt: snapshot.createdAt,
13360
13486
  updatedAt: snapshot.updatedAt,
13361
13487
  lazy: true,
13362
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
13488
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {},
13489
+ ...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
13363
13490
  };
13364
13491
  }
13365
- function aggregateAssets(entries, isTransferring, registry) {
13492
+ function aggregateAssets(entries, reading, registry) {
13366
13493
  const groups = /* @__PURE__ */ new Map();
13367
13494
  for (const [tokenId, entry] of entries) {
13368
- const moving = isTransferring(tokenId);
13495
+ const moving = reading.isTransferring(tokenId);
13369
13496
  for (const asset of entry.assets) {
13370
- let group = groups.get(asset.coinId);
13497
+ const standing = reading.standing(tokenId, asset.coinId);
13498
+ const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
13499
+ let group = groups.get(key);
13371
13500
  if (!group) {
13372
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
13373
- groups.set(asset.coinId, group);
13501
+ group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
13502
+ groups.set(key, group);
13374
13503
  }
13375
13504
  if (moving) {
13376
13505
  group.transferring += BigInt(asset.amount);
@@ -13381,9 +13510,10 @@ function aggregateAssets(entries, isTransferring, registry) {
13381
13510
  }
13382
13511
  }
13383
13512
  }
13384
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
13513
+ return [...groups.values()].map((totals) => toAsset(totals, registry));
13385
13514
  }
13386
- function toAsset(coinId, totals, registry) {
13515
+ function toAsset(totals, registry) {
13516
+ const coinId = totals.coinId;
13387
13517
  const iconUrl = registry.getIconUrl(coinId);
13388
13518
  return {
13389
13519
  coinId,
@@ -13403,7 +13533,8 @@ function toAsset(coinId, totals, registry) {
13403
13533
  priceEur: null,
13404
13534
  change24h: null,
13405
13535
  fiatValueUsd: null,
13406
- fiatValueEur: null
13536
+ fiatValueEur: null,
13537
+ ...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
13407
13538
  };
13408
13539
  }
13409
13540
  function priceIdOf(registry, coinId) {
@@ -13411,9 +13542,9 @@ function priceIdOf(registry, coinId) {
13411
13542
  }
13412
13543
  async function withPrices(raw, registry, price) {
13413
13544
  try {
13414
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
13545
+ const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
13415
13546
  const quotes = await price.getPrices(ids);
13416
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13547
+ return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13417
13548
  } catch {
13418
13549
  return raw;
13419
13550
  }
@@ -13434,11 +13565,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
13434
13565
  const snapshot = { assets: [], createdAt: now, updatedAt: now };
13435
13566
  return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
13436
13567
  transferring: true,
13437
- suspectedSpent: false
13568
+ suspectedSpent: false,
13569
+ standing: "trusted"
13438
13570
  });
13439
13571
  }
13440
13572
 
13441
13573
  // modules/payments-v2/inventory/InventoryView.ts
13574
+ var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
13442
13575
  var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
13443
13576
  var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
13444
13577
  var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
@@ -13532,6 +13665,12 @@ var InventoryView = class {
13532
13665
  for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
13533
13666
  if (changed) this.deps.emit("inventory:updated");
13534
13667
  }
13668
+ get trust() {
13669
+ return this.deps.trust ?? TRUST_ALL;
13670
+ }
13671
+ trusts(tokenId, coinId) {
13672
+ return this.trust.standing(tokenId, coinId) === "trusted";
13673
+ }
13535
13674
  pinned(tokenId) {
13536
13675
  return this.deps.isPinned?.(tokenId) ?? false;
13537
13676
  }
@@ -13558,6 +13697,7 @@ var InventoryView = class {
13558
13697
  isSpendable(tokenId, entry) {
13559
13698
  if (entry.status !== "active") return false;
13560
13699
  if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13700
+ if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
13561
13701
  return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13562
13702
  }
13563
13703
  /** Is this NAMED token spendable whole — coinless or valued (#777)? */
@@ -13584,7 +13724,8 @@ var InventoryView = class {
13584
13724
  out.push(
13585
13725
  toToken(tokenId, entry, asset, registry, {
13586
13726
  transferring: this.held(tokenId),
13587
- suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
13727
+ suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
13728
+ standing: this.trust.standing(tokenId, asset.coinId)
13588
13729
  })
13589
13730
  );
13590
13731
  }
@@ -13614,10 +13755,21 @@ var InventoryView = class {
13614
13755
  return out;
13615
13756
  }
13616
13757
  async assets(registry, price) {
13617
- const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
13758
+ const reading = {
13759
+ isTransferring: (tokenId) => this.held(tokenId),
13760
+ standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
13761
+ };
13762
+ const raw = aggregateAssets(this.activeEntries(), reading, registry);
13618
13763
  if (!price || raw.length === 0) return raw;
13619
13764
  return withPrices(raw, registry, price);
13620
13765
  }
13766
+ claimHolders() {
13767
+ const out = [];
13768
+ for (const [tokenId, entry] of this.activeEntries()) {
13769
+ if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
13770
+ }
13771
+ return out;
13772
+ }
13621
13773
  *activeEntries() {
13622
13774
  for (const [tokenId, entry] of this.mirror) {
13623
13775
  if (entry.status === "active") yield [tokenId, entry];
@@ -13787,6 +13939,171 @@ var InventoryView = class {
13787
13939
  }
13788
13940
  };
13789
13941
 
13942
+ // modules/payments-v2/inventory/verdicts.ts
13943
+ var VERDICT_RETRY_MS = 3e4;
13944
+ var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
13945
+ function honoursClaims(claims, token) {
13946
+ const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
13947
+ return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
13948
+ }
13949
+ var TokenVerdicts = class {
13950
+ constructor(deps) {
13951
+ this.deps = deps;
13952
+ }
13953
+ verified = /* @__PURE__ */ new Set();
13954
+ refused = /* @__PURE__ */ new Set();
13955
+ retries = /* @__PURE__ */ new Map();
13956
+ writes = new SerialChain();
13957
+ hydration = null;
13958
+ queued = null;
13959
+ running = null;
13960
+ held = [];
13961
+ timer = null;
13962
+ live = false;
13963
+ isClaimed(coinId) {
13964
+ return this.deps.claims.issuerOf(coinId) !== null;
13965
+ }
13966
+ standing(tokenId, coinId) {
13967
+ if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
13968
+ return this.refused.has(tokenId) ? "refused" : "pending";
13969
+ }
13970
+ trusts(tokenId, coinId) {
13971
+ return this.standing(tokenId, coinId) === "trusted";
13972
+ }
13973
+ hydrate() {
13974
+ this.hydration ??= this.load().catch((error) => {
13975
+ this.hydration = null;
13976
+ throw error;
13977
+ });
13978
+ return this.hydration;
13979
+ }
13980
+ start() {
13981
+ this.live = true;
13982
+ return this.hydrate();
13983
+ }
13984
+ stop() {
13985
+ this.live = false;
13986
+ if (this.timer !== null) clearTimeout(this.timer);
13987
+ this.timer = null;
13988
+ }
13989
+ async accept(token) {
13990
+ if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
13991
+ }
13992
+ async recordSplit(sourceTokenId, outputTokenId) {
13993
+ await this.hydrate();
13994
+ if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
13995
+ }
13996
+ review(holders) {
13997
+ this.queued = holders;
13998
+ this.running ??= this.drain();
13999
+ return this.running;
14000
+ }
14001
+ async drain() {
14002
+ try {
14003
+ while (this.queued !== null) {
14004
+ const holders = this.queued;
14005
+ this.queued = null;
14006
+ await this.reviewOnce(holders);
14007
+ }
14008
+ } finally {
14009
+ this.running = null;
14010
+ }
14011
+ }
14012
+ async reviewOnce(holders) {
14013
+ await this.hydrate();
14014
+ this.held = holders;
14015
+ const due = holders.filter((tokenId) => this.isDue(tokenId));
14016
+ const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
14017
+ for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
14018
+ if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
14019
+ await this.persist();
14020
+ this.deps.changed();
14021
+ }
14022
+ this.scheduleRetry();
14023
+ }
14024
+ isDue(tokenId) {
14025
+ if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
14026
+ return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
14027
+ }
14028
+ async check(due) {
14029
+ const verdicts = /* @__PURE__ */ new Map();
14030
+ let blobs;
14031
+ try {
14032
+ blobs = await this.deps.getBlobs([...due]);
14033
+ } catch {
14034
+ return verdicts;
14035
+ }
14036
+ for (const tokenId of due) {
14037
+ const bytes = blobs.get(tokenId);
14038
+ if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
14039
+ }
14040
+ return verdicts;
14041
+ }
14042
+ async verdictOf(tokenId, bytes) {
14043
+ let engine;
14044
+ try {
14045
+ engine = this.deps.engine();
14046
+ } catch {
14047
+ return "retry";
14048
+ }
14049
+ let token;
14050
+ try {
14051
+ token = await engine.decodeToken({ tokenId, token: bytes });
14052
+ } catch {
14053
+ return "refused";
14054
+ }
14055
+ if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
14056
+ try {
14057
+ return (await engine.verify(token)).ok ? "verified" : "refused";
14058
+ } catch {
14059
+ return "retry";
14060
+ }
14061
+ }
14062
+ settle(tokenId, verdict) {
14063
+ if (verdict === "retry") {
14064
+ this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
14065
+ return;
14066
+ }
14067
+ this.retries.delete(tokenId);
14068
+ (verdict === "verified" ? this.verified : this.refused).add(tokenId);
14069
+ }
14070
+ nextRetry(previous) {
14071
+ const attempts = (previous?.attempts ?? 0) + 1;
14072
+ const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
14073
+ return { attempts, atMs: this.now() + delay };
14074
+ }
14075
+ scheduleRetry() {
14076
+ if (!this.live || this.timer !== null) return;
14077
+ const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
14078
+ if (due.length === 0) return;
14079
+ const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
14080
+ this.timer = setTimeout(() => {
14081
+ this.timer = null;
14082
+ void this.review(this.held).catch(() => void 0);
14083
+ }, delay);
14084
+ }
14085
+ async remember(tokenId) {
14086
+ await this.hydrate();
14087
+ if (this.verified.has(tokenId)) return;
14088
+ this.verified.add(tokenId);
14089
+ this.refused.delete(tokenId);
14090
+ this.retries.delete(tokenId);
14091
+ await this.persist();
14092
+ }
14093
+ persist() {
14094
+ const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
14095
+ return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
14096
+ }
14097
+ async load() {
14098
+ const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
14099
+ if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
14100
+ for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
14101
+ }
14102
+ now() {
14103
+ return (this.deps.now ?? Date.now)();
14104
+ }
14105
+ };
14106
+
13790
14107
  // modules/payments-v2/ports.ts
13791
14108
  function isRetryableAckError(err) {
13792
14109
  return err !== null && typeof err === "object" && err.retryable === true;
@@ -13796,6 +14113,8 @@ function isRetryableAckError(err) {
13796
14113
  var ACK_BATCH_SIZE = 200;
13797
14114
  var REFRESH_INTERVAL_MS = 2500;
13798
14115
  var POLL_INTERVAL_MS = 3e4;
14116
+ var RECHECK_BASE_MS = POLL_INTERVAL_MS;
14117
+ var RECHECK_MAX_MS = 60 * 60 * 1e3;
13799
14118
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
13800
14119
  function receivedDedupKey(tokenId, stateHash) {
13801
14120
  return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
@@ -13812,6 +14131,7 @@ var Receive = class {
13812
14131
  pollTimer = null;
13813
14132
  unsubscribeWake = null;
13814
14133
  lastRefreshAt = 0;
14134
+ rechecks = /* @__PURE__ */ new Map();
13815
14135
  drainOnce() {
13816
14136
  return this.drainFlight.run(() => this.doDrain());
13817
14137
  }
@@ -13870,6 +14190,7 @@ var Receive = class {
13870
14190
  const pending = [];
13871
14191
  const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
13872
14192
  const claimable = { n: 0 };
14193
+ const deferred = { any: false };
13873
14194
  const finish = () => {
13874
14195
  if (claimable.n > 0) deps.refreshView?.();
13875
14196
  return stored;
@@ -13878,7 +14199,7 @@ var Receive = class {
13878
14199
  const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
13879
14200
  let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
13880
14201
  for (let pass = 0; pass < 2; pass++) {
13881
- await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
14202
+ await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
13882
14203
  const served = deps.delivery.incomingEpoch();
13883
14204
  if (basis === null || served === null || served === basis.syncEpoch) break;
13884
14205
  await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
@@ -13895,20 +14216,41 @@ var Receive = class {
13895
14216
  return finish();
13896
14217
  }
13897
14218
  async processEntry(ctx, entry) {
13898
- const { deps, engine, pending, stored, claimable } = ctx;
13899
- const screened = await screen(deps, engine, entry);
14219
+ const { deps, engine, stored, claimable } = ctx;
14220
+ const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
14221
+ this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
14222
+ if (screened.kind === "defer") {
14223
+ ctx.deferred.any = true;
14224
+ return;
14225
+ }
13900
14226
  if (screened.kind === "ack") {
13901
- pending.push(screened.ack);
14227
+ queueAck(ctx, screened.ack);
13902
14228
  if (screened.ack.disposition === "claimed") claimable.n += 1;
13903
14229
  return;
13904
14230
  }
14231
+ await deps.accepted?.(screened.token);
13905
14232
  await deps.view.store(screened.record);
13906
- pending.push(claimAck(entry));
14233
+ queueAck(ctx, claimAck(entry));
13907
14234
  claimable.n += 1;
13908
14235
  const transfer = await announce(deps, entry, screened.record);
13909
14236
  stored.push(transfer);
13910
14237
  deps.emit("transfer:incoming", transfer);
13911
14238
  }
14239
+ recheckDue(deliveryId) {
14240
+ const recheck = this.rechecks.get(deliveryId);
14241
+ return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
14242
+ }
14243
+ scheduleRecheck(deliveryId, deferred) {
14244
+ if (!deferred) {
14245
+ this.rechecks.delete(deliveryId);
14246
+ return;
14247
+ }
14248
+ const previous = this.rechecks.get(deliveryId);
14249
+ if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
14250
+ const attempts = (previous?.attempts ?? 0) + 1;
14251
+ const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
14252
+ this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
14253
+ }
13912
14254
  };
13913
14255
  async function screen(deps, engine, entry) {
13914
14256
  const blobBytes = await entry.fetchBlob();
@@ -13919,7 +14261,8 @@ async function screen(deps, engine, entry) {
13919
14261
  logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
13920
14262
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
13921
14263
  }
13922
- const verdict = await engine.verify(token);
14264
+ const verdict = await verdictOf(engine, token);
14265
+ if (verdict === null) return { kind: "defer" };
13923
14266
  if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
13924
14267
  if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
13925
14268
  return { kind: "ack", ack: rejectAck(entry, "not-owned") };
@@ -13930,38 +14273,43 @@ async function screen(deps, engine, entry) {
13930
14273
  if (held !== null && await engine.isSpent(token)) {
13931
14274
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
13932
14275
  }
14276
+ const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
13933
14277
  return {
13934
14278
  kind: "accept",
14279
+ token,
13935
14280
  record: {
13936
14281
  tokenId: keys.tokenId,
13937
14282
  stateHash: keys.stateHash,
13938
14283
  assets: toAssetAmounts(token),
13939
- ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
14284
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
14285
+ ...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
13940
14286
  }
13941
14287
  };
13942
14288
  }
13943
- async function announce(deps, entry, record) {
13944
- const receivedAt = (deps.now ?? Date.now)();
14289
+ function unverifiedCoinsOf(deps, token) {
14290
+ const claims = deps.claims;
14291
+ if (claims === void 0 || honoursClaims(claims, token)) return [];
14292
+ return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
14293
+ }
14294
+ async function verdictOf(engine, token) {
13945
14295
  try {
13946
- await deps.recordReceived({
13947
- dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
13948
- tokenId: record.tokenId,
13949
- stateHash: record.stateHash,
13950
- assets: record.assets,
13951
- ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
13952
- ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
13953
- ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13954
- ...entry.memo !== void 0 ? { memo: entry.memo } : {},
13955
- receivedAt
13956
- });
14296
+ return await engine.verify(token);
13957
14297
  } catch (err) {
13958
- logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
14298
+ if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
14299
+ throw err;
13959
14300
  }
14301
+ }
14302
+ async function announce(deps, entry, record) {
14303
+ const receivedAt = (deps.now ?? Date.now)();
14304
+ await recordArrival(deps, entry, record, receivedAt);
14305
+ const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
14306
+ const counted = record.assets.filter((asset) => !refused2.includes(asset));
13960
14307
  return {
13961
14308
  id: record.tokenId,
13962
14309
  senderPubkey: entry.senderPubkey ?? "",
13963
14310
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13964
- tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
14311
+ tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
14312
+ ...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
13965
14313
  // #777: named here rather than mapped from assets, which announced an EMPTY list.
13966
14314
  ...record.tokenType !== void 0 ? {
13967
14315
  coinless: [
@@ -13979,6 +14327,25 @@ async function announce(deps, entry, record) {
13979
14327
  receivedAt
13980
14328
  };
13981
14329
  }
14330
+ async function recordArrival(deps, entry, record, receivedAt) {
14331
+ const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
14332
+ if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
14333
+ try {
14334
+ await deps.recordReceived({
14335
+ dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
14336
+ tokenId: record.tokenId,
14337
+ stateHash: record.stateHash,
14338
+ assets,
14339
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
14340
+ ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
14341
+ ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
14342
+ ...entry.memo !== void 0 ? { memo: entry.memo } : {},
14343
+ receivedAt
14344
+ });
14345
+ } catch (err) {
14346
+ logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
14347
+ }
14348
+ }
13982
14349
  async function flushAcks(deps, pending, epochOf) {
13983
14350
  if (pending.length === 0) return { progressed: true };
13984
14351
  const before = pending.length;
@@ -13986,7 +14353,7 @@ async function flushAcks(deps, pending, epochOf) {
13986
14353
  try {
13987
14354
  const settled = await settleAcks(deps, pending);
13988
14355
  let i = 0;
13989
- while (i < pending.length && settled.has(pending[i].deliveryId)) {
14356
+ while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
13990
14357
  lastAcked = pending[i].cursor;
13991
14358
  i += 1;
13992
14359
  }
@@ -14062,6 +14429,9 @@ async function ackOne(deps, ack) {
14062
14429
  await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
14063
14430
  }
14064
14431
  }
14432
+ function queueAck(ctx, ack) {
14433
+ ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
14434
+ }
14065
14435
  function claimAck(entry) {
14066
14436
  return {
14067
14437
  deliveryId: entry.deliveryId,
@@ -14770,72 +15140,276 @@ var SpendQueue = class {
14770
15140
  queue = [];
14771
15141
  this.queues.set(coinId, queue);
14772
15142
  }
14773
- queue.push(entry);
14774
- return settled;
15143
+ queue.push(entry);
15144
+ return settled;
15145
+ }
15146
+ expire(id, coinId) {
15147
+ const entries = this.queues.get(coinId);
15148
+ if (!entries) return;
15149
+ const index = entries.findIndex((e) => e.id === id);
15150
+ if (index === -1) return;
15151
+ const [entry] = entries.splice(index, 1);
15152
+ entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
15153
+ if (entries.length === 0) this.queues.delete(coinId);
15154
+ }
15155
+ // Whole-token semantics: only fully-free tokens enter the selector pool. What
15156
+ // the reservations hold back is counted too — the refusal has to name it (#737).
15157
+ freeView(coinId) {
15158
+ const freeView = [];
15159
+ let freeTotal = 0n;
15160
+ let pinnedTotal = 0n;
15161
+ const holders = /* @__PURE__ */ new Set();
15162
+ const unproven = this.deps.ledger.unprovenReason();
15163
+ for (const entry of this.deps.getPool(coinId)) {
15164
+ if (entry.amount <= 0n) continue;
15165
+ const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
15166
+ if (holder === void 0) {
15167
+ freeView.push(entry);
15168
+ freeTotal += entry.amount;
15169
+ } else {
15170
+ pinnedTotal += entry.amount;
15171
+ holders.add(holder);
15172
+ }
15173
+ }
15174
+ return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
15175
+ }
15176
+ expectedChangeTotal(coinId) {
15177
+ let total = 0n;
15178
+ for (const change of this.expectedChange.values()) {
15179
+ if (change.coinId === coinId) total += change.amount;
15180
+ }
15181
+ return total;
15182
+ }
15183
+ reservePlan(reservationId, plan, freeView) {
15184
+ const pool = new Set(freeView.map((e) => e.tokenId));
15185
+ const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
15186
+ for (const tokenId of tokenIds) {
15187
+ if (!pool.has(tokenId)) {
15188
+ throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
15189
+ }
15190
+ }
15191
+ this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
15192
+ }
15193
+ totalQueued() {
15194
+ let total = 0;
15195
+ for (const entries of this.queues.values()) total += entries.length;
15196
+ return total;
15197
+ }
15198
+ };
15199
+ function parseAmount(raw) {
15200
+ if (!/^\d+$/.test(raw)) {
15201
+ throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
15202
+ }
15203
+ const amount = BigInt(raw);
15204
+ if (amount <= 0n) {
15205
+ throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
15206
+ }
15207
+ return amount;
15208
+ }
15209
+
15210
+ // modules/payments-v2/machine/payload.ts
15211
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
15212
+ return {
15213
+ v: 2,
15214
+ kind: "coin",
15215
+ recipient: recipientPubkey,
15216
+ coinId: request.coinId,
15217
+ amount: request.amount,
15218
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
15219
+ direct: [...spend.plan.direct],
15220
+ ...spend.plan.split !== void 0 ? {
15221
+ split: {
15222
+ tokenId: spend.plan.split.tokenId,
15223
+ splitAmount: spend.plan.split.splitAmount.toString(),
15224
+ remainderAmount: spend.plan.split.remainderAmount.toString()
15225
+ }
15226
+ } : {},
15227
+ spentStates
15228
+ };
15229
+ }
15230
+ function messageOf(err) {
15231
+ return err instanceof Error ? err.message : String(err);
15232
+ }
15233
+ function buildWholePayload(recipientPubkey, request, spentStates) {
15234
+ return {
15235
+ v: 2,
15236
+ kind: "whole",
15237
+ recipient: recipientPubkey,
15238
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
15239
+ direct: [request.tokenId],
15240
+ spentStates
15241
+ };
15242
+ }
15243
+
15244
+ // modules/payments-v2/burn.ts
15245
+ var HEX32 = /^[0-9a-f]{64}$/;
15246
+ async function heldToken2(deps, tokenId) {
15247
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
15248
+ if (bytes === void 0) {
15249
+ throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
15250
+ }
15251
+ return deps.engine.decodeToken({ tokenId, token: bytes });
15252
+ }
15253
+ function assertBurnable(engine, token, tokenId) {
15254
+ if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
15255
+ throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
15256
+ }
15257
+ if (token.valueEnvelope === "bare_collection") {
15258
+ throw new SphereError(
15259
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
15260
+ "VALIDATION_ERROR"
15261
+ );
15262
+ }
15263
+ }
15264
+ function assetsOf(token) {
15265
+ return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
15266
+ }
15267
+ async function runBurnUnderJournal(deps, input) {
15268
+ const { burnId } = input;
15269
+ const { tokenId, reasonBytes } = input.request;
15270
+ if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
15271
+ return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
15272
+ }
15273
+ let token;
15274
+ try {
15275
+ token = await heldToken2(deps, tokenId);
15276
+ assertBurnable(deps.engine, token, tokenId);
15277
+ } catch (err) {
15278
+ return refused(burnId, tokenId, err);
15279
+ }
15280
+ const entry = {
15281
+ burnId,
15282
+ tokenId,
15283
+ reasonHex: bytesToHex3(reasonBytes),
15284
+ burnedTokenHex: null,
15285
+ settled: false,
15286
+ assets: assetsOf(token),
15287
+ createdAt: deps.now()
15288
+ };
15289
+ await deps.burnJournal.upsert(entry);
15290
+ try {
15291
+ const settled = await burnJournaled(deps, entry, token);
15292
+ return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
15293
+ } catch (err) {
15294
+ const failure = failureOf(err);
15295
+ if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
15296
+ else deps.armHeartbeat();
15297
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
15298
+ }
15299
+ }
15300
+ async function replayBurns(deps) {
15301
+ let progressed = 0;
15302
+ for (const entry of await deps.burnJournal.list()) {
15303
+ if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
15304
+ try {
15305
+ const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
15306
+ await burnJournaled(deps, entry, token);
15307
+ progressed += 1;
15308
+ } catch (err) {
15309
+ if (!isTerminal(err)) continue;
15310
+ await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
15311
+ progressed += 1;
15312
+ }
15313
+ }
15314
+ return progressed;
15315
+ }
15316
+ async function pendingBurns(deps) {
15317
+ return (await deps.burnJournal.list()).map((entry) => ({
15318
+ burnId: entry.burnId,
15319
+ tokenId: entry.tokenId,
15320
+ reasonBytes: hexToBytes4(entry.reasonHex),
15321
+ burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
15322
+ settled: entry.settled,
15323
+ createdAt: entry.createdAt,
15324
+ ...entry.failure !== void 0 ? { failure: entry.failure } : {}
15325
+ }));
15326
+ }
15327
+ async function acknowledgeBurn(deps, burnId) {
15328
+ const entry = await deps.burnJournal.getByKey(burnId);
15329
+ if (entry === void 0) return;
15330
+ if (isLiveBurn(entry)) {
15331
+ throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
15332
+ }
15333
+ await deps.burnJournal.removeByKey(burnId);
15334
+ }
15335
+ function refused(burnId, tokenId, err) {
15336
+ const failure = failureOf(err);
15337
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
15338
+ }
15339
+ function isTerminal(err) {
15340
+ return classifyError(err) === "conflict";
15341
+ }
15342
+ function failureOf(err) {
15343
+ const code = err !== null && typeof err === "object" ? err.code : void 0;
15344
+ return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
15345
+ }
15346
+ async function burnJournaled(deps, entry, token) {
15347
+ let current = entry;
15348
+ if (current.burnedTokenHex === null) {
15349
+ if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
15350
+ const burned = await deps.engine.burn(
15351
+ { token, reasonBytes: hexToBytes4(current.reasonHex) },
15352
+ { transferId: current.burnId }
15353
+ );
15354
+ if (burned.blob.tokenId !== current.tokenId) {
15355
+ throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
15356
+ }
15357
+ current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
15358
+ await deps.burnJournal.upsert(current);
14775
15359
  }
14776
- expire(id, coinId) {
14777
- const entries = this.queues.get(coinId);
14778
- if (!entries) return;
14779
- const index = entries.findIndex((e) => e.id === id);
14780
- if (index === -1) return;
14781
- const [entry] = entries.splice(index, 1);
14782
- entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
14783
- if (entries.length === 0) this.queues.delete(coinId);
15360
+ await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
15361
+ await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
15362
+ current = { ...current, settled: true };
15363
+ await deps.burnJournal.upsert(current);
15364
+ deps.refreshView();
15365
+ return current;
15366
+ }
15367
+
15368
+ // modules/payments-v2/burn-hold.ts
15369
+ var BurnHold = class {
15370
+ constructor(deps) {
15371
+ this.deps = deps;
14784
15372
  }
14785
- // Whole-token semantics: only fully-free tokens enter the selector pool. What
14786
- // the reservations hold back is counted too — the refusal has to name it (#737).
14787
- freeView(coinId) {
14788
- const freeView = [];
14789
- let freeTotal = 0n;
14790
- let pinnedTotal = 0n;
14791
- const holders = /* @__PURE__ */ new Set();
14792
- const unproven = this.deps.ledger.unprovenReason();
14793
- for (const entry of this.deps.getPool(coinId)) {
14794
- if (entry.amount <= 0n) continue;
14795
- const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
14796
- if (holder === void 0) {
14797
- freeView.push(entry);
14798
- freeTotal += entry.amount;
14799
- } else {
14800
- pinnedTotal += entry.amount;
14801
- holders.add(holder);
14802
- }
15373
+ async run(burnId, tokenId, burn) {
15374
+ try {
15375
+ this.deps.queue.planWhole(burnId, tokenId);
15376
+ } catch (err) {
15377
+ return refused(burnId, tokenId, err);
14803
15378
  }
14804
- return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
14805
- }
14806
- expectedChangeTotal(coinId) {
14807
- let total = 0n;
14808
- for (const change of this.expectedChange.values()) {
14809
- if (change.coinId === coinId) total += change.amount;
15379
+ this.deps.view.markInFlightMany([tokenId]);
15380
+ let burned = false;
15381
+ try {
15382
+ const result = await burn();
15383
+ burned = result.success;
15384
+ return result;
15385
+ } finally {
15386
+ await this.settle(burnId, tokenId, burned);
14810
15387
  }
14811
- return total;
14812
15388
  }
14813
- reservePlan(reservationId, plan, freeView) {
14814
- const pool = new Set(freeView.map((e) => e.tokenId));
14815
- const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
14816
- for (const tokenId of tokenIds) {
14817
- if (!pool.has(tokenId)) {
14818
- throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
14819
- }
15389
+ async settle(burnId, tokenId, burned) {
15390
+ if (burned) {
15391
+ this.deps.ledger.commit(burnId);
15392
+ this.deps.track(this.refreshThenRelease(tokenId));
15393
+ return;
14820
15394
  }
14821
- this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
15395
+ const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
15396
+ if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
15397
+ this.deps.ledger.cancel(burnId);
15398
+ this.deps.view.releaseMany([tokenId]);
15399
+ this.deps.queue.notifyChange("");
15400
+ return;
15401
+ }
15402
+ this.deps.pins.adopt(burnId);
14822
15403
  }
14823
- totalQueued() {
14824
- let total = 0;
14825
- for (const entries of this.queues.values()) total += entries.length;
14826
- return total;
15404
+ async refreshThenRelease(tokenId) {
15405
+ try {
15406
+ await this.deps.view.delta();
15407
+ this.deps.view.releaseMany([tokenId]);
15408
+ } finally {
15409
+ this.deps.queue.notifyChange("");
15410
+ }
14827
15411
  }
14828
15412
  };
14829
- function parseAmount(raw) {
14830
- if (!/^\d+$/.test(raw)) {
14831
- throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
14832
- }
14833
- const amount = BigInt(raw);
14834
- if (amount <= 0n) {
14835
- throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
14836
- }
14837
- return amount;
14838
- }
14839
15413
 
14840
15414
  // modules/payments-v2/compose.ts
14841
15415
  function supportsDeterministicMint(engine) {
@@ -14844,16 +15418,8 @@ function supportsDeterministicMint(engine) {
14844
15418
  function composeFacadeParts(deps, hooks) {
14845
15419
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
14846
15420
  const ledger = new ReservationLedger();
14847
- const view = new InventoryView({
14848
- port: deps.storagePort,
14849
- kv: deps.kv,
14850
- emit: (event) => deps.emit(event, {}),
14851
- // #737: a reserved token is unselectable, so it is never confirmed balance.
14852
- // #738: while the held-set is unproven, EVERY token reads pinned — the report
14853
- // must not call a token spendable that the queue is about to refuse to spend.
14854
- isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
14855
- ...deps.now !== void 0 ? { now: deps.now } : {}
14856
- });
15421
+ const verdicts = buildVerdicts(deps, hooks);
15422
+ const view = buildView(deps, hooks, ledger, verdicts);
14857
15423
  const queue = new SpendQueue({
14858
15424
  ledger,
14859
15425
  getPool: (coinId) => view.pool(coinId),
@@ -14868,27 +15434,57 @@ function composeFacadeParts(deps, hooks) {
14868
15434
  ...deps.now !== void 0 ? { now: deps.now } : {},
14869
15435
  ...deps.newId !== void 0 ? { newId: deps.newId } : {}
14870
15436
  });
14871
- const machineDeps = buildMachineDeps(deps, hooks, historyStore);
15437
+ const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
14872
15438
  const machineStores = createMachineStores(deps.kv);
14873
15439
  const heldStates = /* @__PURE__ */ new Map();
14874
15440
  const refreshView = coalesced(() => view.delta(), hooks.track);
15441
+ const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
14875
15442
  return {
14876
15443
  ownPubkeyBytes,
14877
15444
  view,
15445
+ verdicts,
14878
15446
  refreshView,
14879
15447
  ledger,
14880
- pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
15448
+ pins,
14881
15449
  queue,
15450
+ burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
14882
15451
  historyStore,
14883
15452
  machineStores,
14884
15453
  machineDeps,
14885
15454
  machine: new TransferMachine(machineDeps),
14886
15455
  heldStates,
14887
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
15456
+ receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
14888
15457
  requests: buildRequests(deps, hooks),
14889
15458
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
14890
15459
  };
14891
15460
  }
15461
+ function buildView(deps, hooks, ledger, verdicts) {
15462
+ const view = new InventoryView({
15463
+ port: deps.storagePort,
15464
+ kv: deps.kv,
15465
+ trust: verdicts,
15466
+ emit: (event) => {
15467
+ deps.emit(event, {});
15468
+ hooks.track(verdicts.review(view.claimHolders()));
15469
+ },
15470
+ // #737: a reserved token is unselectable, so it is never confirmed balance.
15471
+ // #738: while the held-set is unproven, EVERY token reads pinned — the report
15472
+ // must not call a token spendable that the queue is about to refuse to spend.
15473
+ isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
15474
+ ...deps.now !== void 0 ? { now: deps.now } : {}
15475
+ });
15476
+ return view;
15477
+ }
15478
+ function buildVerdicts(deps, hooks) {
15479
+ return new TokenVerdicts({
15480
+ kv: deps.kv,
15481
+ claims: deps.claims ?? new CoinClaims(),
15482
+ engine: () => hooks.engine(),
15483
+ getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
15484
+ changed: () => deps.emit("inventory:updated", {}),
15485
+ ...deps.now !== void 0 ? { now: deps.now } : {}
15486
+ });
15487
+ }
14892
15488
  function buildPins(deps, hooks, parts) {
14893
15489
  return new IntentPins({
14894
15490
  ledger: parts.ledger,
@@ -14916,8 +15512,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
14916
15512
  }
14917
15513
  };
14918
15514
  }
14919
- function buildMachineDeps(deps, hooks, historyStore) {
15515
+ function buildMachineDeps(deps, hooks, historyStore, verdicts) {
14920
15516
  return {
15517
+ recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
14921
15518
  engine: () => hooks.engine(),
14922
15519
  storage: deps.storagePort,
14923
15520
  delivery: deps.deliveryPort,
@@ -14950,10 +15547,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
14950
15547
  }
14951
15548
  };
14952
15549
  }
14953
- function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
15550
+ function buildReceive(deps, hooks, parts, refreshView) {
15551
+ const { historyStore, heldStates, verdicts } = parts;
14954
15552
  return new Receive({
14955
15553
  delivery: deps.deliveryPort,
14956
15554
  engine: () => hooks.engine(),
15555
+ claims: deps.claims ?? new CoinClaims(),
15556
+ accepted: (token) => verdicts.accept(token),
14957
15557
  view: {
14958
15558
  heldState: (tokenId) => heldStates.get(tokenId) ?? null,
14959
15559
  store: async (entry) => {
@@ -15003,40 +15603,6 @@ function buildRequests(deps, hooks) {
15003
15603
  });
15004
15604
  }
15005
15605
 
15006
- // modules/payments-v2/machine/payload.ts
15007
- function buildPayload(recipientPubkey, request, spend, spentStates) {
15008
- return {
15009
- v: 2,
15010
- kind: "coin",
15011
- recipient: recipientPubkey,
15012
- coinId: request.coinId,
15013
- amount: request.amount,
15014
- ...request.memo !== void 0 ? { memo: request.memo } : {},
15015
- direct: [...spend.plan.direct],
15016
- ...spend.plan.split !== void 0 ? {
15017
- split: {
15018
- tokenId: spend.plan.split.tokenId,
15019
- splitAmount: spend.plan.split.splitAmount.toString(),
15020
- remainderAmount: spend.plan.split.remainderAmount.toString()
15021
- }
15022
- } : {},
15023
- spentStates
15024
- };
15025
- }
15026
- function messageOf(err) {
15027
- return err instanceof Error ? err.message : String(err);
15028
- }
15029
- function buildWholePayload(recipientPubkey, request, spentStates) {
15030
- return {
15031
- v: 2,
15032
- kind: "whole",
15033
- recipient: recipientPubkey,
15034
- ...request.memo !== void 0 ? { memo: request.memo } : {},
15035
- direct: [request.tokenId],
15036
- spentStates
15037
- };
15038
- }
15039
-
15040
15606
  // modules/payments-v2/mint-params.ts
15041
15607
  function mintParams(recipientPubkey, coinId, amount) {
15042
15608
  return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
@@ -15106,6 +15672,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
15106
15672
  }
15107
15673
  return false;
15108
15674
  }
15675
+ async function seedHeldStates(storagePort, heldStates) {
15676
+ let page = await storagePort.listInventory();
15677
+ for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
15678
+ for (const item of page.items) {
15679
+ if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
15680
+ }
15681
+ if (!page.more) return;
15682
+ page = await storagePort.listInventory(page.cursor);
15683
+ }
15684
+ }
15109
15685
  async function replayCoinMints(deps) {
15110
15686
  let resolved = 0;
15111
15687
  for (const entry of await deps.mintJournal.list()) {
@@ -15210,7 +15786,8 @@ async function mintJournaled(deps, entry) {
15210
15786
  }
15211
15787
 
15212
15788
  // modules/payments-v2/mint-custom.ts
15213
- var HEX32 = /^[0-9a-f]{64}$/;
15789
+ var HEX322 = /^[0-9a-f]{64}$/;
15790
+ var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
15214
15791
  function assertRequest(request) {
15215
15792
  if (request.tokenType.length !== 32) {
15216
15793
  throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
@@ -15221,19 +15798,27 @@ function assertRequest(request) {
15221
15798
  if (request.data.length === 0) {
15222
15799
  throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
15223
15800
  }
15801
+ if (request.justification !== void 0 && request.justification.length === 0) {
15802
+ throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
15803
+ }
15804
+ if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
15805
+ throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
15806
+ }
15224
15807
  for (const asset of request.assets) {
15225
- if (!HEX32.test(asset.coinId) || asset.amount <= 0n) {
15808
+ if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
15226
15809
  throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
15227
15810
  }
15228
15811
  }
15229
15812
  }
15230
15813
  async function runCustomMintUnderJournal(deps, input) {
15814
+ let entry;
15231
15815
  try {
15232
15816
  assertRequest(input.request);
15817
+ entry = journalEntryOf2(input.mintId, input.request, deps.now());
15818
+ deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
15233
15819
  } catch (err) {
15234
15820
  return { success: false, error: messageOf(err) };
15235
15821
  }
15236
- const entry = journalEntryOf2(input.mintId, input.request, deps.now());
15237
15822
  await deps.customMintJournal.upsert(entry);
15238
15823
  let tokenId;
15239
15824
  try {
@@ -15273,24 +15858,25 @@ function journalEntryOf2(mintId, request, createdAt) {
15273
15858
  createdAt
15274
15859
  };
15275
15860
  }
15861
+ function mintParamsOf(deps, entry, mintJustificationVerifiers) {
15862
+ return {
15863
+ recipientPubkey: deps.ownPubkeyBytes,
15864
+ data: hexToBytes4(entry.dataHex),
15865
+ tokenType: hexToBytes4(entry.tokenTypeHex),
15866
+ salt: hexToBytes4(entry.saltHex),
15867
+ ...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
15868
+ ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
15869
+ };
15870
+ }
15276
15871
  async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
15277
- const token = await deps.engine.mintDataToken(
15278
- {
15279
- recipientPubkey: deps.ownPubkeyBytes,
15280
- data: hexToBytes4(entry.dataHex),
15281
- tokenType: hexToBytes4(entry.tokenTypeHex),
15282
- salt: hexToBytes4(entry.saltHex),
15283
- ...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
15284
- ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
15285
- },
15286
- { transferId: entry.mintId }
15287
- );
15872
+ const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
15288
15873
  if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
15289
15874
  throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
15290
15875
  }
15291
15876
  if (entry.tokenId === "") {
15292
15877
  await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
15293
15878
  }
15879
+ if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
15294
15880
  await finalizeMint(deps, {
15295
15881
  mintId: entry.mintId,
15296
15882
  token,
@@ -15300,108 +15886,6 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
15300
15886
  return token.blob.tokenId;
15301
15887
  }
15302
15888
 
15303
- // modules/payments-v2/burn.ts
15304
- var HEX322 = /^[0-9a-f]{64}$/;
15305
- async function heldToken2(deps, tokenId) {
15306
- const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
15307
- if (bytes === void 0) {
15308
- throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
15309
- }
15310
- return deps.engine.decodeToken({ tokenId, token: bytes });
15311
- }
15312
- function assetsOf(token) {
15313
- return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
15314
- }
15315
- async function runBurnUnderJournal(deps, input) {
15316
- const { burnId } = input;
15317
- const { tokenId, reasonBytes } = input.request;
15318
- if (!HEX322.test(tokenId) || reasonBytes.length === 0) {
15319
- return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty" };
15320
- }
15321
- let token;
15322
- try {
15323
- token = await heldToken2(deps, tokenId);
15324
- if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
15325
- throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
15326
- }
15327
- } catch (err) {
15328
- return { success: false, burnId, tokenId, error: messageOf(err) };
15329
- }
15330
- const entry = {
15331
- burnId,
15332
- tokenId,
15333
- reasonHex: bytesToHex3(reasonBytes),
15334
- burnedTokenHex: null,
15335
- settled: false,
15336
- assets: assetsOf(token),
15337
- createdAt: deps.now()
15338
- };
15339
- await deps.burnJournal.upsert(entry);
15340
- try {
15341
- const settled = await burnJournaled(deps, entry, token, { applied: false });
15342
- return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
15343
- } catch (err) {
15344
- deps.armHeartbeat();
15345
- return { success: false, burnId, tokenId, error: messageOf(err) };
15346
- }
15347
- }
15348
- async function replayBurns(deps) {
15349
- let progressed = 0;
15350
- for (const entry of await deps.burnJournal.list()) {
15351
- if (entry.settled || deps.isActiveOp(entry.burnId)) continue;
15352
- try {
15353
- const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
15354
- const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
15355
- await burnJournaled(deps, entry, token, { applied });
15356
- progressed += 1;
15357
- } catch {
15358
- continue;
15359
- }
15360
- }
15361
- return progressed;
15362
- }
15363
- async function pendingBurns(deps) {
15364
- return (await deps.burnJournal.list()).map((entry) => ({
15365
- burnId: entry.burnId,
15366
- tokenId: entry.tokenId,
15367
- reasonBytes: hexToBytes4(entry.reasonHex),
15368
- burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
15369
- settled: entry.settled,
15370
- createdAt: entry.createdAt
15371
- }));
15372
- }
15373
- async function acknowledgeBurn(deps, burnId) {
15374
- const entry = await deps.burnJournal.getByKey(burnId);
15375
- if (entry === void 0) return;
15376
- if (!entry.settled) {
15377
- throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
15378
- }
15379
- await deps.burnJournal.removeByKey(burnId);
15380
- }
15381
- async function burnJournaled(deps, entry, token, state) {
15382
- let current = entry;
15383
- if (current.burnedTokenHex === null) {
15384
- if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
15385
- const burned = await deps.engine.burn(
15386
- { token, reasonBytes: hexToBytes4(current.reasonHex) },
15387
- { transferId: current.burnId }
15388
- );
15389
- if (burned.blob.tokenId !== current.tokenId) {
15390
- throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
15391
- }
15392
- current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
15393
- await deps.burnJournal.upsert(current);
15394
- }
15395
- if (!state.applied) {
15396
- await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [] });
15397
- }
15398
- await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
15399
- current = { ...current, settled: true };
15400
- await deps.burnJournal.upsert(current);
15401
- deps.refreshView();
15402
- return current;
15403
- }
15404
-
15405
15889
  // modules/payments-v2/send-whole.ts
15406
15890
  async function materializeWholeSpend(deps, input) {
15407
15891
  const { engine } = deps;
@@ -15597,6 +16081,7 @@ var PaymentsFacade = class {
15597
16081
  track: (op) => this.trackTail(op)
15598
16082
  });
15599
16083
  this.view = parts.view;
16084
+ this.verdicts = parts.verdicts;
15600
16085
  this.wakeRefresh = parts.refreshView;
15601
16086
  this.ledger = parts.ledger;
15602
16087
  this.queue = parts.queue;
@@ -15610,6 +16095,7 @@ var PaymentsFacade = class {
15610
16095
  this.restoreDeps = parts.restoreDeps;
15611
16096
  this.requests = parts.requests;
15612
16097
  this.pins = parts.pins;
16098
+ this.burnHold = parts.burnHold;
15613
16099
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
15614
16100
  this.heartbeat = new ConvergenceHeartbeat({
15615
16101
  now: () => this.nowMs(),
@@ -15633,6 +16119,7 @@ var PaymentsFacade = class {
15633
16119
  wakeRefresh;
15634
16120
  prewarmed = new PrewarmCache();
15635
16121
  view;
16122
+ verdicts;
15636
16123
  ledger;
15637
16124
  queue;
15638
16125
  machine;
@@ -15659,6 +16146,7 @@ var PaymentsFacade = class {
15659
16146
  activeMoneyOps = /* @__PURE__ */ new Set();
15660
16147
  /** #737: the ledger holds exactly the sources of the still-open intents. */
15661
16148
  pins;
16149
+ burnHold;
15662
16150
  /** Per facade, so per address: an NFT reading is never served to another wallet. */
15663
16151
  nftCache = new NftCache();
15664
16152
  // ── lifecycle ──────────────────────────────────────────────────────────────
@@ -15683,7 +16171,8 @@ var PaymentsFacade = class {
15683
16171
  await this.deps.session.start();
15684
16172
  await this.pins.sync().catch(() => void 0);
15685
16173
  this.trackTail(this.runConvergencePass());
15686
- this.trackTail(this.seedHeldStates());
16174
+ this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
16175
+ await this.verdicts.start().catch(() => void 0);
15687
16176
  await this.view.fullPull().catch(() => void 0);
15688
16177
  this.receiveLoop.start(this.deps.receivePollMs);
15689
16178
  }
@@ -15692,6 +16181,7 @@ var PaymentsFacade = class {
15692
16181
  this.started = false;
15693
16182
  this.heartbeat.stop();
15694
16183
  this.receiveLoop.stop();
16184
+ this.verdicts.stop();
15695
16185
  for (const unsubscribe of this.unsubscribers) unsubscribe();
15696
16186
  this.unsubscribers = [];
15697
16187
  await this.deps.session.stop();
@@ -15708,12 +16198,17 @@ var PaymentsFacade = class {
15708
16198
  }
15709
16199
  // ── reads ──────────────────────────────────────────────────────────────────
15710
16200
  async assets(coinId) {
16201
+ return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
16202
+ }
16203
+ unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
16204
+ async heldAssets(coinId) {
15711
16205
  const all = await this.view.assets(this.deps.registry, this.deps.price);
15712
16206
  return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
15713
16207
  }
15714
16208
  tokens(filter) {
15715
- return this.view.tokens(this.deps.registry, filter);
16209
+ return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
15716
16210
  }
16211
+ unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
15717
16212
  coinless() {
15718
16213
  return this.view.coinless(this.deps.registry);
15719
16214
  }
@@ -15774,9 +16269,9 @@ var PaymentsFacade = class {
15774
16269
  return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
15775
16270
  }
15776
16271
  mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
15777
- burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
15778
- pendingBurns = () => this.track(pendingBurns(this.mintDeps()));
15779
- acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.mintDeps(), burnId));
16272
+ burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
16273
+ pendingBurns = () => this.track(pendingBurns(this.machineStores));
16274
+ acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
15780
16275
  // §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
15781
16276
  async resumeNow() {
15782
16277
  if (!this.started) return;
@@ -15819,7 +16314,8 @@ var PaymentsFacade = class {
15819
16314
  isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
15820
16315
  tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
15821
16316
  emit: this.deps.emit,
15822
- now: () => this.nowMs()
16317
+ now: () => this.nowMs(),
16318
+ accepted: (token) => this.verdicts.accept(token)
15823
16319
  };
15824
16320
  }
15825
16321
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
@@ -16157,16 +16653,6 @@ var PaymentsFacade = class {
16157
16653
  newId() {
16158
16654
  return (this.deps.newId ?? randomUUID)();
16159
16655
  }
16160
- async seedHeldStates() {
16161
- let page = await this.deps.storagePort.listInventory();
16162
- for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
16163
- for (const item of page.items) {
16164
- if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
16165
- }
16166
- if (!page.more) return;
16167
- page = await this.deps.storagePort.listInventory(page.cursor);
16168
- }
16169
- }
16170
16656
  track(op) {
16171
16657
  const settled = op.then(() => void 0, () => void 0);
16172
16658
  this.pendingOps.add(settled);
@@ -17410,6 +17896,7 @@ function composePaymentsV2(spec) {
17410
17896
  engineRef: spec.engineRef,
17411
17897
  kv,
17412
17898
  registry: host.registry,
17899
+ claims: CoinClaims.fromPlugins(spec.plugins),
17413
17900
  ...host.price !== null ? { price: host.price } : {},
17414
17901
  emit: host.emit,
17415
17902
  resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
@@ -20458,7 +20945,8 @@ var Sphere = class _Sphere {
20458
20945
  // deliver the NEXT address's Unicity ID — a name the sending key does
20459
20946
  // not own (sphere#487 review).
20460
20947
  nametag: () => this.getNametagForAddress(facadeAddressId)
20461
- }
20948
+ },
20949
+ ...this._plugins ? { plugins: this._plugins } : {}
20462
20950
  });
20463
20951
  this._paymentsV2Active = { index, facade };
20464
20952
  await facade.start();