@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
package/dist/index.js CHANGED
@@ -9529,6 +9529,150 @@ async function deriveDirectAddress(publicKey) {
9529
9529
  return `DIRECT://${HexConverter.encode(imprint)}${HexConverter.encode(checksum)}`;
9530
9530
  }
9531
9531
 
9532
+ // token-engine/claims.ts
9533
+ var CoinClaims = class _CoinClaims {
9534
+ issuers = /* @__PURE__ */ new Map();
9535
+ policies = [];
9536
+ static fromPlugins(plugins) {
9537
+ const claims = new _CoinClaims();
9538
+ for (const plugin of plugins ?? []) {
9539
+ for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
9540
+ }
9541
+ return claims;
9542
+ }
9543
+ add(policy) {
9544
+ const tokenType = HexConverter.encode(policy.tokenType.bytes);
9545
+ const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
9546
+ const taken = coinIds.find((coinId) => this.issuers.has(coinId));
9547
+ if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
9548
+ for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
9549
+ this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
9550
+ }
9551
+ fingerprint() {
9552
+ return [...this.policies].sort().join("\n");
9553
+ }
9554
+ issuerOf(coinId) {
9555
+ return this.issuers.get(coinId.toLowerCase()) ?? null;
9556
+ }
9557
+ };
9558
+
9559
+ // token-engine/errors.ts
9560
+ var TransferConflictError = class extends SphereError {
9561
+ /**
9562
+ * #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
9563
+ * Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
9564
+ */
9565
+ conflictedSourceId;
9566
+ constructor(message, cause) {
9567
+ super(message, "TRANSFER_CONFLICT", cause);
9568
+ this.name = "TransferConflictError";
9569
+ }
9570
+ };
9571
+ var MintReasonUnverifiableError = class extends SphereError {
9572
+ constructor(message, cause) {
9573
+ super(message, "MINT_REASON_UNVERIFIABLE", cause);
9574
+ this.name = "MintReasonUnverifiableError";
9575
+ }
9576
+ };
9577
+ var ProofUnconfirmedError = class extends SphereError {
9578
+ mayHaveCertified = true;
9579
+ constructor(message, cause) {
9580
+ super(message, "CERTIFICATION_UNCONFIRMED", cause);
9581
+ this.name = "ProofUnconfirmedError";
9582
+ }
9583
+ };
9584
+ var CheckpointPersistFailedError = class extends SphereError {
9585
+ mayHaveCertified = true;
9586
+ constructor(message, cause) {
9587
+ super(message, "CHECKPOINT_PERSIST_FAILED", cause);
9588
+ this.name = "CheckpointPersistFailedError";
9589
+ }
9590
+ };
9591
+ var SplitCheckpointLostError = class extends SphereError {
9592
+ constructor(message, cause) {
9593
+ super(message, "SPLIT_CHECKPOINT_LOST", cause);
9594
+ this.name = "SplitCheckpointLostError";
9595
+ }
9596
+ };
9597
+ var CheckpointTrustbaseMismatchError = class extends SphereError {
9598
+ constructor(message, cause) {
9599
+ super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
9600
+ this.name = "CheckpointTrustbaseMismatchError";
9601
+ }
9602
+ };
9603
+ function isKeepOpenSplitError(err) {
9604
+ return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
9605
+ }
9606
+
9607
+ // token-engine/mint-reasons.ts
9608
+ var RULE = "MintJustificationVerification";
9609
+ var MintReasonRegistry = class _MintReasonRegistry extends MintJustificationVerifierService {
9610
+ byTag = /* @__PURE__ */ new Map();
9611
+ pluginTags = /* @__PURE__ */ new Set();
9612
+ register(verifier) {
9613
+ super.register(verifier);
9614
+ this.byTag.set(verifier.tag, verifier);
9615
+ return this;
9616
+ }
9617
+ registerPlugin(verifier) {
9618
+ this.register(verifier);
9619
+ this.pluginTags.add(verifier.tag);
9620
+ return this;
9621
+ }
9622
+ async verify(transaction, nestedTokenCollector) {
9623
+ const tag = transaction.justification ? reasonTag(transaction.justification) : null;
9624
+ if (tag === null) return super.verify(transaction, nestedTokenCollector);
9625
+ const verifier = this.byTag.get(tag);
9626
+ if (verifier === void 0) throw new MintReasonUnverifiableError(`No verifier is registered for mint-reason tag ${tag}`);
9627
+ if (!this.pluginTags.has(tag)) return super.verify(transaction, nestedTokenCollector);
9628
+ return verifyPlugin(verifier, transaction, nestedTokenCollector);
9629
+ }
9630
+ knows(tag) {
9631
+ return this.byTag.has(tag);
9632
+ }
9633
+ /** `verifiers` replace the registered ones of their tags; a tag this registry lacks is refused. */
9634
+ overlaid(verifiers) {
9635
+ for (const verifier of verifiers) assertKnown(this, verifier.tag);
9636
+ const replaced = new Map(verifiers.map((verifier) => [verifier.tag, verifier]));
9637
+ const overlay = new _MintReasonRegistry();
9638
+ for (const [tag, verifier] of this.byTag) {
9639
+ const perCall = replaced.get(tag);
9640
+ if (perCall !== void 0 || this.pluginTags.has(tag)) overlay.registerPlugin(perCall ?? verifier);
9641
+ else overlay.register(verifier);
9642
+ }
9643
+ return overlay;
9644
+ }
9645
+ assertVerifiable(justification) {
9646
+ const tag = reasonTag(justification);
9647
+ if (tag === null) throw new SphereError("The mint reason is not tagged CBOR", "VALIDATION_ERROR");
9648
+ assertKnown(this, tag);
9649
+ }
9650
+ };
9651
+ function reasonTag(justification) {
9652
+ try {
9653
+ return BigInt(CborDeserializer.decodeTag(justification).tag);
9654
+ } catch {
9655
+ return null;
9656
+ }
9657
+ }
9658
+ async function verifyPlugin(verifier, transaction, nestedTokenCollector) {
9659
+ let result;
9660
+ try {
9661
+ result = await verifier.verify(transaction, nestedTokenCollector);
9662
+ } catch (err) {
9663
+ const why = err instanceof Error ? err.message : String(err);
9664
+ throw new MintReasonUnverifiableError(`Mint-reason tag ${verifier.tag} cannot be verified yet: ${why}`, err);
9665
+ }
9666
+ return result.status === VerificationStatus.OK ? new VerificationResult(RULE, VerificationStatus.OK, "", [result]) : new VerificationResult(RULE, VerificationStatus.FAIL, `Verification failed for tag ${verifier.tag}.`, [result]);
9667
+ }
9668
+ function assertKnown(registry, tag) {
9669
+ if (registry.knows(tag)) return;
9670
+ throw new SphereError(
9671
+ `No verifier is registered on the engine for mint-reason tag ${tag}, so a replay of this mint could never verify`,
9672
+ "VALIDATION_ERROR"
9673
+ );
9674
+ }
9675
+
9532
9676
  // token-engine/SpherePaymentData.ts
9533
9677
  var COIN_ID_PATTERN = /^([0-9a-f]{2})+$/;
