@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.cjs CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AUTH_CHALLENGE_PREFIX: () => AUTH_CHALLENGE_PREFIX,
34
34
  COIN_TYPES: () => COIN_TYPES,
35
+ CUSTOM_MINT_MAX_PAYLOAD_BYTES: () => CUSTOM_MINT_MAX_PAYLOAD_BYTES,
35
36
  ChallengeTemplateError: () => ChallengeTemplateError,
36
37
  CoinGeckoPriceProvider: () => CoinGeckoPriceProvider,
37
38
  CommunicationsModule: () => CommunicationsModule,
@@ -9668,6 +9669,150 @@ async function deriveDirectAddress(publicKey) {
9668
9669
  return `DIRECT://${import_HexConverter.HexConverter.encode(imprint)}${import_HexConverter.HexConverter.encode(checksum)}`;
9669
9670
  }
9670
9671
 
9672
+ // token-engine/claims.ts
9673
+ var CoinClaims = class _CoinClaims {
9674
+ issuers = /* @__PURE__ */ new Map();
9675
+ policies = [];
9676
+ static fromPlugins(plugins) {
9677
+ const claims = new _CoinClaims();
9678
+ for (const plugin of plugins ?? []) {
9679
+ for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
9680
+ }
9681
+ return claims;
9682
+ }
9683
+ add(policy) {
9684
+ const tokenType = import_HexConverter.HexConverter.encode(policy.tokenType.bytes);
9685
+ const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
9686
+ const taken = coinIds.find((coinId) => this.issuers.has(coinId));
9687
+ if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
9688
+ for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
9689
+ this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
9690
+ }
9691
+ fingerprint() {
9692
+ return [...this.policies].sort().join("\n");
9693
+ }
9694
+ issuerOf(coinId) {
9695
+ return this.issuers.get(coinId.toLowerCase()) ?? null;
9696
+ }
9697
+ };
9698
+
9699
+ // token-engine/errors.ts
9700
+ var TransferConflictError = class extends SphereError {
9701
+ /**
9702
+ * #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
9703
+ * Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
9704
+ */
9705
+ conflictedSourceId;
9706
+ constructor(message, cause) {
9707
+ super(message, "TRANSFER_CONFLICT", cause);
9708
+ this.name = "TransferConflictError";
9709
+ }
9710
+ };
9711
+ var MintReasonUnverifiableError = class extends SphereError {
9712
+ constructor(message, cause) {
9713
+ super(message, "MINT_REASON_UNVERIFIABLE", cause);
9714
+ this.name = "MintReasonUnverifiableError";
9715
+ }
9716
+ };
9717
+ var ProofUnconfirmedError = class extends SphereError {
9718
+ mayHaveCertified = true;
9719
+ constructor(message, cause) {
9720
+ super(message, "CERTIFICATION_UNCONFIRMED", cause);
9721
+ this.name = "ProofUnconfirmedError";
9722
+ }
9723
+ };
9724
+ var CheckpointPersistFailedError = class extends SphereError {
9725
+ mayHaveCertified = true;
9726
+ constructor(message, cause) {
9727
+ super(message, "CHECKPOINT_PERSIST_FAILED", cause);
9728
+ this.name = "CheckpointPersistFailedError";
9729
+ }
9730
+ };
9731
+ var SplitCheckpointLostError = class extends SphereError {
9732
+ constructor(message, cause) {
9733
+ super(message, "SPLIT_CHECKPOINT_LOST", cause);
9734
+ this.name = "SplitCheckpointLostError";
9735
+ }
9736
+ };
9737
+ var CheckpointTrustbaseMismatchError = class extends SphereError {
9738
+ constructor(message, cause) {
9739
+ super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
9740
+ this.name = "CheckpointTrustbaseMismatchError";
9741
+ }
9742
+ };
9743
+ function isKeepOpenSplitError(err) {
9744
+ return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
9745
+ }
9746
+
9747
+ // token-engine/mint-reasons.ts
9748
+ var RULE = "MintJustificationVerification";
9749
+ var MintReasonRegistry = class _MintReasonRegistry extends import_MintJustificationVerifierService.MintJustificationVerifierService {
9750
+ byTag = /* @__PURE__ */ new Map();
9751
+ pluginTags = /* @__PURE__ */ new Set();
9752
+ register(verifier) {
9753
+ super.register(verifier);
9754
+ this.byTag.set(verifier.tag, verifier);
9755
+ return this;
9756
+ }
9757
+ registerPlugin(verifier) {
9758
+ this.register(verifier);
9759
+ this.pluginTags.add(verifier.tag);
9760
+ return this;
9761
+ }
9762
+ async verify(transaction, nestedTokenCollector) {
9763
+ const tag = transaction.justification ? reasonTag(transaction.justification) : null;
9764
+ if (tag === null) return super.verify(transaction, nestedTokenCollector);
9765
+ const verifier = this.byTag.get(tag);
9766
+ if (verifier === void 0) throw new MintReasonUnverifiableError(`No verifier is registered for mint-reason tag ${tag}`);
9767
+ if (!this.pluginTags.has(tag)) return super.verify(transaction, nestedTokenCollector);
9768
+ return verifyPlugin(verifier, transaction, nestedTokenCollector);
9769
+ }
9770
+ knows(tag) {
9771
+ return this.byTag.has(tag);
9772
+ }
9773
+ /** `verifiers` replace the registered ones of their tags; a tag this registry lacks is refused. */
9774
+ overlaid(verifiers) {
9775
+ for (const verifier of verifiers) assertKnown(this, verifier.tag);
9776
+ const replaced = new Map(verifiers.map((verifier) => [verifier.tag, verifier]));
9777
+ const overlay = new _MintReasonRegistry();
9778
+ for (const [tag, verifier] of this.byTag) {
9779
+ const perCall = replaced.get(tag);
9780
+ if (perCall !== void 0 || this.pluginTags.has(tag)) overlay.registerPlugin(perCall ?? verifier);
9781
+ else overlay.register(verifier);
9782
+ }
9783
+ return overlay;
9784
+ }
9785
+ assertVerifiable(justification) {
9786
+ const tag = reasonTag(justification);
9787
+ if (tag === null) throw new SphereError("The mint reason is not tagged CBOR", "VALIDATION_ERROR");
9788
+ assertKnown(this, tag);
9789
+ }
9790
+ };
9791
+ function reasonTag(justification) {
9792
+ try {
9793
+ return BigInt(import_CborDeserializer.CborDeserializer.decodeTag(justification).tag);
9794
+ } catch {
9795
+ return null;
9796
+ }
9797
+ }
9798
+ async function verifyPlugin(verifier, transaction, nestedTokenCollector) {
9799
+ let result;
9800
+ try {
9801
+ result = await verifier.verify(transaction, nestedTokenCollector);
9802
+ } catch (err) {
9803
+ const why = err instanceof Error ? err.message : String(err);
9804
+ throw new MintReasonUnverifiableError(`Mint-reason tag ${verifier.tag} cannot be verified yet: ${why}`, err);
9805
+ }
9806
+ return result.status === import_VerificationStatus.VerificationStatus.OK ? new import_VerificationResult.VerificationResult(RULE, import_VerificationStatus.VerificationStatus.OK, "", [result]) : new import_VerificationResult.VerificationResult(RULE, import_VerificationStatus.VerificationStatus.FAIL, `Verification failed for tag ${verifier.tag}.`, [result]);
9807
+ }
9808
+ function assertKnown(registry, tag) {
9809
+ if (registry.knows(tag)) return;
9810
+ throw new SphereError(
9811
+ `No verifier is registered on the engine for mint-reason tag ${tag}, so a replay of this mint could never verify`,
9812
+ "VALIDATION_ERROR"
9813
+ );
9814
+ }
9815
+
9671
9816
  // token-engine/SpherePaymentData.ts
9672
9817
  var COIN_ID_PATTERN = /^([0-9a-f]{2})+$/;