9534
9678
  function assertAsset(coinId, amount) {
@@ -9705,45 +9849,6 @@ async function awaitProofBounded(deps, transaction, signal) {
9705
9849
  return value;
9706
9850
  }
9707
9851
 
9708
- // token-engine/errors.ts
9709
- var TransferConflictError = class extends SphereError {
9710
- /**
9711
- * #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
9712
- * Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
9713
- */
9714
- conflictedSourceId;
9715
- constructor(message, cause) {
9716
- super(message, "TRANSFER_CONFLICT", cause);
9717
- this.name = "TransferConflictError";
9718
- }
9719
- };
9720
- var ProofUnconfirmedError = class extends SphereError {
9721
- mayHaveCertified = true;
9722
- constructor(message, cause) {
9723
- super(message, "CERTIFICATION_UNCONFIRMED", cause);
9724
- this.name = "ProofUnconfirmedError";
9725
- }
9726
- };
9727
- var CheckpointPersistFailedError = class extends SphereError {
9728
- mayHaveCertified = true;
9729
- constructor(message, cause) {
9730
- super(message, "CHECKPOINT_PERSIST_FAILED", cause);
9731
- this.name = "CheckpointPersistFailedError";
9732
- }
9733
- };
9734
- var SplitCheckpointLostError = class extends SphereError {
9735
- constructor(message, cause) {
9736
- super(message, "SPLIT_CHECKPOINT_LOST", cause);
9737
- this.name = "SplitCheckpointLostError";
9738
- }
9739
- };
9740
- var CheckpointTrustbaseMismatchError = class extends SphereError {
9741
- constructor(message, cause) {
9742
- super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
9743
- this.name = "CheckpointTrustbaseMismatchError";
9744
- }
9745
- };
9746
-
9747
9852
  // token-engine/certification-outcome.ts
9748
9853
  function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
9749
9854
  if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
@@ -10322,15 +10427,17 @@ async function readTokenNft(token) {
10322
10427
  }
10323
10428
 
10324
10429
  // token-engine/plugin-ops.ts
10325
- function contextWithMintVerifiers(base, verifiers) {
10326
- const registry = new MintJustificationVerifierService();
10327
- for (const verifier of verifiers) registry.register(verifier);
10430
+ function mintContext(deps, params) {
10431
+ assertMintableData(params.data);
10432
+ if (params.justification !== void 0) deps.mintJustificationVerifier.assertVerifiable(params.justification);
10433
+ const verifiers = params.mintJustificationVerifiers;
10434
+ if (verifiers === void 0) return deps.verificationContext;
10328
10435
  return new VerificationContext(
10329
- base.trustBase,
10330
- base.predicateVerifier,
10331
- base.unicityCertificateVerifier,
10332
- registry,
10333
- new TokenIssuanceVerifierService(false)
10436
+ deps.trustBase,
10437
+ deps.predicateVerifier,
10438
+ deps.unicityCertificateVerifier,
10439
+ deps.mintJustificationVerifier.overlaid(verifiers),
10440
+ deps.verificationContext.tokenIssuanceVerifier
10334
10441
  );
10335
10442
  }
10336
10443
 
@@ -10418,9 +10525,6 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
10418
10525
  // token-engine/SphereTokenEngine.ts
10419
10526
  var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
10420
10527
  var MAX_MINT_CONCURRENCY = 8;
10421
- function isKeepOpenSplitError(err) {
10422
- return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
10423
- }
10424
10528
  var TRANSFER_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
10425
10529
  var CLEAN_REJECT_STATUSES = /* @__PURE__ */ new Set([
10426
10530
  CertificationStatus.STATE_ID_MISMATCH,
@@ -10510,8 +10614,9 @@ var SphereTokenEngine = class {
10510
10614
  const token = await Token.mint(certified, this.deps.verificationContext);
10511
10615
  return wrapToken(token);
10512
10616
  }
10617
+ assertMintable = (params) => void mintContext(this.deps, params);
10513
10618
  async mintDataToken(params, options) {
10514
- assertMintableData(params.data);
10619
+ const context = mintContext(this.deps, params);
10515
10620
  const recipient = SignaturePredicate.create(params.recipientPubkey);
10516
10621
  const tokenType = params.tokenType ? new TokenType(params.tokenType) : TokenType.generate();
10517
10622
  const salt = params.salt ? TokenSalt.fromBytes(params.salt) : TokenSalt.generate();
@@ -10525,8 +10630,7 @@ var SphereTokenEngine = class {
10525
10630
  options
10526
10631
  );
10527
10632
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10528
- const verifiers = params.mintJustificationVerifiers;
10529
- return wrapToken(await Token.mint(certified, verifiers ? contextWithMintVerifiers(this.deps, verifiers) : this.deps.verificationContext));
10633
+ return wrapToken(await Token.mint(certified, context));
10530
10634
  }
10531
10635
  buildNftMint(params) {
10532
10636
  return planNftMint(this.deps, params);
@@ -10863,20 +10967,30 @@ var SphereTokenEngine = class {
10863
10967
 
10864
10968
  // token-engine/factory.ts
10865
10969
  var DEFAULT_VERIFICATION_POOL_SIZE = 4;
10866
- function registerPluginVerifiers(registry, plugins) {
10970
+ function registerPlugins(registries, plugins) {
10867
10971
  for (const plugin of plugins ?? []) {
10868
10972
  for (const verifier of plugin.mintJustificationVerifiers ?? []) {
10869
- try {
10870
- registry.register(verifier);
10871
- } catch (err) {
10872
- throw new SphereError(
10873
- `Token plugin '${plugin.id}' registers mint-reason tag ${verifier.tag} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
10874
- "INVALID_CONFIG"
10875
- );
10876
- }
10973
+ claim(plugin, `mint-reason tag ${verifier.tag}`, () => registries.reasons.registerPlugin(verifier));
10974
+ }
10975
+ for (const policy of plugin.tokenIssuancePolicies ?? []) {
10976
+ const tokenType = HexConverter.encode(policy.tokenType.bytes);
10977
+ claim(plugin, `an issuance policy for token type ${tokenType}`, () => {
10978
+ registries.claims.add(policy);
10979
+ registries.policies.register(policy);
10980
+ });
10877
10981
  }
10878
10982
  }
10879
10983
  }
10984
+ function claim(plugin, what, register) {
10985
+ try {
10986
+ register();
10987
+ } catch (err) {
10988
+ throw new SphereError(
10989
+ `Token plugin '${plugin.id}' registers ${what} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
10990
+ "INVALID_CONFIG"
10991
+ );
10992
+ }
10993
+ }
10880
10994
  function disposedError() {
10881
10995
  return new SphereError(
10882
10996
  "Verification worker pool disposed \u2014 the in-flight verification was cancelled",
@@ -10954,11 +11068,15 @@ async function createSphereTokenEngine(config) {
10954
11068
  const unicityCertificateVerifier = new UnicityCertificateVerifier(
10955
11069
  new UnicitySealQuorumSignaturesVerificationRule(new Secp256k1SignatureVerifier(), new VerifiedSealCache(256))
10956
11070
  );
10957
- const mintJustificationVerifier = new MintJustificationVerifierService();
11071
+ const mintJustificationVerifier = new MintReasonRegistry();
10958
11072
  mintJustificationVerifier.register(
10959
11073
  new SplitMintJustificationVerifier(decodeSpherePaymentData)
10960
11074
  );
10961
- registerPluginVerifiers(mintJustificationVerifier, config.plugins);
11075
+ const tokenIssuanceVerifier = new TokenIssuanceVerifierService(false);
11076
+ registerPlugins(
11077
+ { reasons: mintJustificationVerifier, policies: tokenIssuanceVerifier, claims: new CoinClaims() },
11078
+ config.plugins
11079
+ );
10962
11080
  const deps = {
10963
11081
  client: new StateTransitionClient(new AggregatorClient(config.aggregatorUrl, config.apiKey ?? null)),
10964
11082
  trustBase,
@@ -10970,7 +11088,7 @@ async function createSphereTokenEngine(config) {
10970
11088
  predicateVerifier,
10971
11089
  unicityCertificateVerifier,
10972
11090
  mintJustificationVerifier,
10973
- new TokenIssuanceVerifierService(false)
11091
+ tokenIssuanceVerifier
10974
11092
  ),
10975
11093
  signingService: new SigningService(config.privateKey),
10976
11094
  // Also the HKDF ikm for deterministic realization (Part E.1) — the
@@ -12070,6 +12188,9 @@ function createScopedKV(storage, network, chainPubkey) {
12070
12188
  }
12071
12189
  };
12072
12190
  }
12191
+ function isLiveBurn(entry) {
12192
+ return !entry.settled && entry.failure === void 0;
12193
+ }
12073
12194
  var STORE_KEYS = {
12074
12195
  intentBackstop: "intents",
12075
12196
  checkpointCache: "checkpoints",
@@ -12084,7 +12205,8 @@ var STORE_KEYS = {
12084
12205
  epochLatch: "epoch-latch",
12085
12206
  // §5.2 InventoryView durable overlays (#625/#679).
12086
12207
  suspectedSpent: "suspected-spent",
12087
- knownSpends: "known-spends"
12208
+ knownSpends: "known-spends",
12209
+ verifiedTokens: "verified-tokens"
12088
12210
  };
12089
12211
 
12090
12212
  // modules/payments-v2/machine/journal.ts
@@ -12601,6 +12723,7 @@ var TransferMachine = class {
12601
12723
  const key = keys.get(digest);
12602
12724
  if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
12603
12725
  const { tokenId } = await engine.deliveryKeys(o.changeBlob);
12726
+ await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
12604
12727
  added.push({ tokenId, key });
12605
12728
  }
12606
12729
  return added;
@@ -12996,7 +13119,7 @@ var Converger = class {
12996
13119
  this.deps.stores.burnJournal.list()
12997
13120
  ]);
12998
13121
  const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
12999
- const burnsOpen = burns.some((e) => !e.settled);
13122
+ const burnsOpen = burns.some(isLiveBurn);
13000
13123
  return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
13001
13124
  }
13002
13125
  async resumePass() {
@@ -13083,6 +13206,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
13083
13206
  const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
13084
13207
  holds.set(entry.transferId, [...direct, ...split]);
13085
13208
  }
13209
+ for (const burn of await stores.burnJournal.list()) {
13210
+ if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
13211
+ }
13086
13212
  return { open: holds, complete };
13087
13213
  }
13088
13214
  function groupJournal(journal) {
@@ -13393,18 +13519,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
13393
13519
  createdAt: snapshot.createdAt,
13394
13520
  updatedAt: snapshot.updatedAt,
13395
13521
  lazy: true,
13396
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
13522
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {},
13523
+ ...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
13397
13524
  };
13398
13525
  }
13399
- function aggregateAssets(entries, isTransferring, registry) {
13526
+ function aggregateAssets(entries, reading, registry) {
13400
13527
  const groups = /* @__PURE__ */ new Map();
13401
13528
  for (const [tokenId, entry] of entries) {
13402
- const moving = isTransferring(tokenId);
13529
+ const moving = reading.isTransferring(tokenId);
13403
13530
  for (const asset of entry.assets) {
13404
- let group = groups.get(asset.coinId);
13531
+ const standing = reading.standing(tokenId, asset.coinId);
13532
+ const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
13533
+ let group = groups.get(key);
13405
13534
  if (!group) {
13406
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
13407
- groups.set(asset.coinId, group);
13535
+ group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
13536
+ groups.set(key, group);
13408
13537
  }
13409
13538
  if (moving) {
13410
13539
  group.transferring += BigInt(asset.amount);
@@ -13415,9 +13544,10 @@ function aggregateAssets(entries, isTransferring, registry) {
13415
13544
  }
13416
13545
  }
13417
13546
  }
13418
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
13547
+ return [...groups.values()].map((totals) => toAsset(totals, registry));
13419
13548
  }
13420
- function toAsset(coinId, totals, registry) {
13549
+ function toAsset(totals, registry) {
13550
+ const coinId = totals.coinId;
13421
13551
  const iconUrl = registry.getIconUrl(coinId);
13422
13552
  return {
13423
13553
  coinId,
@@ -13437,7 +13567,8 @@ function toAsset(coinId, totals, registry) {
13437
13567
  priceEur: null,
13438
13568
  change24h: null,
13439
13569
  fiatValueUsd: null,
13440
- fiatValueEur: null
13570
+ fiatValueEur: null,
13571
+ ...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
13441
13572
  };
13442
13573
  }
13443
13574
  function priceIdOf(registry, coinId) {
@@ -13445,9 +13576,9 @@ function priceIdOf(registry, coinId) {
13445
13576
  }
13446
13577
  async function withPrices(raw, registry, price) {
13447
13578
  try {
13448
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
13579
+ const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
13449
13580
  const quotes = await price.getPrices(ids);
13450
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13581
+ return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13451
13582
  } catch {
13452
13583
  return raw;
13453
13584
  }
@@ -13468,11 +13599,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
13468
13599
  const snapshot = { assets: [], createdAt: now, updatedAt: now };
13469
13600
  return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
13470
13601
  transferring: true,
13471
- suspectedSpent: false
13602
+ suspectedSpent: false,
13603
+ standing: "trusted"
13472
13604
  });
13473
13605
  }
13474
13606
 
13475
13607
  // modules/payments-v2/inventory/InventoryView.ts
13608
+ var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
13476
13609
  var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
13477
13610
  var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
13478
13611
  var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
@@ -13566,6 +13699,12 @@ var InventoryView = class {
13566
13699
  for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
13567
13700
  if (changed) this.deps.emit("inventory:updated");
13568
13701
  }
13702
+ get trust() {
13703
+ return this.deps.trust ?? TRUST_ALL;
13704
+ }
13705
+ trusts(tokenId, coinId) {
13706
+ return this.trust.standing(tokenId, coinId) === "trusted";
13707
+ }
13569
13708
  pinned(tokenId) {
13570
13709
  return this.deps.isPinned?.(tokenId) ?? false;
13571
13710
  }
@@ -13592,6 +13731,7 @@ var InventoryView = class {
13592
13731
  isSpendable(tokenId, entry) {
13593
13732
  if (entry.status !== "active") return false;
13594
13733
  if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13734
+ if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
13595
13735
  return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13596
13736
  }
13597
13737
  /** Is this NAMED token spendable whole — coinless or valued (#777)? */
@@ -13618,7 +13758,8 @@ var InventoryView = class {
13618
13758
  out.push(
13619
13759
  toToken(tokenId, entry, asset, registry, {
13620
13760
  transferring: this.held(tokenId),
13621
- suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
13761
+ suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
13762
+ standing: this.trust.standing(tokenId, asset.coinId)
13622
13763
  })
13623
13764
  );
13624
13765
  }
@@ -13648,10 +13789,21 @@ var InventoryView = class {
13648
13789
  return out;
13649
13790
  }
13650
13791
  async assets(registry, price) {
13651
- const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
13792
+ const reading = {
13793
+ isTransferring: (tokenId) => this.held(tokenId),
13794
+ standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
13795
+ };
13796
+ const raw = aggregateAssets(this.activeEntries(), reading, registry);
13652
13797
  if (!price || raw.length === 0) return raw;
13653
13798
  return withPrices(raw, registry, price);
13654
13799
  }
13800
+ claimHolders() {
13801
+ const out = [];
13802
+ for (const [tokenId, entry] of this.activeEntries()) {
13803
+ if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
13804
+ }
13805
+ return out;
13806
+ }
13655
13807
  *activeEntries() {
13656
13808
  for (const [tokenId, entry] of this.mirror) {
13657
13809
  if (entry.status === "active") yield [tokenId, entry];
@@ -13821,6 +13973,171 @@ var InventoryView = class {
13821
13973
  }
13822
13974
  };
13823
13975
 
13976
+ // modules/payments-v2/inventory/verdicts.ts
13977
+ var VERDICT_RETRY_MS = 3e4;
13978
+ var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
13979
+ function honoursClaims(claims, token) {
13980
+ const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
13981
+ return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
13982
+ }
13983
+ var TokenVerdicts = class {
13984
+ constructor(deps) {
13985
+ this.deps = deps;
13986
+ }
13987
+ verified = /* @__PURE__ */ new Set();
13988
+ refused = /* @__PURE__ */ new Set();
13989
+ retries = /* @__PURE__ */ new Map();
13990
+ writes = new SerialChain();
13991
+ hydration = null;
13992
+ queued = null;
13993
+ running = null;
13994
+ held = [];
13995
+ timer = null;
13996
+ live = false;
13997
+ isClaimed(coinId) {
13998
+ return this.deps.claims.issuerOf(coinId) !== null;
13999
+ }
14000
+ standing(tokenId, coinId) {
14001
+ if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
14002
+ return this.refused.has(tokenId) ? "refused" : "pending";
14003
+ }
14004
+ trusts(tokenId, coinId) {
14005
+ return this.standing(tokenId, coinId) === "trusted";
14006
+ }
14007
+ hydrate() {
14008
+ this.hydration ??= this.load().catch((error) => {
14009
+ this.hydration = null;
14010
+ throw error;
14011
+ });
14012
+ return this.hydration;
14013
+ }
14014
+ start() {
14015
+ this.live = true;
14016
+ return this.hydrate();
14017
+ }
14018
+ stop() {
14019
+ this.live = false;
14020
+ if (this.timer !== null) clearTimeout(this.timer);
14021
+ this.timer = null;
14022
+ }
14023
+ async accept(token) {
14024
+ if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
14025
+ }
14026
+ async recordSplit(sourceTokenId, outputTokenId) {
14027
+ await this.hydrate();
14028
+ if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
14029
+ }
14030
+ review(holders) {
14031
+ this.queued = holders;
14032
+ this.running ??= this.drain();
14033
+ return this.running;
14034
+ }
14035
+ async drain() {
14036
+ try {
14037
+ while (this.queued !== null) {
14038
+ const holders = this.queued;
14039
+ this.queued = null;
14040
+ await this.reviewOnce(holders);
14041
+ }
14042
+ } finally {
14043
+ this.running = null;
14044
+ }
14045
+ }
14046
+ async reviewOnce(holders) {
14047
+ await this.hydrate();
14048
+ this.held = holders;
14049
+ const due = holders.filter((tokenId) => this.isDue(tokenId));
14050
+ const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
14051
+ for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
14052
+ if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
14053
+ await this.persist();
14054
+ this.deps.changed();
14055
+ }
14056
+ this.scheduleRetry();
14057
+ }
14058
+ isDue(tokenId) {
14059
+ if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
14060
+ return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
14061
+ }
14062
+ async check(due) {
14063
+ const verdicts = /* @__PURE__ */ new Map();
14064
+ let blobs;
14065
+ try {
14066
+ blobs = await this.deps.getBlobs([...due]);
14067
+ } catch {
14068
+ return verdicts;
14069
+ }
14070
+ for (const tokenId of due) {
14071
+ const bytes = blobs.get(tokenId);
14072
+ if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
14073
+ }
14074
+ return verdicts;
14075
+ }
14076
+ async verdictOf(tokenId, bytes) {
14077
+ let engine;
14078
+ try {
14079
+ engine = this.deps.engine();
14080
+ } catch {
14081
+ return "retry";
14082
+ }
14083
+ let token;
14084
+ try {
14085
+ token = await engine.decodeToken({ tokenId, token: bytes });
14086
+ } catch {
14087
+ return "refused";
14088
+ }
14089
+ if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
14090
+ try {
14091
+ return (await engine.verify(token)).ok ? "verified" : "refused";
14092
+ } catch {
14093
+ return "retry";
14094
+ }
14095
+ }
14096
+ settle(tokenId, verdict) {
14097
+ if (verdict === "retry") {
14098
+ this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
14099
+ return;
14100
+ }
14101
+ this.retries.delete(tokenId);
14102
+ (verdict === "verified" ? this.verified : this.refused).add(tokenId);
14103
+ }
14104
+ nextRetry(previous) {
14105
+ const attempts = (previous?.attempts ?? 0) + 1;
14106
+ const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
14107
+ return { attempts, atMs: this.now() + delay };
14108
+ }
14109
+ scheduleRetry() {
14110
+ if (!this.live || this.timer !== null) return;
14111
+ const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
14112
+ if (due.length === 0) return;
14113
+ const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
14114
+ this.timer = setTimeout(() => {
14115
+ this.timer = null;
14116
+ void this.review(this.held).catch(() => void 0);
14117
+ }, delay);
14118
+ }
14119
+ async remember(tokenId) {
14120
+ await this.hydrate();
14121
+ if (this.verified.has(tokenId)) return;
14122
+ this.verified.add(tokenId);
14123
+ this.refused.delete(tokenId);
14124
+ this.retries.delete(tokenId);
14125
+ await this.persist();
14126
+ }
14127
+ persist() {
14128
+ const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
14129
+ return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
14130
+ }
14131
+ async load() {
14132
+ const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
14133
+ if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
14134
+ for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
14135
+ }
14136
+ now() {
14137
+ return (this.deps.now ?? Date.now)();
14138
+ }
14139
+ };
14140
+
13824
14141
  // modules/payments-v2/ports.ts
13825
14142
  function isRetryableAckError(err) {
13826
14143
  return err !== null && typeof err === "object" && err.retryable === true;
@@ -13830,6 +14147,8 @@ function isRetryableAckError(err) {
13830
14147
  var ACK_BATCH_SIZE = 200;
13831
14148
  var REFRESH_INTERVAL_MS = 2500;
13832
14149
  var POLL_INTERVAL_MS = 3e4;
14150
+ var RECHECK_BASE_MS = POLL_INTERVAL_MS;
14151
+ var RECHECK_MAX_MS = 60 * 60 * 1e3;
13833
14152
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
13834
14153
  function receivedDedupKey(tokenId, stateHash) {
13835
14154
  return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
@@ -13846,6 +14165,7 @@ var Receive = class {
13846
14165
  pollTimer = null;
13847
14166
  unsubscribeWake = null;
13848
14167
  lastRefreshAt = 0;
14168
+ rechecks = /* @__PURE__ */ new Map();
13849
14169
  drainOnce() {
13850
14170
  return this.drainFlight.run(() => this.doDrain());
13851
14171
  }
@@ -13904,6 +14224,7 @@ var Receive = class {
13904
14224
  const pending = [];
13905
14225
  const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
13906
14226
  const claimable = { n: 0 };
14227
+ const deferred = { any: false };
13907
14228
  const finish = () => {
13908
14229
  if (claimable.n > 0) deps.refreshView?.();
13909
14230
  return stored;
@@ -13912,7 +14233,7 @@ var Receive = class {
13912
14233
  const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
13913
14234
  let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
13914
14235
  for (let pass = 0; pass < 2; pass++) {
13915
- await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
14236
+ await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
13916
14237
  const served = deps.delivery.incomingEpoch();
13917
14238
  if (basis === null || served === null || served === basis.syncEpoch) break;
13918
14239
  await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
@@ -13929,20 +14250,41 @@ var Receive = class {
13929
14250
  return finish();
13930
14251
  }
13931
14252
  async processEntry(ctx, entry) {
13932
- const { deps, engine, pending, stored, claimable } = ctx;
13933
- const screened = await screen(deps, engine, entry);
14253
+ const { deps, engine, stored, claimable } = ctx;
14254
+ const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
14255
+ this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
14256
+ if (screened.kind === "defer") {
14257
+ ctx.deferred.any = true;
14258
+ return;
14259
+ }
13934
14260
  if (screened.kind === "ack") {
13935
- pending.push(screened.ack);
14261
+ queueAck(ctx, screened.ack);
13936
14262
  if (screened.ack.disposition === "claimed") claimable.n += 1;
13937
14263
  return;
13938
14264
  }
14265
+ await deps.accepted?.(screened.token);
13939
14266
  await deps.view.store(screened.record);
13940
- pending.push(claimAck(entry));
14267
+ queueAck(ctx, claimAck(entry));
13941
14268
  claimable.n += 1;
13942
14269
  const transfer = await announce(deps, entry, screened.record);
13943
14270
  stored.push(transfer);
13944
14271
  deps.emit("transfer:incoming", transfer);
13945
14272
  }
14273
+ recheckDue(deliveryId) {
14274
+ const recheck = this.rechecks.get(deliveryId);
14275
+ return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
14276
+ }
14277
+ scheduleRecheck(deliveryId, deferred) {
14278
+ if (!deferred) {
14279
+ this.rechecks.delete(deliveryId);
14280
+ return;
14281
+ }
14282
+ const previous = this.rechecks.get(deliveryId);
14283
+ if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
14284
+ const attempts = (previous?.attempts ?? 0) + 1;
14285
+ const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
14286
+ this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
14287
+ }
13946
14288
  };
13947
14289
  async function screen(deps, engine, entry) {
13948
14290
  const blobBytes = await entry.fetchBlob();
@@ -13953,7 +14295,8 @@ async function screen(deps, engine, entry) {
13953
14295
  logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
13954
14296
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
13955
14297
  }
13956
- const verdict = await engine.verify(token);
14298
+ const verdict = await verdictOf(engine, token);
14299
+ if (verdict === null) return { kind: "defer" };
13957
14300
  if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
13958
14301
  if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
13959
14302
  return { kind: "ack", ack: rejectAck(entry, "not-owned") };
@@ -13964,38 +14307,43 @@ async function screen(deps, engine, entry) {
13964
14307
  if (held !== null && await engine.isSpent(token)) {
13965
14308
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
13966
14309
  }
14310
+ const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
13967
14311
  return {
13968
14312
  kind: "accept",
14313
+ token,
13969
14314
  record: {
13970
14315
  tokenId: keys.tokenId,
13971
14316
  stateHash: keys.stateHash,
13972
14317
  assets: toAssetAmounts(token),
13973
- ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
14318
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
14319
+ ...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
13974
14320
  }
13975
14321
  };
13976
14322
  }
13977
- async function announce(deps, entry, record) {
13978
- const receivedAt = (deps.now ?? Date.now)();
14323
+ function unverifiedCoinsOf(deps, token) {
14324
+ const claims = deps.claims;
14325
+ if (claims === void 0 || honoursClaims(claims, token)) return [];
14326
+ return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
14327
+ }
14328
+ async function verdictOf(engine, token) {
13979
14329
  try {
13980
- await deps.recordReceived({
13981
- dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
13982
- tokenId: record.tokenId,
13983
- stateHash: record.stateHash,
13984
- assets: record.assets,
13985
- ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
13986
- ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
13987
- ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13988
- ...entry.memo !== void 0 ? { memo: entry.memo } : {},
13989
- receivedAt
13990
- });
14330
+ return await engine.verify(token);
13991
14331
  } catch (err) {
13992
- logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
14332
+ if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
14333
+ throw err;
13993
14334
  }
14335
+ }
14336
+ async function announce(deps, entry, record) {
14337
+ const receivedAt = (deps.now ?? Date.now)();
14338
+ await recordArrival(deps, entry, record, receivedAt);
14339
+ const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
14340
+ const counted = record.assets.filter((asset) => !refused2.includes(asset));
13994
14341
  return {
13995
14342
  id: record.tokenId,
13996
14343
  senderPubkey: entry.senderPubkey ?? "",
13997
14344
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
13998
- tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
14345
+ tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
14346
+ ...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
13999
14347
  // #777: named here rather than mapped from assets, which announced an EMPTY list.
14000
14348
  ...record.tokenType !== void 0 ? {
14001
14349
  coinless: [
@@ -14013,6 +14361,25 @@ async function announce(deps, entry, record) {
14013
14361
  receivedAt
14014
14362
  };
14015
14363
  }
14364
+ async function recordArrival(deps, entry, record, receivedAt) {
14365
+ const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
14366
+ if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
14367
+ try {
14368
+ await deps.recordReceived({
14369
+ dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
14370
+ tokenId: record.tokenId,
14371
+ stateHash: record.stateHash,
14372
+ assets,
14373
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
14374
+ ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
14375
+ ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
14376
+ ...entry.memo !== void 0 ? { memo: entry.memo } : {},
14377
+ receivedAt
14378
+ });
14379
+ } catch (err) {
14380
+ logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
14381
+ }
14382
+ }
14016
14383
  async function flushAcks(deps, pending, epochOf) {
14017
14384
  if (pending.length === 0) return { progressed: true };
14018
14385
  const before = pending.length;
@@ -14020,7 +14387,7 @@ async function flushAcks(deps, pending, epochOf) {
14020
14387
  try {
14021
14388
  const settled = await settleAcks(deps, pending);
14022
14389
  let i = 0;
14023
- while (i < pending.length && settled.has(pending[i].deliveryId)) {
14390
+ while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
14024
14391
  lastAcked = pending[i].cursor;
14025
14392
  i += 1;
14026
14393
  }
@@ -14096,6 +14463,9 @@ async function ackOne(deps, ack) {
14096
14463
  await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
14097
14464
  }
14098
14465
  }
14466
+ function queueAck(ctx, ack) {
14467
+ ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
14468
+ }
14099
14469
  function claimAck(entry) {
14100
14470
  return {
14101
14471
  deliveryId: entry.deliveryId,
@@ -14804,72 +15174,276 @@ var SpendQueue = class {
14804
15174
  queue = [];
14805
15175
  this.queues.set(coinId, queue);
14806
15176
  }
14807
- queue.push(entry);
14808
- return settled;
15177
+ queue.push(entry);
15178
+ return settled;
15179
+ }
15180
+ expire(id, coinId) {
15181
+ const entries = this.queues.get(coinId);
15182
+ if (!entries) return;
15183
+ const index = entries.findIndex((e) => e.id === id);
15184
+ if (index === -1) return;
15185
+ const [entry] = entries.splice(index, 1);
15186
+ entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
15187
+ if (entries.length === 0) this.queues.delete(coinId);
15188
+ }
15189
+ // Whole-token semantics: only fully-free tokens enter the selector pool. What
15190
+ // the reservations hold back is counted too — the refusal has to name it (#737).
15191
+ freeView(coinId) {
15192
+ const freeView = [];
15193
+ let freeTotal = 0n;
15194
+ let pinnedTotal = 0n;
15195
+ const holders = /* @__PURE__ */ new Set();
15196
+ const unproven = this.deps.ledger.unprovenReason();
15197
+ for (const entry of this.deps.getPool(coinId)) {
15198
+ if (entry.amount <= 0n) continue;
15199
+ const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
15200
+ if (holder === void 0) {
15201
+ freeView.push(entry);
15202
+ freeTotal += entry.amount;
15203
+ } else {
15204
+ pinnedTotal += entry.amount;
15205
+ holders.add(holder);
15206
+ }
15207
+ }
15208
+ return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
15209
+ }
15210
+ expectedChangeTotal(coinId) {
15211
+ let total = 0n;
15212
+ for (const change of this.expectedChange.values()) {
15213
+ if (change.coinId === coinId) total += change.amount;
15214
+ }
15215
+ return total;
15216
+ }
15217
+ reservePlan(reservationId, plan, freeView) {
15218
+ const pool = new Set(freeView.map((e) => e.tokenId));
15219
+ const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
15220
+ for (const tokenId of tokenIds) {
15221
+ if (!pool.has(tokenId)) {
15222
+ throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
15223
+ }
15224
+ }
15225
+ this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
15226
+ }
15227
+ totalQueued() {
15228
+ let total = 0;
15229
+ for (const entries of this.queues.values()) total += entries.length;
15230
+ return total;
15231
+ }
15232
+ };
15233
+ function parseAmount(raw) {
15234
+ if (!/^\d+$/.test(raw)) {
15235
+ throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
15236
+ }
15237
+ const amount = BigInt(raw);
15238
+ if (amount <= 0n) {
15239
+ throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
15240
+ }
15241
+ return amount;
15242
+ }
15243
+
15244
+ // modules/payments-v2/machine/payload.ts
15245
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
15246
+ return {
15247
+ v: 2,
15248
+ kind: "coin",
15249
+ recipient: recipientPubkey,
15250
+ coinId: request.coinId,
15251
+ amount: request.amount,
15252
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
15253
+ direct: [...spend.plan.direct],
15254
+ ...spend.plan.split !== void 0 ? {
15255
+ split: {
15256
+ tokenId: spend.plan.split.tokenId,
15257
+ splitAmount: spend.plan.split.splitAmount.toString(),
15258
+ remainderAmount: spend.plan.split.remainderAmount.toString()
15259
+ }
15260
+ } : {},
15261
+ spentStates
15262
+ };
15263
+ }
15264
+ function messageOf(err) {
15265
+ return err instanceof Error ? err.message : String(err);
15266
+ }
15267
+ function buildWholePayload(recipientPubkey, request, spentStates) {
15268
+ return {
15269
+ v: 2,
15270
+ kind: "whole",
15271
+ recipient: recipientPubkey,
15272
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
15273
+ direct: [request.tokenId],
15274
+ spentStates
15275
+ };
15276
+ }
15277
+
15278
+ // modules/payments-v2/burn.ts
15279
+ var HEX32 = /^[0-9a-f]{64}$/;
15280
+ async function heldToken2(deps, tokenId) {
15281
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
15282
+ if (bytes === void 0) {
15283
+ throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
15284
+ }
15285
+ return deps.engine.decodeToken({ tokenId, token: bytes });
15286
+ }
15287
+ function assertBurnable(engine, token, tokenId) {
15288
+ if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
15289
+ throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
15290
+ }
15291
+ if (token.valueEnvelope === "bare_collection") {
15292
+ throw new SphereError(
15293
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
15294
+ "VALIDATION_ERROR"
15295
+ );
15296
+ }
15297
+ }
15298
+ function assetsOf(token) {
15299
+ return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
15300
+ }
15301
+ async function runBurnUnderJournal(deps, input) {
15302
+ const { burnId } = input;
15303
+ const { tokenId, reasonBytes } = input.request;
15304
+ if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
15305
+ return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
15306
+ }
15307
+ let token;
15308
+ try {
15309
+ token = await heldToken2(deps, tokenId);
15310
+ assertBurnable(deps.engine, token, tokenId);
15311
+ } catch (err) {
15312
+ return refused(burnId, tokenId, err);
15313
+ }
15314
+ const entry = {
15315
+ burnId,
15316
+ tokenId,
15317
+ reasonHex: bytesToHex3(reasonBytes),
15318
+ burnedTokenHex: null,
15319
+ settled: false,
15320
+ assets: assetsOf(token),
15321
+ createdAt: deps.now()
15322
+ };
15323
+ await deps.burnJournal.upsert(entry);
15324
+ try {
15325
+ const settled = await burnJournaled(deps, entry, token);
15326
+ return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
15327
+ } catch (err) {
15328
+ const failure = failureOf(err);
15329
+ if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
15330
+ else deps.armHeartbeat();
15331
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
15332
+ }
15333
+ }
15334
+ async function replayBurns(deps) {
15335
+ let progressed = 0;
15336
+ for (const entry of await deps.burnJournal.list()) {
15337
+ if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
15338
+ try {
15339
+ const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
15340
+ await burnJournaled(deps, entry, token);
15341
+ progressed += 1;
15342
+ } catch (err) {
15343
+ if (!isTerminal(err)) continue;
15344
+ await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
15345
+ progressed += 1;
15346
+ }
15347
+ }
15348
+ return progressed;
15349
+ }
15350
+ async function pendingBurns(deps) {
15351
+ return (await deps.burnJournal.list()).map((entry) => ({
15352
+ burnId: entry.burnId,
15353
+ tokenId: entry.tokenId,
15354
+ reasonBytes: hexToBytes4(entry.reasonHex),
15355
+ burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
15356
+ settled: entry.settled,
15357
+ createdAt: entry.createdAt,
15358
+ ...entry.failure !== void 0 ? { failure: entry.failure } : {}
15359
+ }));
15360
+ }
15361
+ async function acknowledgeBurn(deps, burnId) {
15362
+ const entry = await deps.burnJournal.getByKey(burnId);
15363
+ if (entry === void 0) return;
15364
+ if (isLiveBurn(entry)) {
15365
+ throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
15366
+ }
15367
+ await deps.burnJournal.removeByKey(burnId);
15368
+ }
15369
+ function refused(burnId, tokenId, err) {
15370
+ const failure = failureOf(err);
15371
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
15372
+ }
15373
+ function isTerminal(err) {
15374
+ return classifyError(err) === "conflict";
15375
+ }
15376
+ function failureOf(err) {
15377
+ const code = err !== null && typeof err === "object" ? err.code : void 0;
15378
+ return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
15379
+ }
15380
+ async function burnJournaled(deps, entry, token) {
15381
+ let current = entry;
15382
+ if (current.burnedTokenHex === null) {
15383
+ if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
15384
+ const burned = await deps.engine.burn(
15385
+ { token, reasonBytes: hexToBytes4(current.reasonHex) },
15386
+ { transferId: current.burnId }
15387
+ );
15388
+ if (burned.blob.tokenId !== current.tokenId) {
15389
+ throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
15390
+ }
15391
+ current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
15392
+ await deps.burnJournal.upsert(current);
14809
15393
  }
14810
- expire(id, coinId) {
14811
- const entries = this.queues.get(coinId);
14812
- if (!entries) return;
14813
- const index = entries.findIndex((e) => e.id === id);
14814
- if (index === -1) return;
14815
- const [entry] = entries.splice(index, 1);
14816
- entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
14817
- if (entries.length === 0) this.queues.delete(coinId);
15394
+ await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
15395
+ await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
15396
+ current = { ...current, settled: true };
15397
+ await deps.burnJournal.upsert(current);
15398
+ deps.refreshView();
15399
+ return current;
15400
+ }
15401
+
15402
+ // modules/payments-v2/burn-hold.ts
15403
+ var BurnHold = class {
15404
+ constructor(deps) {
15405
+ this.deps = deps;
14818
15406
  }
14819
- // Whole-token semantics: only fully-free tokens enter the selector pool. What
14820
- // the reservations hold back is counted too — the refusal has to name it (#737).
14821
- freeView(coinId) {
14822
- const freeView = [];
14823
- let freeTotal = 0n;
14824
- let pinnedTotal = 0n;
14825
- const holders = /* @__PURE__ */ new Set();
14826
- const unproven = this.deps.ledger.unprovenReason();
14827
- for (const entry of this.deps.getPool(coinId)) {
14828
- if (entry.amount <= 0n) continue;
14829
- const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
14830
- if (holder === void 0) {
14831
- freeView.push(entry);
14832
- freeTotal += entry.amount;
14833
- } else {
14834
- pinnedTotal += entry.amount;
14835
- holders.add(holder);
14836
- }
15407
+ async run(burnId, tokenId, burn) {
15408
+ try {
15409
+ this.deps.queue.planWhole(burnId, tokenId);
15410
+ } catch (err) {
15411
+ return refused(burnId, tokenId, err);
14837
15412
  }
14838
- return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
14839
- }
14840
- expectedChangeTotal(coinId) {
14841
- let total = 0n;
14842
- for (const change of this.expectedChange.values()) {
14843
- if (change.coinId === coinId) total += change.amount;
15413
+ this.deps.view.markInFlightMany([tokenId]);
15414
+ let burned = false;
15415
+ try {
15416
+ const result = await burn();
15417
+ burned = result.success;
15418
+ return result;
15419
+ } finally {
15420
+ await this.settle(burnId, tokenId, burned);
14844
15421
  }
14845
- return total;
14846
15422
  }
14847
- reservePlan(reservationId, plan, freeView) {
14848
- const pool = new Set(freeView.map((e) => e.tokenId));
14849
- const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
14850
- for (const tokenId of tokenIds) {
14851
- if (!pool.has(tokenId)) {
14852
- throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
14853
- }
15423
+ async settle(burnId, tokenId, burned) {
15424
+ if (burned) {
15425
+ this.deps.ledger.commit(burnId);
15426
+ this.deps.track(this.refreshThenRelease(tokenId));
15427
+ return;
14854
15428
  }
14855
- this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
15429
+ const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
15430
+ if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
15431
+ this.deps.ledger.cancel(burnId);
15432
+ this.deps.view.releaseMany([tokenId]);
15433
+ this.deps.queue.notifyChange("");
15434
+ return;
15435
+ }
15436
+ this.deps.pins.adopt(burnId);
14856
15437
  }
14857
- totalQueued() {
14858
- let total = 0;
14859
- for (const entries of this.queues.values()) total += entries.length;
14860
- return total;
15438
+ async refreshThenRelease(tokenId) {
15439
+ try {
15440
+ await this.deps.view.delta();
15441
+ this.deps.view.releaseMany([tokenId]);
15442
+ } finally {
15443
+ this.deps.queue.notifyChange("");
15444
+ }
14861
15445
  }
14862
15446
  };
14863
- function parseAmount(raw) {
14864
- if (!/^\d+$/.test(raw)) {
14865
- throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
14866
- }
14867
- const amount = BigInt(raw);
14868
- if (amount <= 0n) {
14869
- throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
14870
- }
14871
- return amount;
14872
- }
14873
15447
 
14874
15448
  // modules/payments-v2/compose.ts
14875
15449
  function supportsDeterministicMint(engine) {
@@ -14878,16 +15452,8 @@ function supportsDeterministicMint(engine) {
14878
15452
  function composeFacadeParts(deps, hooks) {
14879
15453
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
14880
15454
  const ledger = new ReservationLedger();
14881
- const view = new InventoryView({
14882
- port: deps.storagePort,
14883
- kv: deps.kv,
14884
- emit: (event) => deps.emit(event, {}),
14885
- // #737: a reserved token is unselectable, so it is never confirmed balance.
14886
- // #738: while the held-set is unproven, EVERY token reads pinned — the report
14887
- // must not call a token spendable that the queue is about to refuse to spend.
14888
- isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
14889
- ...deps.now !== void 0 ? { now: deps.now } : {}
14890
- });
15455
+ const verdicts = buildVerdicts(deps, hooks);
15456
+ const view = buildView(deps, hooks, ledger, verdicts);
14891
15457
  const queue = new SpendQueue({
14892
15458
  ledger,
14893
15459
  getPool: (coinId) => view.pool(coinId),
@@ -14902,27 +15468,57 @@ function composeFacadeParts(deps, hooks) {
14902
15468
  ...deps.now !== void 0 ? { now: deps.now } : {},
14903
15469
  ...deps.newId !== void 0 ? { newId: deps.newId } : {}
14904
15470
  });
14905
- const machineDeps = buildMachineDeps(deps, hooks, historyStore);
15471
+ const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
14906
15472
  const machineStores = createMachineStores(deps.kv);
14907
15473
  const heldStates = /* @__PURE__ */ new Map();
14908
15474
  const refreshView = coalesced(() => view.delta(), hooks.track);
15475
+ const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
14909
15476
  return {
14910
15477
  ownPubkeyBytes,
14911
15478
  view,
15479
+ verdicts,
14912
15480
  refreshView,
14913
15481
  ledger,
14914
- pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
15482
+ pins,
14915
15483
  queue,
15484
+ burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
14916
15485
  historyStore,
14917
15486
  machineStores,
14918
15487
  machineDeps,
14919
15488
  machine: new TransferMachine(machineDeps),
14920
15489
  heldStates,
14921
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
15490
+ receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
14922
15491
  requests: buildRequests(deps, hooks),
14923
15492
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
14924
15493
  };
14925
15494
  }
15495
+ function buildView(deps, hooks, ledger, verdicts) {
15496
+ const view = new InventoryView({
15497
+ port: deps.storagePort,
15498
+ kv: deps.kv,
15499
+ trust: verdicts,
15500
+ emit: (event) => {
15501
+ deps.emit(event, {});
15502
+ hooks.track(verdicts.review(view.claimHolders()));
15503
+ },
15504
+ // #737: a reserved token is unselectable, so it is never confirmed balance.
15505
+ // #738: while the held-set is unproven, EVERY token reads pinned — the report
15506
+ // must not call a token spendable that the queue is about to refuse to spend.
15507
+ isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
15508
+ ...deps.now !== void 0 ? { now: deps.now } : {}
15509
+ });
15510
+ return view;
15511
+ }
15512
+ function buildVerdicts(deps, hooks) {
15513
+ return new TokenVerdicts({
15514
+ kv: deps.kv,
15515
+ claims: deps.claims ?? new CoinClaims(),
15516
+ engine: () => hooks.engine(),
15517
+ getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
15518
+ changed: () => deps.emit("inventory:updated", {}),
15519
+ ...deps.now !== void 0 ? { now: deps.now } : {}
15520
+ });
15521
+ }
14926
15522
  function buildPins(deps, hooks, parts) {
14927
15523
  return new IntentPins({
14928
15524
  ledger: parts.ledger,
@@ -14950,8 +15546,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
14950
15546
  }
14951
15547
  };
14952
15548
  }
14953
- function buildMachineDeps(deps, hooks, historyStore) {
15549
+ function buildMachineDeps(deps, hooks, historyStore, verdicts) {
14954
15550
  return {
15551
+ recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
14955
15552
  engine: () => hooks.engine(),
14956
15553
  storage: deps.storagePort,
14957
15554
  delivery: deps.deliveryPort,
@@ -14984,10 +15581,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
14984
15581
  }
14985
15582
  };
14986
15583
  }
14987
- function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
15584
+ function buildReceive(deps, hooks, parts, refreshView) {
15585
+ const { historyStore, heldStates, verdicts } = parts;
14988
15586
  return new Receive({
14989
15587
  delivery: deps.deliveryPort,
14990
15588
  engine: () => hooks.engine(),
15589
+ claims: deps.claims ?? new CoinClaims(),
15590
+ accepted: (token) => verdicts.accept(token),
14991
15591
  view: {
14992
15592
  heldState: (tokenId) => heldStates.get(tokenId) ?? null,
14993
15593
  store: async (entry) => {
@@ -15037,40 +15637,6 @@ function buildRequests(deps, hooks) {
15037
15637
  });
15038
15638
  }
15039
15639
 
15040
- // modules/payments-v2/machine/payload.ts
15041
- function buildPayload(recipientPubkey, request, spend, spentStates) {
15042
- return {
15043
- v: 2,
15044
- kind: "coin",
15045
- recipient: recipientPubkey,
15046
- coinId: request.coinId,
15047
- amount: request.amount,
15048
- ...request.memo !== void 0 ? { memo: request.memo } : {},
15049
- direct: [...spend.plan.direct],
15050
- ...spend.plan.split !== void 0 ? {
15051
- split: {
15052
- tokenId: spend.plan.split.tokenId,
15053
- splitAmount: spend.plan.split.splitAmount.toString(),
15054
- remainderAmount: spend.plan.split.remainderAmount.toString()
15055
- }
15056
- } : {},
15057
- spentStates
15058
- };
15059
- }
15060
- function messageOf(err) {
15061
- return err instanceof Error ? err.message : String(err);
15062
- }
15063
- function buildWholePayload(recipientPubkey, request, spentStates) {
15064
- return {
15065
- v: 2,
15066
- kind: "whole",
15067
- recipient: recipientPubkey,
15068
- ...request.memo !== void 0 ? { memo: request.memo } : {},
15069
- direct: [request.tokenId],
15070
- spentStates
15071
- };
15072
- }
15073
-
15074
15640
  // modules/payments-v2/mint-params.ts
15075
15641
  function mintParams(recipientPubkey, coinId, amount) {
15076
15642
  return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
@@ -15140,6 +15706,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
15140
15706
  }
15141
15707
  return false;
15142
15708
  }
15709
+ async function seedHeldStates(storagePort, heldStates) {
15710
+ let page = await storagePort.listInventory();
15711
+ for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
15712
+ for (const item of page.items) {
15713
+ if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
15714
+ }
15715
+ if (!page.more) return;
15716
+ page = await storagePort.listInventory(page.cursor);
15717
+ }
15718
+ }
15143
15719
  async function replayCoinMints(deps) {
15144
15720
  let resolved = 0;
15145
15721
  for (const entry of await deps.mintJournal.list()) {
@@ -15244,7 +15820,8 @@ async function mintJournaled(deps, entry) {
15244
15820
  }
15245
15821
 
15246
15822
  // modules/payments-v2/mint-custom.ts
15247
- var HEX32 = /^[0-9a-f]{64}$/;
15823
+ var HEX322 = /^[0-9a-f]{64}$/;
15824
+ var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
15248
15825
  function assertRequest(request) {
15249
15826
  if (request.tokenType.length !== 32) {
15250
15827
  throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
@@ -15255,19 +15832,27 @@ function assertRequest(request) {
15255
15832
  if (request.data.length === 0) {
15256
15833
  throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
15257
15834
  }
15835
+ if (request.justification !== void 0 && request.justification.length === 0) {
15836
+ throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
15837
+ }
15838
+ if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
15839
+ throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
15840
+ }
15258
15841
  for (const asset of request.assets) {
15259
- if (!HEX32.test(asset.coinId) || asset.amount <= 0n) {
15842
+ if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
15260
15843
  throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
15261
15844
  }
15262
15845
  }
15263
15846
  }
15264
15847
  async function runCustomMintUnderJournal(deps, input) {
15848
+ let entry;
15265
15849
  try {
15266
15850
  assertRequest(input.request);
15851
+ entry = journalEntryOf2(input.mintId, input.request, deps.now());
15852
+ deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
15267
15853
  } catch (err) {
15268
15854
  return { success: false, error: messageOf(err) };
15269
15855
  }
15270
- const entry = journalEntryOf2(input.mintId, input.request, deps.now());
15271
15856
  await deps.customMintJournal.upsert(entry);
15272
15857
  let tokenId;
15273
15858
  try {
@@ -15307,24 +15892,25 @@ function journalEntryOf2(mintId, request, createdAt) {
15307
15892
  createdAt
15308
15893
  };
15309
15894
  }
15895
+ function mintParamsOf(deps, entry, mintJustificationVerifiers) {
15896
+ return {
15897
+ recipientPubkey: deps.ownPubkeyBytes,
15898
+ data: hexToBytes4(entry.dataHex),
15899
+ tokenType: hexToBytes4(entry.tokenTypeHex),
15900
+ salt: hexToBytes4(entry.saltHex),
15901
+ ...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
15902
+ ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
15903
+ };
15904
+ }
15310
15905
  async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
15311
- const token = await deps.engine.mintDataToken(
15312
- {
15313
- recipientPubkey: deps.ownPubkeyBytes,
15314
- data: hexToBytes4(entry.dataHex),
15315
- tokenType: hexToBytes4(entry.tokenTypeHex),
15316
- salt: hexToBytes4(entry.saltHex),
15317
- ...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
15318
- ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
15319
- },
15320
- { transferId: entry.mintId }
15321
- );
15906
+ const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
15322
15907
  if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
15323
15908
  throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
15324
15909
  }
15325
15910
  if (entry.tokenId === "") {
15326
15911
  await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
15327
15912
  }
15913
+ if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
15328
15914
  await finalizeMint(deps, {
15329
15915
  mintId: entry.mintId,
15330
15916
  token,
@@ -15334,108 +15920,6 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
15334
15920
  return token.blob.tokenId;
15335
15921
  }
15336
15922
 
15337
- // modules/payments-v2/burn.ts
15338
- var HEX322 = /^[0-9a-f]{64}$/;
15339
- async function heldToken2(deps, tokenId) {
15340
- const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
15341
- if (bytes === void 0) {
15342
- throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
15343
- }
15344
- return deps.engine.decodeToken({ tokenId, token: bytes });
15345
- }
15346
- function assetsOf(token) {
15347
- return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
15348
- }
15349
- async function runBurnUnderJournal(deps, input) {
15350
- const { burnId } = input;
15351
- const { tokenId, reasonBytes } = input.request;
15352
- if (!HEX322.test(tokenId) || reasonBytes.length === 0) {
15353
- return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty" };
15354
- }
15355
- let token;
15356
- try {
15357
- token = await heldToken2(deps, tokenId);
15358
- if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
15359
- throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
15360
- }
15361
- } catch (err) {
15362
- return { success: false, burnId, tokenId, error: messageOf(err) };
15363
- }
15364
- const entry = {
15365
- burnId,
15366
- tokenId,
15367
- reasonHex: bytesToHex3(reasonBytes),
15368
- burnedTokenHex: null,
15369
- settled: false,
15370
- assets: assetsOf(token),
15371
- createdAt: deps.now()
15372
- };
15373
- await deps.burnJournal.upsert(entry);
15374
- try {
15375
- const settled = await burnJournaled(deps, entry, token, { applied: false });
15376
- return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
15377
- } catch (err) {
15378
- deps.armHeartbeat();
15379
- return { success: false, burnId, tokenId, error: messageOf(err) };
15380
- }
15381
- }
15382
- async function replayBurns(deps) {
15383
- let progressed = 0;
15384
- for (const entry of await deps.burnJournal.list()) {
15385
- if (entry.settled || deps.isActiveOp(entry.burnId)) continue;
15386
- try {
15387
- const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
15388
- const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
15389
- await burnJournaled(deps, entry, token, { applied });
15390
- progressed += 1;
15391
- } catch {
15392
- continue;
15393
- }
15394
- }
15395
- return progressed;
15396
- }
15397
- async function pendingBurns(deps) {
15398
- return (await deps.burnJournal.list()).map((entry) => ({
15399
- burnId: entry.burnId,
15400
- tokenId: entry.tokenId,
15401
- reasonBytes: hexToBytes4(entry.reasonHex),
15402
- burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
15403
- settled: entry.settled,
15404
- createdAt: entry.createdAt
15405
- }));
15406
- }
15407
- async function acknowledgeBurn(deps, burnId) {
15408
- const entry = await deps.burnJournal.getByKey(burnId);
15409
- if (entry === void 0) return;
15410
- if (!entry.settled) {
15411
- throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
15412
- }
15413
- await deps.burnJournal.removeByKey(burnId);
15414
- }
15415
- async function burnJournaled(deps, entry, token, state) {
15416
- let current = entry;
15417
- if (current.burnedTokenHex === null) {
15418
- if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
15419
- const burned = await deps.engine.burn(
15420
- { token, reasonBytes: hexToBytes4(current.reasonHex) },
15421
- { transferId: current.burnId }
15422
- );
15423
- if (burned.blob.tokenId !== current.tokenId) {
15424
- throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
15425
- }
15426
- current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
15427
- await deps.burnJournal.upsert(current);
15428
- }
15429
- if (!state.applied) {
15430
- await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [] });
15431
- }
15432
- await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
15433
- current = { ...current, settled: true };
15434
- await deps.burnJournal.upsert(current);
15435
- deps.refreshView();
15436
- return current;
15437
- }
15438
-
15439
15923
  // modules/payments-v2/send-whole.ts
15440
15924
  async function materializeWholeSpend(deps, input) {
15441
15925
  const { engine } = deps;
@@ -15631,6 +16115,7 @@ var PaymentsFacade = class {
15631
16115
  track: (op) => this.trackTail(op)
15632
16116
  });
15633
16117
  this.view = parts.view;
16118
+ this.verdicts = parts.verdicts;
15634
16119
  this.wakeRefresh = parts.refreshView;
15635
16120
  this.ledger = parts.ledger;
15636
16121
  this.queue = parts.queue;
@@ -15644,6 +16129,7 @@ var PaymentsFacade = class {
15644
16129
  this.restoreDeps = parts.restoreDeps;
15645
16130
  this.requests = parts.requests;
15646
16131
  this.pins = parts.pins;
16132
+ this.burnHold = parts.burnHold;
15647
16133
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
15648
16134
  this.heartbeat = new ConvergenceHeartbeat({
15649
16135
  now: () => this.nowMs(),
@@ -15667,6 +16153,7 @@ var PaymentsFacade = class {
15667
16153
  wakeRefresh;
15668
16154
  prewarmed = new PrewarmCache();
15669
16155
  view;
16156
+ verdicts;
15670
16157
  ledger;
15671
16158
  queue;
15672
16159
  machine;
@@ -15693,6 +16180,7 @@ var PaymentsFacade = class {
15693
16180
  activeMoneyOps = /* @__PURE__ */ new Set();
15694
16181
  /** #737: the ledger holds exactly the sources of the still-open intents. */
15695
16182
  pins;
16183
+ burnHold;
15696
16184
  /** Per facade, so per address: an NFT reading is never served to another wallet. */
15697
16185
  nftCache = new NftCache();
15698
16186
  // ── lifecycle ──────────────────────────────────────────────────────────────
@@ -15717,7 +16205,8 @@ var PaymentsFacade = class {
15717
16205
  await this.deps.session.start();
15718
16206
  await this.pins.sync().catch(() => void 0);
15719
16207
  this.trackTail(this.runConvergencePass());
15720
- this.trackTail(this.seedHeldStates());
16208
+ this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
16209
+ await this.verdicts.start().catch(() => void 0);
15721
16210
  await this.view.fullPull().catch(() => void 0);
15722
16211
  this.receiveLoop.start(this.deps.receivePollMs);
15723
16212
  }
@@ -15726,6 +16215,7 @@ var PaymentsFacade = class {
15726
16215
  this.started = false;
15727
16216
  this.heartbeat.stop();
15728
16217
  this.receiveLoop.stop();
16218
+ this.verdicts.stop();
15729
16219
  for (const unsubscribe of this.unsubscribers) unsubscribe();
15730
16220
  this.unsubscribers = [];
15731
16221
  await this.deps.session.stop();
@@ -15742,12 +16232,17 @@ var PaymentsFacade = class {
15742
16232
  }
15743
16233
  // ── reads ──────────────────────────────────────────────────────────────────
15744
16234
  async assets(coinId) {
16235
+ return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
16236
+ }
16237
+ unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
16238
+ async heldAssets(coinId) {
15745
16239
  const all = await this.view.assets(this.deps.registry, this.deps.price);
15746
16240
  return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
15747
16241
  }
15748
16242
  tokens(filter) {
15749
- return this.view.tokens(this.deps.registry, filter);
16243
+ return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
15750
16244
  }
16245
+ unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
15751
16246
  coinless() {
15752
16247
  return this.view.coinless(this.deps.registry);
15753
16248
  }
@@ -15808,9 +16303,9 @@ var PaymentsFacade = class {
15808
16303
  return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
15809
16304
  }
15810
16305
  mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
15811
- burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
15812
- pendingBurns = () => this.track(pendingBurns(this.mintDeps()));
15813
- acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.mintDeps(), burnId));
16306
+ burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
16307
+ pendingBurns = () => this.track(pendingBurns(this.machineStores));
16308
+ acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
15814
16309
  // §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
15815
16310
  async resumeNow() {
15816
16311
  if (!this.started) return;
@@ -15853,7 +16348,8 @@ var PaymentsFacade = class {
15853
16348
  isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
15854
16349
  tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
15855
16350
  emit: this.deps.emit,
15856
- now: () => this.nowMs()
16351
+ now: () => this.nowMs(),
16352
+ accepted: (token) => this.verdicts.accept(token)
15857
16353
  };
15858
16354
  }
15859
16355
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
@@ -16191,16 +16687,6 @@ var PaymentsFacade = class {
16191
16687
  newId() {
16192
16688
  return (this.deps.newId ?? randomUUID)();
16193
16689
  }
16194
- async seedHeldStates() {
16195
- let page = await this.deps.storagePort.listInventory();
16196
- for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
16197
- for (const item of page.items) {
16198
- if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
16199
- }
16200
- if (!page.more) return;
16201
- page = await this.deps.storagePort.listInventory(page.cursor);
16202
- }
16203
- }
16204
16690
  track(op) {
16205
16691
  const settled = op.then(() => void 0, () => void 0);
16206
16692
  this.pendingOps.add(settled);
@@ -17444,6 +17930,7 @@ function composePaymentsV2(spec) {
17444
17930
  engineRef: spec.engineRef,
17445
17931
  kv,
17446
17932
  registry: host.registry,
17933
+ claims: CoinClaims.fromPlugins(spec.plugins),
17447
17934
  ...host.price !== null ? { price: host.price } : {},
17448
17935
  emit: host.emit,
17449
17936
  resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
@@ -20492,7 +20979,8 @@ var Sphere = class _Sphere {
20492
20979
  // deliver the NEXT address's Unicity ID — a name the sending key does
20493
20980
  // not own (sphere#487 review).
20494
20981
  nametag: () => this.getNametagForAddress(facadeAddressId)
20495
- }
20982
+ },
20983
+ ...this._plugins ? { plugins: this._plugins } : {}
20496
20984
  });
20497
20985
  this._paymentsV2Active = { index, facade };
20498
20986
  await facade.start();
@@ -21127,6 +21615,7 @@ function addressesMatch(a, b) {
21127
21615
  export {
21128
21616
  AUTH_CHALLENGE_PREFIX,
21129
21617
  COIN_TYPES,
21618
+ CUSTOM_MINT_MAX_PAYLOAD_BYTES,
21130
21619
  ChallengeTemplateError,
21131
21620
  CoinGeckoPriceProvider,
21132
21621
  CommunicationsModule,