9673
9818
  function assertAsset(coinId, amount) {
@@ -9844,45 +9989,6 @@ async function awaitProofBounded(deps, transaction, signal) {
9844
9989
  return value;
9845
9990
  }
9846
9991
 
9847
- // token-engine/errors.ts
9848
- var TransferConflictError = class extends SphereError {
9849
- /**
9850
- * #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
9851
- * Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
9852
- */
9853
- conflictedSourceId;
9854
- constructor(message, cause) {
9855
- super(message, "TRANSFER_CONFLICT", cause);
9856
- this.name = "TransferConflictError";
9857
- }
9858
- };
9859
- var ProofUnconfirmedError = class extends SphereError {
9860
- mayHaveCertified = true;
9861
- constructor(message, cause) {
9862
- super(message, "CERTIFICATION_UNCONFIRMED", cause);
9863
- this.name = "ProofUnconfirmedError";
9864
- }
9865
- };
9866
- var CheckpointPersistFailedError = class extends SphereError {
9867
- mayHaveCertified = true;
9868
- constructor(message, cause) {
9869
- super(message, "CHECKPOINT_PERSIST_FAILED", cause);
9870
- this.name = "CheckpointPersistFailedError";
9871
- }
9872
- };
9873
- var SplitCheckpointLostError = class extends SphereError {
9874
- constructor(message, cause) {
9875
- super(message, "SPLIT_CHECKPOINT_LOST", cause);
9876
- this.name = "SplitCheckpointLostError";
9877
- }
9878
- };
9879
- var CheckpointTrustbaseMismatchError = class extends SphereError {
9880
- constructor(message, cause) {
9881
- super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
9882
- this.name = "CheckpointTrustbaseMismatchError";
9883
- }
9884
- };
9885
-
9886
9992
  // token-engine/certification-outcome.ts
9887
9993
  function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
9888
9994
  if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
@@ -10461,15 +10567,17 @@ async function readTokenNft(token) {
10461
10567
  }
10462
10568
 
10463
10569
  // token-engine/plugin-ops.ts
10464
- function contextWithMintVerifiers(base, verifiers) {
10465
- const registry = new import_MintJustificationVerifierService.MintJustificationVerifierService();
10466
- for (const verifier of verifiers) registry.register(verifier);
10570
+ function mintContext(deps, params) {
10571
+ assertMintableData(params.data);
10572
+ if (params.justification !== void 0) deps.mintJustificationVerifier.assertVerifiable(params.justification);
10573
+ const verifiers = params.mintJustificationVerifiers;
10574
+ if (verifiers === void 0) return deps.verificationContext;
10467
10575
  return new import_VerificationContext.VerificationContext(
10468
- base.trustBase,
10469
- base.predicateVerifier,
10470
- base.unicityCertificateVerifier,
10471
- registry,
10472
- new import_TokenIssuanceVerifierService.TokenIssuanceVerifierService(false)
10576
+ deps.trustBase,
10577
+ deps.predicateVerifier,
10578
+ deps.unicityCertificateVerifier,
10579
+ deps.mintJustificationVerifier.overlaid(verifiers),
10580
+ deps.verificationContext.tokenIssuanceVerifier
10473
10581
  );
10474
10582
  }
10475
10583
 
@@ -10557,9 +10665,6 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
10557
10665
  // token-engine/SphereTokenEngine.ts
10558
10666
  var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
10559
10667
  var MAX_MINT_CONCURRENCY = 8;
10560
- function isKeepOpenSplitError(err) {
10561
- return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
10562
- }
10563
10668
  var TRANSFER_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
10564
10669
  var CLEAN_REJECT_STATUSES = /* @__PURE__ */ new Set([
10565
10670
  import_CertificationResponse.CertificationStatus.STATE_ID_MISMATCH,
@@ -10649,8 +10754,9 @@ var SphereTokenEngine = class {
10649
10754
  const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
10650
10755
  return wrapToken(token);
10651
10756
  }
10757
+ assertMintable = (params) => void mintContext(this.deps, params);
10652
10758
  async mintDataToken(params, options) {
10653
- assertMintableData(params.data);
10759
+ const context = mintContext(this.deps, params);
10654
10760
  const recipient = import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey);
10655
10761
  const tokenType = params.tokenType ? new import_TokenType.TokenType(params.tokenType) : import_TokenType.TokenType.generate();
10656
10762
  const salt = params.salt ? import_TokenSalt.TokenSalt.fromBytes(params.salt) : import_TokenSalt.TokenSalt.generate();
@@ -10664,8 +10770,7 @@ var SphereTokenEngine = class {
10664
10770
  options
10665
10771
  );
10666
10772
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10667
- const verifiers = params.mintJustificationVerifiers;
10668
- return wrapToken(await import_Token.Token.mint(certified, verifiers ? contextWithMintVerifiers(this.deps, verifiers) : this.deps.verificationContext));
10773
+ return wrapToken(await import_Token.Token.mint(certified, context));
10669
10774
  }
10670
10775
  buildNftMint(params) {
10671
10776
  return planNftMint(this.deps, params);
@@ -11002,20 +11107,30 @@ var SphereTokenEngine = class {
11002
11107
 
11003
11108
  // token-engine/factory.ts
11004
11109
  var DEFAULT_VERIFICATION_POOL_SIZE = 4;
11005
- function registerPluginVerifiers(registry, plugins) {
11110
+ function registerPlugins(registries, plugins) {
11006
11111
  for (const plugin of plugins ?? []) {
11007
11112
  for (const verifier of plugin.mintJustificationVerifiers ?? []) {
11008
- try {
11009
- registry.register(verifier);
11010
- } catch (err) {
11011
- throw new SphereError(
11012
- `Token plugin '${plugin.id}' registers mint-reason tag ${verifier.tag} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
11013
- "INVALID_CONFIG"
11014
- );
11015
- }
11113
+ claim(plugin, `mint-reason tag ${verifier.tag}`, () => registries.reasons.registerPlugin(verifier));
11114
+ }
11115
+ for (const policy of plugin.tokenIssuancePolicies ?? []) {
11116
+ const tokenType = import_HexConverter.HexConverter.encode(policy.tokenType.bytes);
11117
+ claim(plugin, `an issuance policy for token type ${tokenType}`, () => {
11118
+ registries.claims.add(policy);
11119
+ registries.policies.register(policy);
11120
+ });
11016
11121
  }
11017
11122
  }
11018
11123
  }
11124
+ function claim(plugin, what, register) {
11125
+ try {
11126
+ register();
11127
+ } catch (err) {
11128
+ throw new SphereError(
11129
+ `Token plugin '${plugin.id}' registers ${what} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
11130
+ "INVALID_CONFIG"
11131
+ );
11132
+ }
11133
+ }
11019
11134
  function disposedError() {
11020
11135
  return new SphereError(
11021
11136
  "Verification worker pool disposed \u2014 the in-flight verification was cancelled",
@@ -11093,11 +11208,15 @@ async function createSphereTokenEngine(config) {
11093
11208
  const unicityCertificateVerifier = new import_UnicityCertificateVerifier.UnicityCertificateVerifier(
11094
11209
  new import_UnicitySealQuorumSignaturesVerificationRule.UnicitySealQuorumSignaturesVerificationRule(new import_Secp256k1SignatureVerifier.Secp256k1SignatureVerifier(), new import_VerifiedSealCache.VerifiedSealCache(256))
11095
11210
  );
11096
- const mintJustificationVerifier = new import_MintJustificationVerifierService.MintJustificationVerifierService();
11211
+ const mintJustificationVerifier = new MintReasonRegistry();
11097
11212
  mintJustificationVerifier.register(
11098
11213
  new import_SplitMintJustificationVerifier.SplitMintJustificationVerifier(decodeSpherePaymentData)
11099
11214
  );
11100
- registerPluginVerifiers(mintJustificationVerifier, config.plugins);
11215
+ const tokenIssuanceVerifier = new import_TokenIssuanceVerifierService.TokenIssuanceVerifierService(false);
11216
+ registerPlugins(
11217
+ { reasons: mintJustificationVerifier, policies: tokenIssuanceVerifier, claims: new CoinClaims() },
11218
+ config.plugins
11219
+ );
11101
11220
  const deps = {
11102
11221
  client: new import_StateTransitionClient.StateTransitionClient(new import_AggregatorClient.AggregatorClient(config.aggregatorUrl, config.apiKey ?? null)),
11103
11222
  trustBase,
@@ -11109,7 +11228,7 @@ async function createSphereTokenEngine(config) {
11109
11228
  predicateVerifier,
11110
11229
  unicityCertificateVerifier,
11111
11230
  mintJustificationVerifier,
11112
- new import_TokenIssuanceVerifierService.TokenIssuanceVerifierService(false)
11231
+ tokenIssuanceVerifier
11113
11232
  ),
11114
11233
  signingService: new import_SigningService.SigningService(config.privateKey),
11115
11234
  // Also the HKDF ikm for deterministic realization (Part E.1) — the
@@ -12209,6 +12328,9 @@ function createScopedKV(storage, network, chainPubkey) {
12209
12328
  }
12210
12329
  };
12211
12330
  }
12331
+ function isLiveBurn(entry) {
12332
+ return !entry.settled && entry.failure === void 0;
12333
+ }
12212
12334
  var STORE_KEYS = {
12213
12335
  intentBackstop: "intents",
12214
12336
  checkpointCache: "checkpoints",
@@ -12223,7 +12345,8 @@ var STORE_KEYS = {
12223
12345
  epochLatch: "epoch-latch",
12224
12346
  // §5.2 InventoryView durable overlays (#625/#679).
12225
12347
  suspectedSpent: "suspected-spent",
12226
- knownSpends: "known-spends"
12348
+ knownSpends: "known-spends",
12349
+ verifiedTokens: "verified-tokens"
12227
12350
  };
12228
12351
 
12229
12352
  // modules/payments-v2/machine/journal.ts
@@ -12740,6 +12863,7 @@ var TransferMachine = class {
12740
12863
  const key = keys.get(digest);
12741
12864
  if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
12742
12865
  const { tokenId } = await engine.deliveryKeys(o.changeBlob);
12866
+ await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
12743
12867
  added.push({ tokenId, key });
12744
12868
  }
12745
12869
  return added;
@@ -13135,7 +13259,7 @@ var Converger = class {
13135
13259
  this.deps.stores.burnJournal.list()
13136
13260
  ]);
13137
13261
  const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
13138
- const burnsOpen = burns.some((e) => !e.settled);
13262
+ const burnsOpen = burns.some(isLiveBurn);
13139
13263
  return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
13140
13264
  }
13141
13265
  async resumePass() {
@@ -13222,6 +13346,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
13222
13346
  const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
13223
13347
  holds.set(entry.transferId, [...direct, ...split]);
13224
13348
  }
13349
+ for (const burn of await stores.burnJournal.list()) {
13350
+ if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
13351
+ }
13225
13352
  return { open: holds, complete };
13226
13353
  }
13227
13354
  function groupJournal(journal) {
@@ -13532,18 +13659,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
13532
13659
  createdAt: snapshot.createdAt,
13533
13660
  updatedAt: snapshot.updatedAt,
13534
13661
  lazy: true,
13535
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
13662
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {},
13663
+ ...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
13536
13664
  };
13537
13665
  }
13538
- function aggregateAssets(entries, isTransferring, registry) {
13666
+ function aggregateAssets(entries, reading, registry) {
13539
13667
  const groups = /* @__PURE__ */ new Map();
13540
13668
  for (const [tokenId, entry] of entries) {
13541
- const moving = isTransferring(tokenId);
13669
+ const moving = reading.isTransferring(tokenId);
13542
13670
  for (const asset of entry.assets) {
13543
- let group = groups.get(asset.coinId);
13671
+ const standing = reading.standing(tokenId, asset.coinId);
13672
+ const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
13673
+ let group = groups.get(key);
13544
13674
  if (!group) {
13545
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
13546
- groups.set(asset.coinId, group);
13675
+ group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
13676
+ groups.set(key, group);
13547
13677
  }
13548
13678
  if (moving) {
13549
13679
  group.transferring += BigInt(asset.amount);
@@ -13554,9 +13684,10 @@ function aggregateAssets(entries, isTransferring, registry) {
13554
13684
  }
13555
13685
  }
13556
13686
  }
13557
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
13687
+ return [...groups.values()].map((totals) => toAsset(totals, registry));
13558
13688
  }
13559
- function toAsset(coinId, totals, registry) {
13689
+ function toAsset(totals, registry) {
13690
+ const coinId = totals.coinId;
13560
13691
  const iconUrl = registry.getIconUrl(coinId);
13561
13692
  return {
13562
13693
  coinId,
@@ -13576,7 +13707,8 @@ function toAsset(coinId, totals, registry) {
13576
13707
  priceEur: null,
13577
13708
  change24h: null,
13578
13709
  fiatValueUsd: null,
13579
- fiatValueEur: null
13710
+ fiatValueEur: null,
13711
+ ...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
13580
13712
  };
13581
13713
  }
13582
13714
  function priceIdOf(registry, coinId) {
@@ -13584,9 +13716,9 @@ function priceIdOf(registry, coinId) {
13584
13716
  }
13585
13717
  async function withPrices(raw, registry, price) {
13586
13718
  try {
13587
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
13719
+ const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
13588
13720
  const quotes = await price.getPrices(ids);
13589
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13721
+ return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13590
13722
  } catch {
13591
13723
  return raw;
13592
13724
  }
@@ -13607,11 +13739,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
13607
13739
  const snapshot = { assets: [], createdAt: now, updatedAt: now };
13608
13740
  return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
13609
13741
  transferring: true,
13610
- suspectedSpent: false
13742
+ suspectedSpent: false,
13743
+ standing: "trusted"
13611
13744
  });
13612
13745
  }
13613
13746
 
13614
13747
  // modules/payments-v2/inventory/InventoryView.ts
13748
+ var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
13615
13749
  var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
13616
13750
  var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
13617
13751
  var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
@@ -13705,6 +13839,12 @@ var InventoryView = class {
13705
13839
  for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
13706
13840
  if (changed) this.deps.emit("inventory:updated");
13707
13841
  }
13842
+ get trust() {
13843
+ return this.deps.trust ?? TRUST_ALL;
13844
+ }
13845
+ trusts(tokenId, coinId) {
13846
+ return this.trust.standing(tokenId, coinId) === "trusted";
13847
+ }
13708
13848
  pinned(tokenId) {
13709
13849
  return this.deps.isPinned?.(tokenId) ?? false;
13710
13850
  }
@@ -13731,6 +13871,7 @@ var InventoryView = class {
13731
13871
  isSpendable(tokenId, entry) {
13732
13872
  if (entry.status !== "active") return false;
13733
13873
  if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13874
+ if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
13734
13875
  return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13735
13876
  }
13736
13877
  /** Is this NAMED token spendable whole — coinless or valued (#777)? */
@@ -13757,7 +13898,8 @@ var InventoryView = class {
13757
13898
  out.push(
13758
13899
  toToken(tokenId, entry, asset, registry, {
13759
13900
  transferring: this.held(tokenId),
13760
- suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
13901
+ suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
13902
+ standing: this.trust.standing(tokenId, asset.coinId)
13761
13903
  })
13762
13904
  );
13763
13905
  }
@@ -13787,10 +13929,21 @@ var InventoryView = class {
13787
13929
  return out;
13788
13930
  }
13789
13931
  async assets(registry, price) {
13790
- const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
13932
+ const reading = {
13933
+ isTransferring: (tokenId) => this.held(tokenId),
13934
+ standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
13935
+ };
13936
+ const raw = aggregateAssets(this.activeEntries(), reading, registry);
13791
13937
  if (!price || raw.length === 0) return raw;
13792
13938
  return withPrices(raw, registry, price);
13793
13939
  }
13940
+ claimHolders() {
13941
+ const out = [];
13942
+ for (const [tokenId, entry] of this.activeEntries()) {
13943
+ if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
13944
+ }
13945
+ return out;
13946
+ }
13794
13947
  *activeEntries() {
13795
13948
  for (const [tokenId, entry] of this.mirror) {
13796
13949
  if (entry.status === "active") yield [tokenId, entry];
@@ -13960,6 +14113,171 @@ var InventoryView = class {
13960
14113
  }
13961
14114
  };
13962
14115
 
14116
+ // modules/payments-v2/inventory/verdicts.ts
14117
+ var VERDICT_RETRY_MS = 3e4;
14118
+ var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
14119
+ function honoursClaims(claims, token) {
14120
+ const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
14121
+ return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
14122
+ }
14123
+ var TokenVerdicts = class {
14124
+ constructor(deps) {
14125
+ this.deps = deps;
14126
+ }
14127
+ verified = /* @__PURE__ */ new Set();
14128
+ refused = /* @__PURE__ */ new Set();
14129
+ retries = /* @__PURE__ */ new Map();
14130
+ writes = new SerialChain();
14131
+ hydration = null;
14132
+ queued = null;
14133
+ running = null;
14134
+ held = [];
14135
+ timer = null;
14136
+ live = false;
14137
+ isClaimed(coinId) {
14138
+ return this.deps.claims.issuerOf(coinId) !== null;
14139
+ }
14140
+ standing(tokenId, coinId) {
14141
+ if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
14142
+ return this.refused.has(tokenId) ? "refused" : "pending";
14143
+ }
14144
+ trusts(tokenId, coinId) {
14145
+ return this.standing(tokenId, coinId) === "trusted";
14146
+ }
14147
+ hydrate() {
14148
+ this.hydration ??= this.load().catch((error) => {
14149
+ this.hydration = null;
14150
+ throw error;
14151
+ });
14152
+ return this.hydration;
14153
+ }
14154
+ start() {
14155
+ this.live = true;
14156
+ return this.hydrate();
14157
+ }
14158
+ stop() {
14159
+ this.live = false;
14160
+ if (this.timer !== null) clearTimeout(this.timer);
14161
+ this.timer = null;
14162
+ }
14163
+ async accept(token) {
14164
+ if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
14165
+ }
14166
+ async recordSplit(sourceTokenId, outputTokenId) {
14167
+ await this.hydrate();
14168
+ if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
14169
+ }
14170
+ review(holders) {
14171
+ this.queued = holders;
14172
+ this.running ??= this.drain();
14173
+ return this.running;
14174
+ }
14175
+ async drain() {
14176
+ try {
14177
+ while (this.queued !== null) {
14178
+ const holders = this.queued;
14179
+ this.queued = null;
14180
+ await this.reviewOnce(holders);
14181
+ }
14182
+ } finally {
14183
+ this.running = null;
14184
+ }
14185
+ }
14186
+ async reviewOnce(holders) {
14187
+ await this.hydrate();
14188
+ this.held = holders;
14189
+ const due = holders.filter((tokenId) => this.isDue(tokenId));
14190
+ const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
14191
+ for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
14192
+ if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
14193
+ await this.persist();
14194
+ this.deps.changed();
14195
+ }
14196
+ this.scheduleRetry();
14197
+ }
14198
+ isDue(tokenId) {
14199
+ if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
14200
+ return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
14201
+ }
14202
+ async check(due) {
14203
+ const verdicts = /* @__PURE__ */ new Map();
14204
+ let blobs;
14205
+ try {
14206
+ blobs = await this.deps.getBlobs([...due]);
14207
+ } catch {
14208
+ return verdicts;
14209
+ }
14210
+ for (const tokenId of due) {
14211
+ const bytes = blobs.get(tokenId);
14212
+ if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
14213
+ }
14214
+ return verdicts;
14215
+ }
14216
+ async verdictOf(tokenId, bytes) {
14217
+ let engine;
14218
+ try {
14219
+ engine = this.deps.engine();
14220
+ } catch {
14221
+ return "retry";
14222
+ }
14223
+ let token;
14224
+ try {
14225
+ token = await engine.decodeToken({ tokenId, token: bytes });
14226
+ } catch {
14227
+ return "refused";
14228
+ }
14229
+ if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
14230
+ try {
14231
+ return (await engine.verify(token)).ok ? "verified" : "refused";
14232
+ } catch {
14233
+ return "retry";
14234
+ }
14235
+ }
14236
+ settle(tokenId, verdict) {
14237
+ if (verdict === "retry") {
14238
+ this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
14239
+ return;
14240
+ }
14241
+ this.retries.delete(tokenId);
14242
+ (verdict === "verified" ? this.verified : this.refused).add(tokenId);
14243
+ }
14244
+ nextRetry(previous) {
14245
+ const attempts = (previous?.attempts ?? 0) + 1;
14246
+ const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
14247
+ return { attempts, atMs: this.now() + delay };
14248
+ }
14249
+ scheduleRetry() {
14250
+ if (!this.live || this.timer !== null) return;
14251
+ const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
14252
+ if (due.length === 0) return;
14253
+ const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
14254
+ this.timer = setTimeout(() => {
14255
+ this.timer = null;
14256
+ void this.review(this.held).catch(() => void 0);
14257
+ }, delay);
14258
+ }
14259
+ async remember(tokenId) {
14260
+ await this.hydrate();
14261
+ if (this.verified.has(tokenId)) return;
14262
+ this.verified.add(tokenId);
14263
+ this.refused.delete(tokenId);
14264
+ this.retries.delete(tokenId);
14265
+ await this.persist();
14266
+ }
14267
+ persist() {
14268
+ const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
14269
+ return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
14270
+ }
14271
+ async load() {
14272
+ const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
14273
+ if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
14274
+ for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
14275
+ }
14276
+ now() {
14277
+ return (this.deps.now ?? Date.now)();
14278
+ }
14279
+ };
14280
+
13963
14281
  // modules/payments-v2/ports.ts
13964
14282
  function isRetryableAckError(err) {
13965
14283
  return err !== null && typeof err === "object" && err.retryable === true;
@@ -13969,6 +14287,8 @@ function isRetryableAckError(err) {
13969
14287
  var ACK_BATCH_SIZE = 200;
13970
14288
  var REFRESH_INTERVAL_MS = 2500;
13971
14289
  var POLL_INTERVAL_MS = 3e4;
14290
+ var RECHECK_BASE_MS = POLL_INTERVAL_MS;
14291
+ var RECHECK_MAX_MS = 60 * 60 * 1e3;
13972
14292
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
13973
14293
  function receivedDedupKey(tokenId, stateHash) {
13974
14294
  return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
@@ -13985,6 +14305,7 @@ var Receive = class {
13985
14305
  pollTimer = null;
13986
14306
  unsubscribeWake = null;
13987
14307
  lastRefreshAt = 0;
14308
+ rechecks = /* @__PURE__ */ new Map();
13988
14309
  drainOnce() {
13989
14310
  return this.drainFlight.run(() => this.doDrain());
13990
14311
  }
@@ -14043,6 +14364,7 @@ var Receive = class {
14043
14364
  const pending = [];
14044
14365
  const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
14045
14366
  const claimable = { n: 0 };
14367
+ const deferred = { any: false };
14046
14368
  const finish = () => {
14047
14369
  if (claimable.n > 0) deps.refreshView?.();
14048
14370
  return stored;
@@ -14051,7 +14373,7 @@ var Receive = class {
14051
14373
  const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
14052
14374
  let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
14053
14375
  for (let pass = 0; pass < 2; pass++) {
14054
- await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
14376
+ await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
14055
14377
  const served = deps.delivery.incomingEpoch();
14056
14378
  if (basis === null || served === null || served === basis.syncEpoch) break;
14057
14379
  await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
@@ -14068,20 +14390,41 @@ var Receive = class {
14068
14390
  return finish();
14069
14391
  }
14070
14392
  async processEntry(ctx, entry) {
14071
- const { deps, engine, pending, stored, claimable } = ctx;
14072
- const screened = await screen(deps, engine, entry);
14393
+ const { deps, engine, stored, claimable } = ctx;
14394
+ const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
14395
+ this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
14396
+ if (screened.kind === "defer") {
14397
+ ctx.deferred.any = true;
14398
+ return;
14399
+ }
14073
14400
  if (screened.kind === "ack") {
14074
- pending.push(screened.ack);
14401
+ queueAck(ctx, screened.ack);
14075
14402
  if (screened.ack.disposition === "claimed") claimable.n += 1;
14076
14403
  return;
14077
14404
  }
14405
+ await deps.accepted?.(screened.token);
14078
14406
  await deps.view.store(screened.record);
14079
- pending.push(claimAck(entry));
14407
+ queueAck(ctx, claimAck(entry));
14080
14408
  claimable.n += 1;
14081
14409
  const transfer = await announce(deps, entry, screened.record);
14082
14410
  stored.push(transfer);
14083
14411
  deps.emit("transfer:incoming", transfer);
14084
14412
  }
14413
+ recheckDue(deliveryId) {
14414
+ const recheck = this.rechecks.get(deliveryId);
14415
+ return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
14416
+ }
14417
+ scheduleRecheck(deliveryId, deferred) {
14418
+ if (!deferred) {
14419
+ this.rechecks.delete(deliveryId);
14420
+ return;
14421
+ }
14422
+ const previous = this.rechecks.get(deliveryId);
14423
+ if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
14424
+ const attempts = (previous?.attempts ?? 0) + 1;
14425
+ const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
14426
+ this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
14427
+ }
14085
14428
  };
14086
14429
  async function screen(deps, engine, entry) {
14087
14430
  const blobBytes = await entry.fetchBlob();
@@ -14092,7 +14435,8 @@ async function screen(deps, engine, entry) {
14092
14435
  logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
14093
14436
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
14094
14437
  }
14095
- const verdict = await engine.verify(token);
14438
+ const verdict = await verdictOf(engine, token);
14439
+ if (verdict === null) return { kind: "defer" };
14096
14440
  if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
14097
14441
  if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
14098
14442
  return { kind: "ack", ack: rejectAck(entry, "not-owned") };
@@ -14103,38 +14447,43 @@ async function screen(deps, engine, entry) {
14103
14447
  if (held !== null && await engine.isSpent(token)) {
14104
14448
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
14105
14449
  }
14450
+ const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
14106
14451
  return {
14107
14452
  kind: "accept",
14453
+ token,
14108
14454
  record: {
14109
14455
  tokenId: keys.tokenId,
14110
14456
  stateHash: keys.stateHash,
14111
14457
  assets: toAssetAmounts(token),
14112
- ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
14458
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
14459
+ ...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
14113
14460
  }
14114
14461
  };
14115
14462
  }
14116
- async function announce(deps, entry, record) {
14117
- const receivedAt = (deps.now ?? Date.now)();
14463
+ function unverifiedCoinsOf(deps, token) {
14464
+ const claims = deps.claims;
14465
+ if (claims === void 0 || honoursClaims(claims, token)) return [];
14466
+ return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
14467
+ }
14468
+ async function verdictOf(engine, token) {
14118
14469
  try {
14119
- await deps.recordReceived({
14120
- dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
14121
- tokenId: record.tokenId,
14122
- stateHash: record.stateHash,
14123
- assets: record.assets,
14124
- ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
14125
- ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
14126
- ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
14127
- ...entry.memo !== void 0 ? { memo: entry.memo } : {},
14128
- receivedAt
14129
- });
14470
+ return await engine.verify(token);
14130
14471
  } catch (err) {
14131
- logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
14472
+ if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
14473
+ throw err;
14132
14474
  }
14475
+ }
14476
+ async function announce(deps, entry, record) {
14477
+ const receivedAt = (deps.now ?? Date.now)();
14478
+ await recordArrival(deps, entry, record, receivedAt);
14479
+ const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
14480
+ const counted = record.assets.filter((asset) => !refused2.includes(asset));
14133
14481
  return {
14134
14482
  id: record.tokenId,
14135
14483
  senderPubkey: entry.senderPubkey ?? "",
14136
14484
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
14137
- tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
14485
+ tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
14486
+ ...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
14138
14487
  // #777: named here rather than mapped from assets, which announced an EMPTY list.
14139
14488
  ...record.tokenType !== void 0 ? {
14140
14489
  coinless: [
@@ -14152,6 +14501,25 @@ async function announce(deps, entry, record) {
14152
14501
  receivedAt
14153
14502
  };
14154
14503
  }
14504
+ async function recordArrival(deps, entry, record, receivedAt) {
14505
+ const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
14506
+ if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
14507
+ try {
14508
+ await deps.recordReceived({
14509
+ dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
14510
+ tokenId: record.tokenId,
14511
+ stateHash: record.stateHash,
14512
+ assets,
14513
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
14514
+ ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
14515
+ ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
14516
+ ...entry.memo !== void 0 ? { memo: entry.memo } : {},
14517
+ receivedAt
14518
+ });
14519
+ } catch (err) {
14520
+ logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
14521
+ }
14522
+ }
14155
14523
  async function flushAcks(deps, pending, epochOf) {
14156
14524
  if (pending.length === 0) return { progressed: true };
14157
14525
  const before = pending.length;
@@ -14159,7 +14527,7 @@ async function flushAcks(deps, pending, epochOf) {
14159
14527
  try {
14160
14528
  const settled = await settleAcks(deps, pending);
14161
14529
  let i = 0;
14162
- while (i < pending.length && settled.has(pending[i].deliveryId)) {
14530
+ while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
14163
14531
  lastAcked = pending[i].cursor;
14164
14532
  i += 1;
14165
14533
  }
@@ -14235,6 +14603,9 @@ async function ackOne(deps, ack) {
14235
14603
  await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
14236
14604
  }
14237
14605
  }
14606
+ function queueAck(ctx, ack) {
14607
+ ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
14608
+ }
14238
14609
  function claimAck(entry) {
14239
14610
  return {
14240
14611
  deliveryId: entry.deliveryId,
@@ -14943,72 +15314,276 @@ var SpendQueue = class {
14943
15314
  queue = [];
14944
15315
  this.queues.set(coinId, queue);
14945
15316
  }
14946
- queue.push(entry);
14947
- return settled;
15317
+ queue.push(entry);
15318
+ return settled;
15319
+ }
15320
+ expire(id, coinId) {
15321
+ const entries = this.queues.get(coinId);
15322
+ if (!entries) return;
15323
+ const index = entries.findIndex((e) => e.id === id);
15324
+ if (index === -1) return;
15325
+ const [entry] = entries.splice(index, 1);
15326
+ entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
15327
+ if (entries.length === 0) this.queues.delete(coinId);
15328
+ }
15329
+ // Whole-token semantics: only fully-free tokens enter the selector pool. What
15330
+ // the reservations hold back is counted too — the refusal has to name it (#737).
15331
+ freeView(coinId) {
15332
+ const freeView = [];
15333
+ let freeTotal = 0n;
15334
+ let pinnedTotal = 0n;
15335
+ const holders = /* @__PURE__ */ new Set();
15336
+ const unproven = this.deps.ledger.unprovenReason();
15337
+ for (const entry of this.deps.getPool(coinId)) {
15338
+ if (entry.amount <= 0n) continue;
15339
+ const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
15340
+ if (holder === void 0) {
15341
+ freeView.push(entry);
15342
+ freeTotal += entry.amount;
15343
+ } else {
15344
+ pinnedTotal += entry.amount;
15345
+ holders.add(holder);
15346
+ }
15347
+ }
15348
+ return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
15349
+ }
15350
+ expectedChangeTotal(coinId) {
15351
+ let total = 0n;
15352
+ for (const change of this.expectedChange.values()) {
15353
+ if (change.coinId === coinId) total += change.amount;
15354
+ }
15355
+ return total;
15356
+ }
15357
+ reservePlan(reservationId, plan, freeView) {
15358
+ const pool = new Set(freeView.map((e) => e.tokenId));
15359
+ const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
15360
+ for (const tokenId of tokenIds) {
15361
+ if (!pool.has(tokenId)) {
15362
+ throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
15363
+ }
15364
+ }
15365
+ this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
15366
+ }
15367
+ totalQueued() {
15368
+ let total = 0;
15369
+ for (const entries of this.queues.values()) total += entries.length;
15370
+ return total;
15371
+ }
15372
+ };
15373
+ function parseAmount(raw) {
15374
+ if (!/^\d+$/.test(raw)) {
15375
+ throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
15376
+ }
15377
+ const amount = BigInt(raw);
15378
+ if (amount <= 0n) {
15379
+ throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
15380
+ }
15381
+ return amount;
15382
+ }
15383
+
15384
+ // modules/payments-v2/machine/payload.ts
15385
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
15386
+ return {
15387
+ v: 2,
15388
+ kind: "coin",
15389
+ recipient: recipientPubkey,
15390
+ coinId: request.coinId,
15391
+ amount: request.amount,
15392
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
15393
+ direct: [...spend.plan.direct],
15394
+ ...spend.plan.split !== void 0 ? {
15395
+ split: {
15396
+ tokenId: spend.plan.split.tokenId,
15397
+ splitAmount: spend.plan.split.splitAmount.toString(),
15398
+ remainderAmount: spend.plan.split.remainderAmount.toString()
15399
+ }
15400
+ } : {},
15401
+ spentStates
15402
+ };
15403
+ }
15404
+ function messageOf(err) {
15405
+ return err instanceof Error ? err.message : String(err);
15406
+ }
15407
+ function buildWholePayload(recipientPubkey, request, spentStates) {
15408
+ return {
15409
+ v: 2,
15410
+ kind: "whole",
15411
+ recipient: recipientPubkey,
15412
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
15413
+ direct: [request.tokenId],
15414
+ spentStates
15415
+ };
15416
+ }
15417
+
15418
+ // modules/payments-v2/burn.ts
15419
+ var HEX32 = /^[0-9a-f]{64}$/;
15420
+ async function heldToken2(deps, tokenId) {
15421
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
15422
+ if (bytes === void 0) {
15423
+ throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
15424
+ }
15425
+ return deps.engine.decodeToken({ tokenId, token: bytes });
15426
+ }
15427
+ function assertBurnable(engine, token, tokenId) {
15428
+ if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
15429
+ throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
15430
+ }
15431
+ if (token.valueEnvelope === "bare_collection") {
15432
+ throw new SphereError(
15433
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
15434
+ "VALIDATION_ERROR"
15435
+ );
15436
+ }
15437
+ }
15438
+ function assetsOf(token) {
15439
+ return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
15440
+ }
15441
+ async function runBurnUnderJournal(deps, input) {
15442
+ const { burnId } = input;
15443
+ const { tokenId, reasonBytes } = input.request;
15444
+ if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
15445
+ return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
15446
+ }
15447
+ let token;
15448
+ try {
15449
+ token = await heldToken2(deps, tokenId);
15450
+ assertBurnable(deps.engine, token, tokenId);
15451
+ } catch (err) {
15452
+ return refused(burnId, tokenId, err);
15453
+ }
15454
+ const entry = {
15455
+ burnId,
15456
+ tokenId,
15457
+ reasonHex: bytesToHex3(reasonBytes),
15458
+ burnedTokenHex: null,
15459
+ settled: false,
15460
+ assets: assetsOf(token),
15461
+ createdAt: deps.now()
15462
+ };
15463
+ await deps.burnJournal.upsert(entry);
15464
+ try {
15465
+ const settled = await burnJournaled(deps, entry, token);
15466
+ return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
15467
+ } catch (err) {
15468
+ const failure = failureOf(err);
15469
+ if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
15470
+ else deps.armHeartbeat();
15471
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
15472
+ }
15473
+ }
15474
+ async function replayBurns(deps) {
15475
+ let progressed = 0;
15476
+ for (const entry of await deps.burnJournal.list()) {
15477
+ if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
15478
+ try {
15479
+ const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
15480
+ await burnJournaled(deps, entry, token);
15481
+ progressed += 1;
15482
+ } catch (err) {
15483
+ if (!isTerminal(err)) continue;
15484
+ await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
15485
+ progressed += 1;
15486
+ }
15487
+ }
15488
+ return progressed;
15489
+ }
15490
+ async function pendingBurns(deps) {
15491
+ return (await deps.burnJournal.list()).map((entry) => ({
15492
+ burnId: entry.burnId,
15493
+ tokenId: entry.tokenId,
15494
+ reasonBytes: hexToBytes4(entry.reasonHex),
15495
+ burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
15496
+ settled: entry.settled,
15497
+ createdAt: entry.createdAt,
15498
+ ...entry.failure !== void 0 ? { failure: entry.failure } : {}
15499
+ }));
15500
+ }
15501
+ async function acknowledgeBurn(deps, burnId) {
15502
+ const entry = await deps.burnJournal.getByKey(burnId);
15503
+ if (entry === void 0) return;
15504
+ if (isLiveBurn(entry)) {
15505
+ throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
15506
+ }
15507
+ await deps.burnJournal.removeByKey(burnId);
15508
+ }
15509
+ function refused(burnId, tokenId, err) {
15510
+ const failure = failureOf(err);
15511
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
15512
+ }
15513
+ function isTerminal(err) {
15514
+ return classifyError(err) === "conflict";
15515
+ }
15516
+ function failureOf(err) {
15517
+ const code = err !== null && typeof err === "object" ? err.code : void 0;
15518
+ return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
15519
+ }
15520
+ async function burnJournaled(deps, entry, token) {
15521
+ let current = entry;
15522
+ if (current.burnedTokenHex === null) {
15523
+ if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
15524
+ const burned = await deps.engine.burn(
15525
+ { token, reasonBytes: hexToBytes4(current.reasonHex) },
15526
+ { transferId: current.burnId }
15527
+ );
15528
+ if (burned.blob.tokenId !== current.tokenId) {
15529
+ throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
15530
+ }
15531
+ current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
15532
+ await deps.burnJournal.upsert(current);
14948
15533
  }
14949
- expire(id, coinId) {
14950
- const entries = this.queues.get(coinId);
14951
- if (!entries) return;
14952
- const index = entries.findIndex((e) => e.id === id);
14953
- if (index === -1) return;
14954
- const [entry] = entries.splice(index, 1);
14955
- entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
14956
- if (entries.length === 0) this.queues.delete(coinId);
15534
+ await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
15535
+ await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
15536
+ current = { ...current, settled: true };
15537
+ await deps.burnJournal.upsert(current);
15538
+ deps.refreshView();
15539
+ return current;
15540
+ }
15541
+
15542
+ // modules/payments-v2/burn-hold.ts
15543
+ var BurnHold = class {
15544
+ constructor(deps) {
15545
+ this.deps = deps;
14957
15546
  }
14958
- // Whole-token semantics: only fully-free tokens enter the selector pool. What
14959
- // the reservations hold back is counted too — the refusal has to name it (#737).
14960
- freeView(coinId) {
14961
- const freeView = [];
14962
- let freeTotal = 0n;
14963
- let pinnedTotal = 0n;
14964
- const holders = /* @__PURE__ */ new Set();
14965
- const unproven = this.deps.ledger.unprovenReason();
14966
- for (const entry of this.deps.getPool(coinId)) {
14967
- if (entry.amount <= 0n) continue;
14968
- const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
14969
- if (holder === void 0) {
14970
- freeView.push(entry);
14971
- freeTotal += entry.amount;
14972
- } else {
14973
- pinnedTotal += entry.amount;
14974
- holders.add(holder);
14975
- }
15547
+ async run(burnId, tokenId, burn) {
15548
+ try {
15549
+ this.deps.queue.planWhole(burnId, tokenId);
15550
+ } catch (err) {
15551
+ return refused(burnId, tokenId, err);
14976
15552
  }
14977
- return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
14978
- }
14979
- expectedChangeTotal(coinId) {
14980
- let total = 0n;
14981
- for (const change of this.expectedChange.values()) {
14982
- if (change.coinId === coinId) total += change.amount;
15553
+ this.deps.view.markInFlightMany([tokenId]);
15554
+ let burned = false;
15555
+ try {
15556
+ const result = await burn();
15557
+ burned = result.success;
15558
+ return result;
15559
+ } finally {
15560
+ await this.settle(burnId, tokenId, burned);
14983
15561
  }
14984
- return total;
14985
15562
  }
14986
- reservePlan(reservationId, plan, freeView) {
14987
- const pool = new Set(freeView.map((e) => e.tokenId));
14988
- const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
14989
- for (const tokenId of tokenIds) {
14990
- if (!pool.has(tokenId)) {
14991
- throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
14992
- }
15563
+ async settle(burnId, tokenId, burned) {
15564
+ if (burned) {
15565
+ this.deps.ledger.commit(burnId);
15566
+ this.deps.track(this.refreshThenRelease(tokenId));
15567
+ return;
14993
15568
  }
14994
- this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
15569
+ const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
15570
+ if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
15571
+ this.deps.ledger.cancel(burnId);
15572
+ this.deps.view.releaseMany([tokenId]);
15573
+ this.deps.queue.notifyChange("");
15574
+ return;
15575
+ }
15576
+ this.deps.pins.adopt(burnId);
14995
15577
  }
14996
- totalQueued() {
14997
- let total = 0;
14998
- for (const entries of this.queues.values()) total += entries.length;
14999
- return total;
15578
+ async refreshThenRelease(tokenId) {
15579
+ try {
15580
+ await this.deps.view.delta();
15581
+ this.deps.view.releaseMany([tokenId]);
15582
+ } finally {
15583
+ this.deps.queue.notifyChange("");
15584
+ }
15000
15585
  }
15001
15586
  };
15002
- function parseAmount(raw) {
15003
- if (!/^\d+$/.test(raw)) {
15004
- throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
15005
- }
15006
- const amount = BigInt(raw);
15007
- if (amount <= 0n) {
15008
- throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
15009
- }
15010
- return amount;
15011
- }
15012
15587
 
15013
15588
  // modules/payments-v2/compose.ts
15014
15589
  function supportsDeterministicMint(engine) {
@@ -15017,16 +15592,8 @@ function supportsDeterministicMint(engine) {
15017
15592
  function composeFacadeParts(deps, hooks) {
15018
15593
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
15019
15594
  const ledger = new ReservationLedger();
15020
- const view = new InventoryView({
15021
- port: deps.storagePort,
15022
- kv: deps.kv,
15023
- emit: (event) => deps.emit(event, {}),
15024
- // #737: a reserved token is unselectable, so it is never confirmed balance.
15025
- // #738: while the held-set is unproven, EVERY token reads pinned — the report
15026
- // must not call a token spendable that the queue is about to refuse to spend.
15027
- isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
15028
- ...deps.now !== void 0 ? { now: deps.now } : {}
15029
- });
15595
+ const verdicts = buildVerdicts(deps, hooks);
15596
+ const view = buildView(deps, hooks, ledger, verdicts);
15030
15597
  const queue = new SpendQueue({
15031
15598
  ledger,
15032
15599
  getPool: (coinId) => view.pool(coinId),
@@ -15041,27 +15608,57 @@ function composeFacadeParts(deps, hooks) {
15041
15608
  ...deps.now !== void 0 ? { now: deps.now } : {},
15042
15609
  ...deps.newId !== void 0 ? { newId: deps.newId } : {}
15043
15610
  });
15044
- const machineDeps = buildMachineDeps(deps, hooks, historyStore);
15611
+ const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
15045
15612
  const machineStores = createMachineStores(deps.kv);
15046
15613
  const heldStates = /* @__PURE__ */ new Map();
15047
15614
  const refreshView = coalesced(() => view.delta(), hooks.track);
15615
+ const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
15048
15616
  return {
15049
15617
  ownPubkeyBytes,
15050
15618
  view,
15619
+ verdicts,
15051
15620
  refreshView,
15052
15621
  ledger,
15053
- pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
15622
+ pins,
15054
15623
  queue,
15624
+ burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
15055
15625
  historyStore,
15056
15626
  machineStores,
15057
15627
  machineDeps,
15058
15628
  machine: new TransferMachine(machineDeps),
15059
15629
  heldStates,
15060
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
15630
+ receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
15061
15631
  requests: buildRequests(deps, hooks),
15062
15632
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
15063
15633
  };
15064
15634
  }
15635
+ function buildView(deps, hooks, ledger, verdicts) {
15636
+ const view = new InventoryView({
15637
+ port: deps.storagePort,
15638
+ kv: deps.kv,
15639
+ trust: verdicts,
15640
+ emit: (event) => {
15641
+ deps.emit(event, {});
15642
+ hooks.track(verdicts.review(view.claimHolders()));
15643
+ },
15644
+ // #737: a reserved token is unselectable, so it is never confirmed balance.
15645
+ // #738: while the held-set is unproven, EVERY token reads pinned — the report
15646
+ // must not call a token spendable that the queue is about to refuse to spend.
15647
+ isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
15648
+ ...deps.now !== void 0 ? { now: deps.now } : {}
15649
+ });
15650
+ return view;
15651
+ }
15652
+ function buildVerdicts(deps, hooks) {
15653
+ return new TokenVerdicts({
15654
+ kv: deps.kv,
15655
+ claims: deps.claims ?? new CoinClaims(),
15656
+ engine: () => hooks.engine(),
15657
+ getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
15658
+ changed: () => deps.emit("inventory:updated", {}),
15659
+ ...deps.now !== void 0 ? { now: deps.now } : {}
15660
+ });
15661
+ }
15065
15662
  function buildPins(deps, hooks, parts) {
15066
15663
  return new IntentPins({
15067
15664
  ledger: parts.ledger,
@@ -15089,8 +15686,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
15089
15686
  }
15090
15687
  };
15091
15688
  }
15092
- function buildMachineDeps(deps, hooks, historyStore) {
15689
+ function buildMachineDeps(deps, hooks, historyStore, verdicts) {
15093
15690
  return {
15691
+ recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
15094
15692
  engine: () => hooks.engine(),
15095
15693
  storage: deps.storagePort,
15096
15694
  delivery: deps.deliveryPort,
@@ -15123,10 +15721,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
15123
15721
  }
15124
15722
  };
15125
15723
  }
15126
- function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
15724
+ function buildReceive(deps, hooks, parts, refreshView) {
15725
+ const { historyStore, heldStates, verdicts } = parts;
15127
15726
  return new Receive({
15128
15727
  delivery: deps.deliveryPort,
15129
15728
  engine: () => hooks.engine(),
15729
+ claims: deps.claims ?? new CoinClaims(),
15730
+ accepted: (token) => verdicts.accept(token),
15130
15731
  view: {
15131
15732
  heldState: (tokenId) => heldStates.get(tokenId) ?? null,
15132
15733
  store: async (entry) => {
@@ -15176,40 +15777,6 @@ function buildRequests(deps, hooks) {
15176
15777
  });
15177
15778
  }
15178
15779
 
15179
- // modules/payments-v2/machine/payload.ts
15180
- function buildPayload(recipientPubkey, request, spend, spentStates) {
15181
- return {
15182
- v: 2,
15183
- kind: "coin",
15184
- recipient: recipientPubkey,
15185
- coinId: request.coinId,
15186
- amount: request.amount,
15187
- ...request.memo !== void 0 ? { memo: request.memo } : {},
15188
- direct: [...spend.plan.direct],
15189
- ...spend.plan.split !== void 0 ? {
15190
- split: {
15191
- tokenId: spend.plan.split.tokenId,
15192
- splitAmount: spend.plan.split.splitAmount.toString(),
15193
- remainderAmount: spend.plan.split.remainderAmount.toString()
15194
- }
15195
- } : {},
15196
- spentStates
15197
- };
15198
- }
15199
- function messageOf(err) {
15200
- return err instanceof Error ? err.message : String(err);
15201
- }
15202
- function buildWholePayload(recipientPubkey, request, spentStates) {
15203
- return {
15204
- v: 2,
15205
- kind: "whole",
15206
- recipient: recipientPubkey,
15207
- ...request.memo !== void 0 ? { memo: request.memo } : {},
15208
- direct: [request.tokenId],
15209
- spentStates
15210
- };
15211
- }
15212
-
15213
15780
  // modules/payments-v2/mint-params.ts
15214
15781
  function mintParams(recipientPubkey, coinId, amount) {
15215
15782
  return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
@@ -15279,6 +15846,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
15279
15846
  }
15280
15847
  return false;
15281
15848
  }
15849
+ async function seedHeldStates(storagePort, heldStates) {
15850
+ let page = await storagePort.listInventory();
15851
+ for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
15852
+ for (const item of page.items) {
15853
+ if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
15854
+ }
15855
+ if (!page.more) return;
15856
+ page = await storagePort.listInventory(page.cursor);
15857
+ }
15858
+ }
15282
15859
  async function replayCoinMints(deps) {
15283
15860
  let resolved = 0;
15284
15861
  for (const entry of await deps.mintJournal.list()) {
@@ -15383,7 +15960,8 @@ async function mintJournaled(deps, entry) {
15383
15960
  }
15384
15961
 
15385
15962
  // modules/payments-v2/mint-custom.ts
15386
- var HEX32 = /^[0-9a-f]{64}$/;
15963
+ var HEX322 = /^[0-9a-f]{64}$/;
15964
+ var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
15387
15965
  function assertRequest(request) {
15388
15966
  if (request.tokenType.length !== 32) {
15389
15967
  throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
@@ -15394,19 +15972,27 @@ function assertRequest(request) {
15394
15972
  if (request.data.length === 0) {
15395
15973
  throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
15396
15974
  }
15975
+ if (request.justification !== void 0 && request.justification.length === 0) {
15976
+ throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
15977
+ }
15978
+ if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
15979
+ throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
15980
+ }
15397
15981
  for (const asset of request.assets) {
15398
- if (!HEX32.test(asset.coinId) || asset.amount <= 0n) {
15982
+ if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
15399
15983
  throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
15400
15984
  }
15401
15985
  }
15402
15986
  }
15403
15987
  async function runCustomMintUnderJournal(deps, input) {
15988
+ let entry;
15404
15989
  try {
15405
15990
  assertRequest(input.request);
15991
+ entry = journalEntryOf2(input.mintId, input.request, deps.now());
15992
+ deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
15406
15993
  } catch (err) {
15407
15994
  return { success: false, error: messageOf(err) };
15408
15995
  }
15409
- const entry = journalEntryOf2(input.mintId, input.request, deps.now());
15410
15996
  await deps.customMintJournal.upsert(entry);
15411
15997
  let tokenId;
15412
15998
  try {
@@ -15446,24 +16032,25 @@ function journalEntryOf2(mintId, request, createdAt) {
15446
16032
  createdAt
15447
16033
  };
15448
16034
  }
16035
+ function mintParamsOf(deps, entry, mintJustificationVerifiers) {
16036
+ return {
16037
+ recipientPubkey: deps.ownPubkeyBytes,
16038
+ data: hexToBytes4(entry.dataHex),
16039
+ tokenType: hexToBytes4(entry.tokenTypeHex),
16040
+ salt: hexToBytes4(entry.saltHex),
16041
+ ...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
16042
+ ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
16043
+ };
16044
+ }
15449
16045
  async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
15450
- const token = await deps.engine.mintDataToken(
15451
- {
15452
- recipientPubkey: deps.ownPubkeyBytes,
15453
- data: hexToBytes4(entry.dataHex),
15454
- tokenType: hexToBytes4(entry.tokenTypeHex),
15455
- salt: hexToBytes4(entry.saltHex),
15456
- ...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
15457
- ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
15458
- },
15459
- { transferId: entry.mintId }
15460
- );
16046
+ const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
15461
16047
  if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
15462
16048
  throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
15463
16049
  }
15464
16050
  if (entry.tokenId === "") {
15465
16051
  await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
15466
16052
  }
16053
+ if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
15467
16054
  await finalizeMint(deps, {
15468
16055
  mintId: entry.mintId,
15469
16056
  token,
@@ -15473,108 +16060,6 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
15473
16060
  return token.blob.tokenId;
15474
16061
  }
15475
16062
 
15476
- // modules/payments-v2/burn.ts
15477
- var HEX322 = /^[0-9a-f]{64}$/;
15478
- async function heldToken2(deps, tokenId) {
15479
- const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
15480
- if (bytes === void 0) {
15481
- throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
15482
- }
15483
- return deps.engine.decodeToken({ tokenId, token: bytes });
15484
- }
15485
- function assetsOf(token) {
15486
- return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
15487
- }
15488
- async function runBurnUnderJournal(deps, input) {
15489
- const { burnId } = input;
15490
- const { tokenId, reasonBytes } = input.request;
15491
- if (!HEX322.test(tokenId) || reasonBytes.length === 0) {
15492
- return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty" };
15493
- }
15494
- let token;
15495
- try {
15496
- token = await heldToken2(deps, tokenId);
15497
- if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
15498
- throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
15499
- }
15500
- } catch (err) {
15501
- return { success: false, burnId, tokenId, error: messageOf(err) };
15502
- }
15503
- const entry = {
15504
- burnId,
15505
- tokenId,
15506
- reasonHex: bytesToHex3(reasonBytes),
15507
- burnedTokenHex: null,
15508
- settled: false,
15509
- assets: assetsOf(token),
15510
- createdAt: deps.now()
15511
- };
15512
- await deps.burnJournal.upsert(entry);
15513
- try {
15514
- const settled = await burnJournaled(deps, entry, token, { applied: false });
15515
- return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
15516
- } catch (err) {
15517
- deps.armHeartbeat();
15518
- return { success: false, burnId, tokenId, error: messageOf(err) };
15519
- }
15520
- }
15521
- async function replayBurns(deps) {
15522
- let progressed = 0;
15523
- for (const entry of await deps.burnJournal.list()) {
15524
- if (entry.settled || deps.isActiveOp(entry.burnId)) continue;
15525
- try {
15526
- const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
15527
- const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
15528
- await burnJournaled(deps, entry, token, { applied });
15529
- progressed += 1;
15530
- } catch {
15531
- continue;
15532
- }
15533
- }
15534
- return progressed;
15535
- }
15536
- async function pendingBurns(deps) {
15537
- return (await deps.burnJournal.list()).map((entry) => ({
15538
- burnId: entry.burnId,
15539
- tokenId: entry.tokenId,
15540
- reasonBytes: hexToBytes4(entry.reasonHex),
15541
- burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
15542
- settled: entry.settled,
15543
- createdAt: entry.createdAt
15544
- }));
15545
- }
15546
- async function acknowledgeBurn(deps, burnId) {
15547
- const entry = await deps.burnJournal.getByKey(burnId);
15548
- if (entry === void 0) return;
15549
- if (!entry.settled) {
15550
- throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
15551
- }
15552
- await deps.burnJournal.removeByKey(burnId);
15553
- }
15554
- async function burnJournaled(deps, entry, token, state) {
15555
- let current = entry;
15556
- if (current.burnedTokenHex === null) {
15557
- if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
15558
- const burned = await deps.engine.burn(
15559
- { token, reasonBytes: hexToBytes4(current.reasonHex) },
15560
- { transferId: current.burnId }
15561
- );
15562
- if (burned.blob.tokenId !== current.tokenId) {
15563
- throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
15564
- }
15565
- current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
15566
- await deps.burnJournal.upsert(current);
15567
- }
15568
- if (!state.applied) {
15569
- await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [] });
15570
- }
15571
- await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
15572
- current = { ...current, settled: true };
15573
- await deps.burnJournal.upsert(current);
15574
- deps.refreshView();
15575
- return current;
15576
- }
15577
-
15578
16063
  // modules/payments-v2/send-whole.ts
15579
16064
  async function materializeWholeSpend(deps, input) {
15580
16065
  const { engine } = deps;
@@ -15770,6 +16255,7 @@ var PaymentsFacade = class {
15770
16255
  track: (op) => this.trackTail(op)
15771
16256
  });
15772
16257
  this.view = parts.view;
16258
+ this.verdicts = parts.verdicts;
15773
16259
  this.wakeRefresh = parts.refreshView;
15774
16260
  this.ledger = parts.ledger;
15775
16261
  this.queue = parts.queue;
@@ -15783,6 +16269,7 @@ var PaymentsFacade = class {
15783
16269
  this.restoreDeps = parts.restoreDeps;
15784
16270
  this.requests = parts.requests;
15785
16271
  this.pins = parts.pins;
16272
+ this.burnHold = parts.burnHold;
15786
16273
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
15787
16274
  this.heartbeat = new ConvergenceHeartbeat({
15788
16275
  now: () => this.nowMs(),
@@ -15806,6 +16293,7 @@ var PaymentsFacade = class {
15806
16293
  wakeRefresh;
15807
16294
  prewarmed = new PrewarmCache();
15808
16295
  view;
16296
+ verdicts;
15809
16297
  ledger;
15810
16298
  queue;
15811
16299
  machine;
@@ -15832,6 +16320,7 @@ var PaymentsFacade = class {
15832
16320
  activeMoneyOps = /* @__PURE__ */ new Set();
15833
16321
  /** #737: the ledger holds exactly the sources of the still-open intents. */
15834
16322
  pins;
16323
+ burnHold;
15835
16324
  /** Per facade, so per address: an NFT reading is never served to another wallet. */
15836
16325
  nftCache = new NftCache();
15837
16326
  // ── lifecycle ──────────────────────────────────────────────────────────────
@@ -15856,7 +16345,8 @@ var PaymentsFacade = class {
15856
16345
  await this.deps.session.start();
15857
16346
  await this.pins.sync().catch(() => void 0);
15858
16347
  this.trackTail(this.runConvergencePass());
15859
- this.trackTail(this.seedHeldStates());
16348
+ this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
16349
+ await this.verdicts.start().catch(() => void 0);
15860
16350
  await this.view.fullPull().catch(() => void 0);
15861
16351
  this.receiveLoop.start(this.deps.receivePollMs);
15862
16352
  }
@@ -15865,6 +16355,7 @@ var PaymentsFacade = class {
15865
16355
  this.started = false;
15866
16356
  this.heartbeat.stop();
15867
16357
  this.receiveLoop.stop();
16358
+ this.verdicts.stop();
15868
16359
  for (const unsubscribe of this.unsubscribers) unsubscribe();
15869
16360
  this.unsubscribers = [];
15870
16361
  await this.deps.session.stop();
@@ -15881,12 +16372,17 @@ var PaymentsFacade = class {
15881
16372
  }
15882
16373
  // ── reads ──────────────────────────────────────────────────────────────────
15883
16374
  async assets(coinId) {
16375
+ return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
16376
+ }
16377
+ unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
16378
+ async heldAssets(coinId) {
15884
16379
  const all = await this.view.assets(this.deps.registry, this.deps.price);
15885
16380
  return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
15886
16381
  }
15887
16382
  tokens(filter) {
15888
- return this.view.tokens(this.deps.registry, filter);
16383
+ return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
15889
16384
  }
16385
+ unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
15890
16386
  coinless() {
15891
16387
  return this.view.coinless(this.deps.registry);
15892
16388
  }
@@ -15947,9 +16443,9 @@ var PaymentsFacade = class {
15947
16443
  return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
15948
16444
  }
15949
16445
  mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
15950
- burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
15951
- pendingBurns = () => this.track(pendingBurns(this.mintDeps()));
15952
- acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.mintDeps(), burnId));
16446
+ burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
16447
+ pendingBurns = () => this.track(pendingBurns(this.machineStores));
16448
+ acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
15953
16449
  // §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
15954
16450
  async resumeNow() {
15955
16451
  if (!this.started) return;
@@ -15992,7 +16488,8 @@ var PaymentsFacade = class {
15992
16488
  isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
15993
16489
  tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
15994
16490
  emit: this.deps.emit,
15995
- now: () => this.nowMs()
16491
+ now: () => this.nowMs(),
16492
+ accepted: (token) => this.verdicts.accept(token)
15996
16493
  };
15997
16494
  }
15998
16495
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
@@ -16330,16 +16827,6 @@ var PaymentsFacade = class {
16330
16827
  newId() {
16331
16828
  return (this.deps.newId ?? randomUUID)();
16332
16829
  }
16333
- async seedHeldStates() {
16334
- let page = await this.deps.storagePort.listInventory();
16335
- for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
16336
- for (const item of page.items) {
16337
- if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
16338
- }
16339
- if (!page.more) return;
16340
- page = await this.deps.storagePort.listInventory(page.cursor);
16341
- }
16342
- }
16343
16830
  track(op) {
16344
16831
  const settled = op.then(() => void 0, () => void 0);
16345
16832
  this.pendingOps.add(settled);
@@ -17583,6 +18070,7 @@ function composePaymentsV2(spec) {
17583
18070
  engineRef: spec.engineRef,
17584
18071
  kv,
17585
18072
  registry: host.registry,
18073
+ claims: CoinClaims.fromPlugins(spec.plugins),
17586
18074
  ...host.price !== null ? { price: host.price } : {},
17587
18075
  emit: host.emit,
17588
18076
  resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
@@ -20631,7 +21119,8 @@ var Sphere = class _Sphere {
20631
21119
  // deliver the NEXT address's Unicity ID — a name the sending key does
20632
21120
  // not own (sphere#487 review).
20633
21121
  nametag: () => this.getNametagForAddress(facadeAddressId)
20634
- }
21122
+ },
21123
+ ...this._plugins ? { plugins: this._plugins } : {}
20635
21124
  });
20636
21125
  this._paymentsV2Active = { index, facade };
20637
21126
  await facade.start();
@@ -21259,6 +21748,7 @@ function addressesMatch(a, b) {
21259
21748
  0 && (module.exports = {
21260
21749
  AUTH_CHALLENGE_PREFIX,
21261
21750
  COIN_TYPES,
21751
+ CUSTOM_MINT_MAX_PAYLOAD_BYTES,
21262
21752
  ChallengeTemplateError,
21263
21753
  CoinGeckoPriceProvider,
21264
21754
  CommunicationsModule,