@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
@@ -9597,6 +9597,150 @@ async function deriveDirectAddress(publicKey) {
9597
9597
  return `DIRECT://${import_HexConverter.HexConverter.encode(imprint)}${import_HexConverter.HexConverter.encode(checksum)}`;
9598
9598
  }
9599
9599
 
9600
+ // token-engine/claims.ts
9601
+ var CoinClaims = class _CoinClaims {
9602
+ issuers = /* @__PURE__ */ new Map();
9603
+ policies = [];
9604
+ static fromPlugins(plugins) {
9605
+ const claims = new _CoinClaims();
9606
+ for (const plugin of plugins ?? []) {
9607
+ for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
9608
+ }
9609
+ return claims;
9610
+ }
9611
+ add(policy) {
9612
+ const tokenType = import_HexConverter.HexConverter.encode(policy.tokenType.bytes);
9613
+ const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
9614
+ const taken = coinIds.find((coinId) => this.issuers.has(coinId));
9615
+ if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
9616
+ for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
9617
+ this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
9618
+ }
9619
+ fingerprint() {
9620
+ return [...this.policies].sort().join("\n");
9621
+ }
9622
+ issuerOf(coinId) {
9623
+ return this.issuers.get(coinId.toLowerCase()) ?? null;
9624
+ }
9625
+ };
9626
+
9627
+ // token-engine/errors.ts
9628
+ var TransferConflictError = class extends SphereError {
9629
+ /**
9630
+ * #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
9631
+ * Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
9632
+ */
9633
+ conflictedSourceId;
9634
+ constructor(message, cause) {
9635
+ super(message, "TRANSFER_CONFLICT", cause);
9636
+ this.name = "TransferConflictError";
9637
+ }
9638
+ };
9639
+ var MintReasonUnverifiableError = class extends SphereError {
9640
+ constructor(message, cause) {
9641
+ super(message, "MINT_REASON_UNVERIFIABLE", cause);
9642
+ this.name = "MintReasonUnverifiableError";
9643
+ }
9644
+ };
9645
+ var ProofUnconfirmedError = class extends SphereError {
9646
+ mayHaveCertified = true;
9647
+ constructor(message, cause) {
9648
+ super(message, "CERTIFICATION_UNCONFIRMED", cause);
9649
+ this.name = "ProofUnconfirmedError";
9650
+ }
9651
+ };
9652
+ var CheckpointPersistFailedError = class extends SphereError {
9653
+ mayHaveCertified = true;
9654
+ constructor(message, cause) {
9655
+ super(message, "CHECKPOINT_PERSIST_FAILED", cause);
9656
+ this.name = "CheckpointPersistFailedError";
9657
+ }
9658
+ };
9659
+ var SplitCheckpointLostError = class extends SphereError {
9660
+ constructor(message, cause) {
9661
+ super(message, "SPLIT_CHECKPOINT_LOST", cause);
9662
+ this.name = "SplitCheckpointLostError";
9663
+ }
9664
+ };
9665
+ var CheckpointTrustbaseMismatchError = class extends SphereError {
9666
+ constructor(message, cause) {
9667
+ super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
9668
+ this.name = "CheckpointTrustbaseMismatchError";
9669
+ }
9670
+ };
9671
+ function isKeepOpenSplitError(err) {
9672
+ return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
9673
+ }
9674
+
9675
+ // token-engine/mint-reasons.ts
9676
+ var RULE = "MintJustificationVerification";
9677
+ var MintReasonRegistry = class _MintReasonRegistry extends import_MintJustificationVerifierService.MintJustificationVerifierService {
9678
+ byTag = /* @__PURE__ */ new Map();
9679
+ pluginTags = /* @__PURE__ */ new Set();
9680
+ register(verifier) {
9681
+ super.register(verifier);
9682
+ this.byTag.set(verifier.tag, verifier);
9683
+ return this;
9684
+ }
9685
+ registerPlugin(verifier) {
9686
+ this.register(verifier);
9687
+ this.pluginTags.add(verifier.tag);
9688
+ return this;
9689
+ }
9690
+ async verify(transaction, nestedTokenCollector) {
9691
+ const tag = transaction.justification ? reasonTag(transaction.justification) : null;
9692
+ if (tag === null) return super.verify(transaction, nestedTokenCollector);
9693
+ const verifier = this.byTag.get(tag);
9694
+ if (verifier === void 0) throw new MintReasonUnverifiableError(`No verifier is registered for mint-reason tag ${tag}`);
9695
+ if (!this.pluginTags.has(tag)) return super.verify(transaction, nestedTokenCollector);
9696
+ return verifyPlugin(verifier, transaction, nestedTokenCollector);
9697
+ }
9698
+ knows(tag) {
9699
+ return this.byTag.has(tag);
9700
+ }
9701
+ /** `verifiers` replace the registered ones of their tags; a tag this registry lacks is refused. */
9702
+ overlaid(verifiers) {
9703
+ for (const verifier of verifiers) assertKnown(this, verifier.tag);
9704
+ const replaced = new Map(verifiers.map((verifier) => [verifier.tag, verifier]));
9705
+ const overlay = new _MintReasonRegistry();
9706
+ for (const [tag, verifier] of this.byTag) {
9707
+ const perCall = replaced.get(tag);
9708
+ if (perCall !== void 0 || this.pluginTags.has(tag)) overlay.registerPlugin(perCall ?? verifier);
9709
+ else overlay.register(verifier);
9710
+ }
9711
+ return overlay;
9712
+ }
9713
+ assertVerifiable(justification) {
9714
+ const tag = reasonTag(justification);
9715
+ if (tag === null) throw new SphereError("The mint reason is not tagged CBOR", "VALIDATION_ERROR");
9716
+ assertKnown(this, tag);
9717
+ }
9718
+ };
9719
+ function reasonTag(justification) {
9720
+ try {
9721
+ return BigInt(import_CborDeserializer.CborDeserializer.decodeTag(justification).tag);
9722
+ } catch {
9723
+ return null;
9724
+ }
9725
+ }
9726
+ async function verifyPlugin(verifier, transaction, nestedTokenCollector) {
9727
+ let result;
9728
+ try {
9729
+ result = await verifier.verify(transaction, nestedTokenCollector);
9730
+ } catch (err) {
9731
+ const why = err instanceof Error ? err.message : String(err);
9732
+ throw new MintReasonUnverifiableError(`Mint-reason tag ${verifier.tag} cannot be verified yet: ${why}`, err);
9733
+ }
9734
+ 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]);
9735
+ }
9736
+ function assertKnown(registry, tag) {
9737
+ if (registry.knows(tag)) return;
9738
+ throw new SphereError(
9739
+ `No verifier is registered on the engine for mint-reason tag ${tag}, so a replay of this mint could never verify`,
9740
+ "VALIDATION_ERROR"
9741
+ );
9742
+ }
9743
+
9600
9744
  // token-engine/SpherePaymentData.ts
9601
9745
  var COIN_ID_PATTERN = /^([0-9a-f]{2})+$/;
9602
9746
  function assertAsset(coinId, amount) {
@@ -9773,45 +9917,6 @@ async function awaitProofBounded(deps, transaction, signal) {
9773
9917
  return value;
9774
9918
  }
9775
9919
 
9776
- // token-engine/errors.ts
9777
- var TransferConflictError = class extends SphereError {
9778
- /**
9779
- * #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
9780
- * Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
9781
- */
9782
- conflictedSourceId;
9783
- constructor(message, cause) {
9784
- super(message, "TRANSFER_CONFLICT", cause);
9785
- this.name = "TransferConflictError";
9786
- }
9787
- };
9788
- var ProofUnconfirmedError = class extends SphereError {
9789
- mayHaveCertified = true;
9790
- constructor(message, cause) {
9791
- super(message, "CERTIFICATION_UNCONFIRMED", cause);
9792
- this.name = "ProofUnconfirmedError";
9793
- }
9794
- };
9795
- var CheckpointPersistFailedError = class extends SphereError {
9796
- mayHaveCertified = true;
9797
- constructor(message, cause) {
9798
- super(message, "CHECKPOINT_PERSIST_FAILED", cause);
9799
- this.name = "CheckpointPersistFailedError";
9800
- }
9801
- };
9802
- var SplitCheckpointLostError = class extends SphereError {
9803
- constructor(message, cause) {
9804
- super(message, "SPLIT_CHECKPOINT_LOST", cause);
9805
- this.name = "SplitCheckpointLostError";
9806
- }
9807
- };
9808
- var CheckpointTrustbaseMismatchError = class extends SphereError {
9809
- constructor(message, cause) {
9810
- super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
9811
- this.name = "CheckpointTrustbaseMismatchError";
9812
- }
9813
- };
9814
-
9815
9920
  // token-engine/certification-outcome.ts
9816
9921
  function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
9817
9922
  if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
@@ -10374,15 +10479,17 @@ async function readTokenNft(token) {
10374
10479
  }
10375
10480
 
10376
10481
  // token-engine/plugin-ops.ts
10377
- function contextWithMintVerifiers(base, verifiers) {
10378
- const registry = new import_MintJustificationVerifierService.MintJustificationVerifierService();
10379
- for (const verifier of verifiers) registry.register(verifier);
10482
+ function mintContext(deps, params) {
10483
+ assertMintableData(params.data);
10484
+ if (params.justification !== void 0) deps.mintJustificationVerifier.assertVerifiable(params.justification);
10485
+ const verifiers = params.mintJustificationVerifiers;
10486
+ if (verifiers === void 0) return deps.verificationContext;
10380
10487
  return new import_VerificationContext.VerificationContext(
10381
- base.trustBase,
10382
- base.predicateVerifier,
10383
- base.unicityCertificateVerifier,
10384
- registry,
10385
- new import_TokenIssuanceVerifierService.TokenIssuanceVerifierService(false)
10488
+ deps.trustBase,
10489
+ deps.predicateVerifier,
10490
+ deps.unicityCertificateVerifier,
10491
+ deps.mintJustificationVerifier.overlaid(verifiers),
10492
+ deps.verificationContext.tokenIssuanceVerifier
10386
10493
  );
10387
10494
  }
10388
10495
 
@@ -10470,9 +10577,6 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
10470
10577
  // token-engine/SphereTokenEngine.ts
10471
10578
  var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
10472
10579
  var MAX_MINT_CONCURRENCY = 8;
10473
- function isKeepOpenSplitError(err) {
10474
- return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
10475
- }
10476
10580
  var TRANSFER_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
10477
10581
  var CLEAN_REJECT_STATUSES = /* @__PURE__ */ new Set([
10478
10582
  import_CertificationResponse.CertificationStatus.STATE_ID_MISMATCH,
@@ -10562,8 +10666,9 @@ var SphereTokenEngine = class {
10562
10666
  const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
10563
10667
  return wrapToken(token);
10564
10668
  }
10669
+ assertMintable = (params) => void mintContext(this.deps, params);
10565
10670
  async mintDataToken(params, options) {
10566
- assertMintableData(params.data);
10671
+ const context = mintContext(this.deps, params);
10567
10672
  const recipient = import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey);
10568
10673
  const tokenType = params.tokenType ? new import_TokenType.TokenType(params.tokenType) : import_TokenType.TokenType.generate();
10569
10674
  const salt = params.salt ? import_TokenSalt.TokenSalt.fromBytes(params.salt) : import_TokenSalt.TokenSalt.generate();
@@ -10577,8 +10682,7 @@ var SphereTokenEngine = class {
10577
10682
  options
10578
10683
  );
10579
10684
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10580
- const verifiers = params.mintJustificationVerifiers;
10581
- return wrapToken(await import_Token.Token.mint(certified, verifiers ? contextWithMintVerifiers(this.deps, verifiers) : this.deps.verificationContext));
10685
+ return wrapToken(await import_Token.Token.mint(certified, context));
10582
10686
  }
10583
10687
  buildNftMint(params) {
10584
10688
  return planNftMint(this.deps, params);
@@ -10915,20 +11019,30 @@ var SphereTokenEngine = class {
10915
11019
 
10916
11020
  // token-engine/factory.ts
10917
11021
  var DEFAULT_VERIFICATION_POOL_SIZE = 4;
10918
- function registerPluginVerifiers(registry, plugins) {
11022
+ function registerPlugins(registries, plugins) {
10919
11023
  for (const plugin of plugins ?? []) {
10920
11024
  for (const verifier of plugin.mintJustificationVerifiers ?? []) {
10921
- try {
10922
- registry.register(verifier);
10923
- } catch (err) {
10924
- throw new SphereError(
10925
- `Token plugin '${plugin.id}' registers mint-reason tag ${verifier.tag} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
10926
- "INVALID_CONFIG"
10927
- );
10928
- }
11025
+ claim(plugin, `mint-reason tag ${verifier.tag}`, () => registries.reasons.registerPlugin(verifier));
11026
+ }
11027
+ for (const policy of plugin.tokenIssuancePolicies ?? []) {
11028
+ const tokenType = import_HexConverter.HexConverter.encode(policy.tokenType.bytes);
11029
+ claim(plugin, `an issuance policy for token type ${tokenType}`, () => {
11030
+ registries.claims.add(policy);
11031
+ registries.policies.register(policy);
11032
+ });
10929
11033
  }
10930
11034
  }
10931
11035
  }
11036
+ function claim(plugin, what, register) {
11037
+ try {
11038
+ register();
11039
+ } catch (err) {
11040
+ throw new SphereError(
11041
+ `Token plugin '${plugin.id}' registers ${what} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
11042
+ "INVALID_CONFIG"
11043
+ );
11044
+ }
11045
+ }
10932
11046
  function disposedError() {
10933
11047
  return new SphereError(
10934
11048
  "Verification worker pool disposed \u2014 the in-flight verification was cancelled",
@@ -11006,11 +11120,15 @@ async function createSphereTokenEngine(config) {
11006
11120
  const unicityCertificateVerifier = new import_UnicityCertificateVerifier.UnicityCertificateVerifier(
11007
11121
  new import_UnicitySealQuorumSignaturesVerificationRule.UnicitySealQuorumSignaturesVerificationRule(new import_Secp256k1SignatureVerifier.Secp256k1SignatureVerifier(), new import_VerifiedSealCache.VerifiedSealCache(256))
11008
11122
  );
11009
- const mintJustificationVerifier = new import_MintJustificationVerifierService.MintJustificationVerifierService();
11123
+ const mintJustificationVerifier = new MintReasonRegistry();
11010
11124
  mintJustificationVerifier.register(
11011
11125
  new import_SplitMintJustificationVerifier.SplitMintJustificationVerifier(decodeSpherePaymentData)
11012
11126
  );
11013
- registerPluginVerifiers(mintJustificationVerifier, config.plugins);
11127
+ const tokenIssuanceVerifier = new import_TokenIssuanceVerifierService.TokenIssuanceVerifierService(false);
11128
+ registerPlugins(
11129
+ { reasons: mintJustificationVerifier, policies: tokenIssuanceVerifier, claims: new CoinClaims() },
11130
+ config.plugins
11131
+ );
11014
11132
  const deps = {
11015
11133
  client: new import_StateTransitionClient.StateTransitionClient(new import_AggregatorClient.AggregatorClient(config.aggregatorUrl, config.apiKey ?? null)),
11016
11134
  trustBase,
@@ -11022,7 +11140,7 @@ async function createSphereTokenEngine(config) {
11022
11140
  predicateVerifier,
11023
11141
  unicityCertificateVerifier,
11024
11142
  mintJustificationVerifier,
11025
- new import_TokenIssuanceVerifierService.TokenIssuanceVerifierService(false)
11143
+ tokenIssuanceVerifier
11026
11144
  ),
11027
11145
  signingService: new import_SigningService.SigningService(config.privateKey),
11028
11146
  // Also the HKDF ikm for deterministic realization (Part E.1) — the
@@ -12122,6 +12240,9 @@ function createScopedKV(storage, network, chainPubkey) {
12122
12240
  }
12123
12241
  };
12124
12242
  }
12243
+ function isLiveBurn(entry) {
12244
+ return !entry.settled && entry.failure === void 0;
12245
+ }
12125
12246
  var STORE_KEYS = {
12126
12247
  intentBackstop: "intents",
12127
12248
  checkpointCache: "checkpoints",
@@ -12136,7 +12257,8 @@ var STORE_KEYS = {
12136
12257
  epochLatch: "epoch-latch",
12137
12258
  // §5.2 InventoryView durable overlays (#625/#679).
12138
12259
  suspectedSpent: "suspected-spent",
12139
- knownSpends: "known-spends"
12260
+ knownSpends: "known-spends",
12261
+ verifiedTokens: "verified-tokens"
12140
12262
  };
12141
12263
 
12142
12264
  // modules/payments-v2/machine/journal.ts
@@ -12653,6 +12775,7 @@ var TransferMachine = class {
12653
12775
  const key = keys.get(digest);
12654
12776
  if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
12655
12777
  const { tokenId } = await engine.deliveryKeys(o.changeBlob);
12778
+ await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
12656
12779
  added.push({ tokenId, key });
12657
12780
  }
12658
12781
  return added;
@@ -13048,7 +13171,7 @@ var Converger = class {
13048
13171
  this.deps.stores.burnJournal.list()
13049
13172
  ]);
13050
13173
  const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
13051
- const burnsOpen = burns.some((e) => !e.settled);
13174
+ const burnsOpen = burns.some(isLiveBurn);
13052
13175
  return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
13053
13176
  }
13054
13177
  async resumePass() {
@@ -13135,6 +13258,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
13135
13258
  const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
13136
13259
  holds.set(entry.transferId, [...direct, ...split]);
13137
13260
  }
13261
+ for (const burn of await stores.burnJournal.list()) {
13262
+ if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
13263
+ }
13138
13264
  return { open: holds, complete };
13139
13265
  }
13140
13266
  function groupJournal(journal) {
@@ -13445,18 +13571,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
13445
13571
  createdAt: snapshot.createdAt,
13446
13572
  updatedAt: snapshot.updatedAt,
13447
13573
  lazy: true,
13448
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
13574
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {},
13575
+ ...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
13449
13576
  };
13450
13577
  }
13451
- function aggregateAssets(entries, isTransferring, registry) {
13578
+ function aggregateAssets(entries, reading, registry) {
13452
13579
  const groups = /* @__PURE__ */ new Map();
13453
13580
  for (const [tokenId, entry] of entries) {
13454
- const moving = isTransferring(tokenId);
13581
+ const moving = reading.isTransferring(tokenId);
13455
13582
  for (const asset of entry.assets) {
13456
- let group = groups.get(asset.coinId);
13583
+ const standing = reading.standing(tokenId, asset.coinId);
13584
+ const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
13585
+ let group = groups.get(key);
13457
13586
  if (!group) {
13458
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
13459
- groups.set(asset.coinId, group);
13587
+ group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
13588
+ groups.set(key, group);
13460
13589
  }
13461
13590
  if (moving) {
13462
13591
  group.transferring += BigInt(asset.amount);
@@ -13467,9 +13596,10 @@ function aggregateAssets(entries, isTransferring, registry) {
13467
13596
  }
13468
13597
  }
13469
13598
  }
13470
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
13599
+ return [...groups.values()].map((totals) => toAsset(totals, registry));
13471
13600
  }
13472
- function toAsset(coinId, totals, registry) {
13601
+ function toAsset(totals, registry) {
13602
+ const coinId = totals.coinId;
13473
13603
  const iconUrl = registry.getIconUrl(coinId);
13474
13604
  return {
13475
13605
  coinId,
@@ -13489,7 +13619,8 @@ function toAsset(coinId, totals, registry) {
13489
13619
  priceEur: null,
13490
13620
  change24h: null,
13491
13621
  fiatValueUsd: null,
13492
- fiatValueEur: null
13622
+ fiatValueEur: null,
13623
+ ...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
13493
13624
  };
13494
13625
  }
13495
13626
  function priceIdOf(registry, coinId) {
@@ -13497,9 +13628,9 @@ function priceIdOf(registry, coinId) {
13497
13628
  }
13498
13629
  async function withPrices(raw, registry, price) {
13499
13630
  try {
13500
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
13631
+ const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
13501
13632
  const quotes = await price.getPrices(ids);
13502
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13633
+ return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13503
13634
  } catch {
13504
13635
  return raw;
13505
13636
  }
@@ -13520,11 +13651,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
13520
13651
  const snapshot = { assets: [], createdAt: now, updatedAt: now };
13521
13652
  return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
13522
13653
  transferring: true,
13523
- suspectedSpent: false
13654
+ suspectedSpent: false,
13655
+ standing: "trusted"
13524
13656
  });
13525
13657
  }
13526
13658
 
13527
13659
  // modules/payments-v2/inventory/InventoryView.ts
13660
+ var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
13528
13661
  var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
13529
13662
  var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
13530
13663
  var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
@@ -13618,6 +13751,12 @@ var InventoryView = class {
13618
13751
  for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
13619
13752
  if (changed) this.deps.emit("inventory:updated");
13620
13753
  }
13754
+ get trust() {
13755
+ return this.deps.trust ?? TRUST_ALL;
13756
+ }
13757
+ trusts(tokenId, coinId) {
13758
+ return this.trust.standing(tokenId, coinId) === "trusted";
13759
+ }
13621
13760
  pinned(tokenId) {
13622
13761
  return this.deps.isPinned?.(tokenId) ?? false;
13623
13762
  }
@@ -13644,6 +13783,7 @@ var InventoryView = class {
13644
13783
  isSpendable(tokenId, entry) {
13645
13784
  if (entry.status !== "active") return false;
13646
13785
  if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13786
+ if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
13647
13787
  return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13648
13788
  }
13649
13789
  /** Is this NAMED token spendable whole — coinless or valued (#777)? */
@@ -13670,7 +13810,8 @@ var InventoryView = class {
13670
13810
  out.push(
13671
13811
  toToken(tokenId, entry, asset, registry, {
13672
13812
  transferring: this.held(tokenId),
13673
- suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
13813
+ suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
13814
+ standing: this.trust.standing(tokenId, asset.coinId)
13674
13815
  })
13675
13816
  );
13676
13817
  }
@@ -13700,10 +13841,21 @@ var InventoryView = class {
13700
13841
  return out;
13701
13842
  }
13702
13843
  async assets(registry, price) {
13703
- const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
13844
+ const reading = {
13845
+ isTransferring: (tokenId) => this.held(tokenId),
13846
+ standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
13847
+ };
13848
+ const raw = aggregateAssets(this.activeEntries(), reading, registry);
13704
13849
  if (!price || raw.length === 0) return raw;
13705
13850
  return withPrices(raw, registry, price);
13706
13851
  }
13852
+ claimHolders() {
13853
+ const out = [];
13854
+ for (const [tokenId, entry] of this.activeEntries()) {
13855
+ if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
13856
+ }
13857
+ return out;
13858
+ }
13707
13859
  *activeEntries() {
13708
13860
  for (const [tokenId, entry] of this.mirror) {
13709
13861
  if (entry.status === "active") yield [tokenId, entry];
@@ -13873,6 +14025,171 @@ var InventoryView = class {
13873
14025
  }
13874
14026
  };
13875
14027
 
14028
+ // modules/payments-v2/inventory/verdicts.ts
14029
+ var VERDICT_RETRY_MS = 3e4;
14030
+ var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
14031
+ function honoursClaims(claims, token) {
14032
+ const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
14033
+ return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
14034
+ }
14035
+ var TokenVerdicts = class {
14036
+ constructor(deps) {
14037
+ this.deps = deps;
14038
+ }
14039
+ verified = /* @__PURE__ */ new Set();
14040
+ refused = /* @__PURE__ */ new Set();
14041
+ retries = /* @__PURE__ */ new Map();
14042
+ writes = new SerialChain();
14043
+ hydration = null;
14044
+ queued = null;
14045
+ running = null;
14046
+ held = [];
14047
+ timer = null;
14048
+ live = false;
14049
+ isClaimed(coinId) {
14050
+ return this.deps.claims.issuerOf(coinId) !== null;
14051
+ }
14052
+ standing(tokenId, coinId) {
14053
+ if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
14054
+ return this.refused.has(tokenId) ? "refused" : "pending";
14055
+ }
14056
+ trusts(tokenId, coinId) {
14057
+ return this.standing(tokenId, coinId) === "trusted";
14058
+ }
14059
+ hydrate() {
14060
+ this.hydration ??= this.load().catch((error) => {
14061
+ this.hydration = null;
14062
+ throw error;
14063
+ });
14064
+ return this.hydration;
14065
+ }
14066
+ start() {
14067
+ this.live = true;
14068
+ return this.hydrate();
14069
+ }
14070
+ stop() {
14071
+ this.live = false;
14072
+ if (this.timer !== null) clearTimeout(this.timer);
14073
+ this.timer = null;
14074
+ }
14075
+ async accept(token) {
14076
+ if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
14077
+ }
14078
+ async recordSplit(sourceTokenId, outputTokenId) {
14079
+ await this.hydrate();
14080
+ if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
14081
+ }
14082
+ review(holders) {
14083
+ this.queued = holders;
14084
+ this.running ??= this.drain();
14085
+ return this.running;
14086
+ }
14087
+ async drain() {
14088
+ try {
14089
+ while (this.queued !== null) {
14090
+ const holders = this.queued;
14091
+ this.queued = null;
14092
+ await this.reviewOnce(holders);
14093
+ }
14094
+ } finally {
14095
+ this.running = null;
14096
+ }
14097
+ }
14098
+ async reviewOnce(holders) {
14099
+ await this.hydrate();
14100
+ this.held = holders;
14101
+ const due = holders.filter((tokenId) => this.isDue(tokenId));
14102
+ const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
14103
+ for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
14104
+ if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
14105
+ await this.persist();
14106
+ this.deps.changed();
14107
+ }
14108
+ this.scheduleRetry();
14109
+ }
14110
+ isDue(tokenId) {
14111
+ if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
14112
+ return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
14113
+ }
14114
+ async check(due) {
14115
+ const verdicts = /* @__PURE__ */ new Map();
14116
+ let blobs;
14117
+ try {
14118
+ blobs = await this.deps.getBlobs([...due]);
14119
+ } catch {
14120
+ return verdicts;
14121
+ }
14122
+ for (const tokenId of due) {
14123
+ const bytes = blobs.get(tokenId);
14124
+ if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
14125
+ }
14126
+ return verdicts;
14127
+ }
14128
+ async verdictOf(tokenId, bytes) {
14129
+ let engine;
14130
+ try {
14131
+ engine = this.deps.engine();
14132
+ } catch {
14133
+ return "retry";
14134
+ }
14135
+ let token;
14136
+ try {
14137
+ token = await engine.decodeToken({ tokenId, token: bytes });
14138
+ } catch {
14139
+ return "refused";
14140
+ }
14141
+ if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
14142
+ try {
14143
+ return (await engine.verify(token)).ok ? "verified" : "refused";
14144
+ } catch {
14145
+ return "retry";
14146
+ }
14147
+ }
14148
+ settle(tokenId, verdict) {
14149
+ if (verdict === "retry") {
14150
+ this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
14151
+ return;
14152
+ }
14153
+ this.retries.delete(tokenId);
14154
+ (verdict === "verified" ? this.verified : this.refused).add(tokenId);
14155
+ }
14156
+ nextRetry(previous) {
14157
+ const attempts = (previous?.attempts ?? 0) + 1;
14158
+ const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
14159
+ return { attempts, atMs: this.now() + delay };
14160
+ }
14161
+ scheduleRetry() {
14162
+ if (!this.live || this.timer !== null) return;
14163
+ const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
14164
+ if (due.length === 0) return;
14165
+ const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
14166
+ this.timer = setTimeout(() => {
14167
+ this.timer = null;
14168
+ void this.review(this.held).catch(() => void 0);
14169
+ }, delay);
14170
+ }
14171
+ async remember(tokenId) {
14172
+ await this.hydrate();
14173
+ if (this.verified.has(tokenId)) return;
14174
+ this.verified.add(tokenId);
14175
+ this.refused.delete(tokenId);
14176
+ this.retries.delete(tokenId);
14177
+ await this.persist();
14178
+ }
14179
+ persist() {
14180
+ const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
14181
+ return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
14182
+ }
14183
+ async load() {
14184
+ const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
14185
+ if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
14186
+ for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
14187
+ }
14188
+ now() {
14189
+ return (this.deps.now ?? Date.now)();
14190
+ }
14191
+ };
14192
+
13876
14193
  // modules/payments-v2/ports.ts
13877
14194
  function isRetryableAckError(err) {
13878
14195
  return err !== null && typeof err === "object" && err.retryable === true;
@@ -13882,6 +14199,8 @@ function isRetryableAckError(err) {
13882
14199
  var ACK_BATCH_SIZE = 200;
13883
14200
  var REFRESH_INTERVAL_MS = 2500;
13884
14201
  var POLL_INTERVAL_MS = 3e4;
14202
+ var RECHECK_BASE_MS = POLL_INTERVAL_MS;
14203
+ var RECHECK_MAX_MS = 60 * 60 * 1e3;
13885
14204
  var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
13886
14205
  function receivedDedupKey(tokenId, stateHash) {
13887
14206
  return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
@@ -13898,6 +14217,7 @@ var Receive = class {
13898
14217
  pollTimer = null;
13899
14218
  unsubscribeWake = null;
13900
14219
  lastRefreshAt = 0;
14220
+ rechecks = /* @__PURE__ */ new Map();
13901
14221
  drainOnce() {
13902
14222
  return this.drainFlight.run(() => this.doDrain());
13903
14223
  }
@@ -13956,6 +14276,7 @@ var Receive = class {
13956
14276
  const pending = [];
13957
14277
  const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
13958
14278
  const claimable = { n: 0 };
14279
+ const deferred = { any: false };
13959
14280
  const finish = () => {
13960
14281
  if (claimable.n > 0) deps.refreshView?.();
13961
14282
  return stored;
@@ -13964,7 +14285,7 @@ var Receive = class {
13964
14285
  const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
13965
14286
  let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
13966
14287
  for (let pass = 0; pass < 2; pass++) {
13967
- await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
14288
+ await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
13968
14289
  const served = deps.delivery.incomingEpoch();
13969
14290
  if (basis === null || served === null || served === basis.syncEpoch) break;
13970
14291
  await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
@@ -13981,20 +14302,41 @@ var Receive = class {
13981
14302
  return finish();
13982
14303
  }
13983
14304
  async processEntry(ctx, entry) {
13984
- const { deps, engine, pending, stored, claimable } = ctx;
13985
- const screened = await screen(deps, engine, entry);
14305
+ const { deps, engine, stored, claimable } = ctx;
14306
+ const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
14307
+ this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
14308
+ if (screened.kind === "defer") {
14309
+ ctx.deferred.any = true;
14310
+ return;
14311
+ }
13986
14312
  if (screened.kind === "ack") {
13987
- pending.push(screened.ack);
14313
+ queueAck(ctx, screened.ack);
13988
14314
  if (screened.ack.disposition === "claimed") claimable.n += 1;
13989
14315
  return;
13990
14316
  }
14317
+ await deps.accepted?.(screened.token);
13991
14318
  await deps.view.store(screened.record);
13992
- pending.push(claimAck(entry));
14319
+ queueAck(ctx, claimAck(entry));
13993
14320
  claimable.n += 1;
13994
14321
  const transfer = await announce(deps, entry, screened.record);
13995
14322
  stored.push(transfer);
13996
14323
  deps.emit("transfer:incoming", transfer);
13997
14324
  }
14325
+ recheckDue(deliveryId) {
14326
+ const recheck = this.rechecks.get(deliveryId);
14327
+ return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
14328
+ }
14329
+ scheduleRecheck(deliveryId, deferred) {
14330
+ if (!deferred) {
14331
+ this.rechecks.delete(deliveryId);
14332
+ return;
14333
+ }
14334
+ const previous = this.rechecks.get(deliveryId);
14335
+ if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
14336
+ const attempts = (previous?.attempts ?? 0) + 1;
14337
+ const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
14338
+ this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
14339
+ }
13998
14340
  };
13999
14341
  async function screen(deps, engine, entry) {
14000
14342
  const blobBytes = await entry.fetchBlob();
@@ -14005,7 +14347,8 @@ async function screen(deps, engine, entry) {
14005
14347
  logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
14006
14348
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
14007
14349
  }
14008
- const verdict = await engine.verify(token);
14350
+ const verdict = await verdictOf(engine, token);
14351
+ if (verdict === null) return { kind: "defer" };
14009
14352
  if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
14010
14353
  if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
14011
14354
  return { kind: "ack", ack: rejectAck(entry, "not-owned") };
@@ -14016,38 +14359,43 @@ async function screen(deps, engine, entry) {
14016
14359
  if (held !== null && await engine.isSpent(token)) {
14017
14360
  return { kind: "ack", ack: rejectAck(entry, "invalid") };
14018
14361
  }
14362
+ const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
14019
14363
  return {
14020
14364
  kind: "accept",
14365
+ token,
14021
14366
  record: {
14022
14367
  tokenId: keys.tokenId,
14023
14368
  stateHash: keys.stateHash,
14024
14369
  assets: toAssetAmounts(token),
14025
- ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
14370
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
14371
+ ...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
14026
14372
  }
14027
14373
  };
14028
14374
  }
14029
- async function announce(deps, entry, record) {
14030
- const receivedAt = (deps.now ?? Date.now)();
14375
+ function unverifiedCoinsOf(deps, token) {
14376
+ const claims = deps.claims;
14377
+ if (claims === void 0 || honoursClaims(claims, token)) return [];
14378
+ return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
14379
+ }
14380
+ async function verdictOf(engine, token) {
14031
14381
  try {
14032
- await deps.recordReceived({
14033
- dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
14034
- tokenId: record.tokenId,
14035
- stateHash: record.stateHash,
14036
- assets: record.assets,
14037
- ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
14038
- ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
14039
- ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
14040
- ...entry.memo !== void 0 ? { memo: entry.memo } : {},
14041
- receivedAt
14042
- });
14382
+ return await engine.verify(token);
14043
14383
  } catch (err) {
14044
- logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
14384
+ if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
14385
+ throw err;
14045
14386
  }
14387
+ }
14388
+ async function announce(deps, entry, record) {
14389
+ const receivedAt = (deps.now ?? Date.now)();
14390
+ await recordArrival(deps, entry, record, receivedAt);
14391
+ const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
14392
+ const counted = record.assets.filter((asset) => !refused2.includes(asset));
14046
14393
  return {
14047
14394
  id: record.tokenId,
14048
14395
  senderPubkey: entry.senderPubkey ?? "",
14049
14396
  ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
14050
- tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
14397
+ tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
14398
+ ...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
14051
14399
  // #777: named here rather than mapped from assets, which announced an EMPTY list.
14052
14400
  ...record.tokenType !== void 0 ? {
14053
14401
  coinless: [
@@ -14065,6 +14413,25 @@ async function announce(deps, entry, record) {
14065
14413
  receivedAt
14066
14414
  };
14067
14415
  }
14416
+ async function recordArrival(deps, entry, record, receivedAt) {
14417
+ const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
14418
+ if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
14419
+ try {
14420
+ await deps.recordReceived({
14421
+ dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
14422
+ tokenId: record.tokenId,
14423
+ stateHash: record.stateHash,
14424
+ assets,
14425
+ ...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
14426
+ ...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
14427
+ ...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
14428
+ ...entry.memo !== void 0 ? { memo: entry.memo } : {},
14429
+ receivedAt
14430
+ });
14431
+ } catch (err) {
14432
+ logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
14433
+ }
14434
+ }
14068
14435
  async function flushAcks(deps, pending, epochOf) {
14069
14436
  if (pending.length === 0) return { progressed: true };
14070
14437
  const before = pending.length;
@@ -14072,7 +14439,7 @@ async function flushAcks(deps, pending, epochOf) {
14072
14439
  try {
14073
14440
  const settled = await settleAcks(deps, pending);
14074
14441
  let i = 0;
14075
- while (i < pending.length && settled.has(pending[i].deliveryId)) {
14442
+ while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
14076
14443
  lastAcked = pending[i].cursor;
14077
14444
  i += 1;
14078
14445
  }
@@ -14148,6 +14515,9 @@ async function ackOne(deps, ack) {
14148
14515
  await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
14149
14516
  }
14150
14517
  }
14518
+ function queueAck(ctx, ack) {
14519
+ ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
14520
+ }
14151
14521
  function claimAck(entry) {
14152
14522
  return {
14153
14523
  deliveryId: entry.deliveryId,
@@ -14856,72 +15226,276 @@ var SpendQueue = class {
14856
15226
  queue = [];
14857
15227
  this.queues.set(coinId, queue);
14858
15228
  }
14859
- queue.push(entry);
14860
- return settled;
15229
+ queue.push(entry);
15230
+ return settled;
15231
+ }
15232
+ expire(id, coinId) {
15233
+ const entries = this.queues.get(coinId);
15234
+ if (!entries) return;
15235
+ const index = entries.findIndex((e) => e.id === id);
15236
+ if (index === -1) return;
15237
+ const [entry] = entries.splice(index, 1);
15238
+ entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
15239
+ if (entries.length === 0) this.queues.delete(coinId);
15240
+ }
15241
+ // Whole-token semantics: only fully-free tokens enter the selector pool. What
15242
+ // the reservations hold back is counted too — the refusal has to name it (#737).
15243
+ freeView(coinId) {
15244
+ const freeView = [];
15245
+ let freeTotal = 0n;
15246
+ let pinnedTotal = 0n;
15247
+ const holders = /* @__PURE__ */ new Set();
15248
+ const unproven = this.deps.ledger.unprovenReason();
15249
+ for (const entry of this.deps.getPool(coinId)) {
15250
+ if (entry.amount <= 0n) continue;
15251
+ const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
15252
+ if (holder === void 0) {
15253
+ freeView.push(entry);
15254
+ freeTotal += entry.amount;
15255
+ } else {
15256
+ pinnedTotal += entry.amount;
15257
+ holders.add(holder);
15258
+ }
15259
+ }
15260
+ return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
15261
+ }
15262
+ expectedChangeTotal(coinId) {
15263
+ let total = 0n;
15264
+ for (const change of this.expectedChange.values()) {
15265
+ if (change.coinId === coinId) total += change.amount;
15266
+ }
15267
+ return total;
15268
+ }
15269
+ reservePlan(reservationId, plan, freeView) {
15270
+ const pool = new Set(freeView.map((e) => e.tokenId));
15271
+ const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
15272
+ for (const tokenId of tokenIds) {
15273
+ if (!pool.has(tokenId)) {
15274
+ throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
15275
+ }
15276
+ }
15277
+ this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
15278
+ }
15279
+ totalQueued() {
15280
+ let total = 0;
15281
+ for (const entries of this.queues.values()) total += entries.length;
15282
+ return total;
15283
+ }
15284
+ };
15285
+ function parseAmount(raw) {
15286
+ if (!/^\d+$/.test(raw)) {
15287
+ throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
15288
+ }
15289
+ const amount = BigInt(raw);
15290
+ if (amount <= 0n) {
15291
+ throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
15292
+ }
15293
+ return amount;
15294
+ }
15295
+
15296
+ // modules/payments-v2/machine/payload.ts
15297
+ function buildPayload(recipientPubkey, request, spend, spentStates) {
15298
+ return {
15299
+ v: 2,
15300
+ kind: "coin",
15301
+ recipient: recipientPubkey,
15302
+ coinId: request.coinId,
15303
+ amount: request.amount,
15304
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
15305
+ direct: [...spend.plan.direct],
15306
+ ...spend.plan.split !== void 0 ? {
15307
+ split: {
15308
+ tokenId: spend.plan.split.tokenId,
15309
+ splitAmount: spend.plan.split.splitAmount.toString(),
15310
+ remainderAmount: spend.plan.split.remainderAmount.toString()
15311
+ }
15312
+ } : {},
15313
+ spentStates
15314
+ };
15315
+ }
15316
+ function messageOf(err) {
15317
+ return err instanceof Error ? err.message : String(err);
15318
+ }
15319
+ function buildWholePayload(recipientPubkey, request, spentStates) {
15320
+ return {
15321
+ v: 2,
15322
+ kind: "whole",
15323
+ recipient: recipientPubkey,
15324
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
15325
+ direct: [request.tokenId],
15326
+ spentStates
15327
+ };
15328
+ }
15329
+
15330
+ // modules/payments-v2/burn.ts
15331
+ var HEX32 = /^[0-9a-f]{64}$/;
15332
+ async function heldToken2(deps, tokenId) {
15333
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
15334
+ if (bytes === void 0) {
15335
+ throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
15336
+ }
15337
+ return deps.engine.decodeToken({ tokenId, token: bytes });
15338
+ }
15339
+ function assertBurnable(engine, token, tokenId) {
15340
+ if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
15341
+ throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
15342
+ }
15343
+ if (token.valueEnvelope === "bare_collection") {
15344
+ throw new SphereError(
15345
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
15346
+ "VALIDATION_ERROR"
15347
+ );
15348
+ }
15349
+ }
15350
+ function assetsOf(token) {
15351
+ return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
15352
+ }
15353
+ async function runBurnUnderJournal(deps, input) {
15354
+ const { burnId } = input;
15355
+ const { tokenId, reasonBytes } = input.request;
15356
+ if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
15357
+ return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
15358
+ }
15359
+ let token;
15360
+ try {
15361
+ token = await heldToken2(deps, tokenId);
15362
+ assertBurnable(deps.engine, token, tokenId);
15363
+ } catch (err) {
15364
+ return refused(burnId, tokenId, err);
15365
+ }
15366
+ const entry = {
15367
+ burnId,
15368
+ tokenId,
15369
+ reasonHex: bytesToHex3(reasonBytes),
15370
+ burnedTokenHex: null,
15371
+ settled: false,
15372
+ assets: assetsOf(token),
15373
+ createdAt: deps.now()
15374
+ };
15375
+ await deps.burnJournal.upsert(entry);
15376
+ try {
15377
+ const settled = await burnJournaled(deps, entry, token);
15378
+ return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
15379
+ } catch (err) {
15380
+ const failure = failureOf(err);
15381
+ if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
15382
+ else deps.armHeartbeat();
15383
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
15384
+ }
15385
+ }
15386
+ async function replayBurns(deps) {
15387
+ let progressed = 0;
15388
+ for (const entry of await deps.burnJournal.list()) {
15389
+ if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
15390
+ try {
15391
+ const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
15392
+ await burnJournaled(deps, entry, token);
15393
+ progressed += 1;
15394
+ } catch (err) {
15395
+ if (!isTerminal(err)) continue;
15396
+ await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
15397
+ progressed += 1;
15398
+ }
15399
+ }
15400
+ return progressed;
15401
+ }
15402
+ async function pendingBurns(deps) {
15403
+ return (await deps.burnJournal.list()).map((entry) => ({
15404
+ burnId: entry.burnId,
15405
+ tokenId: entry.tokenId,
15406
+ reasonBytes: hexToBytes4(entry.reasonHex),
15407
+ burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
15408
+ settled: entry.settled,
15409
+ createdAt: entry.createdAt,
15410
+ ...entry.failure !== void 0 ? { failure: entry.failure } : {}
15411
+ }));
15412
+ }
15413
+ async function acknowledgeBurn(deps, burnId) {
15414
+ const entry = await deps.burnJournal.getByKey(burnId);
15415
+ if (entry === void 0) return;
15416
+ if (isLiveBurn(entry)) {
15417
+ throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
15418
+ }
15419
+ await deps.burnJournal.removeByKey(burnId);
15420
+ }
15421
+ function refused(burnId, tokenId, err) {
15422
+ const failure = failureOf(err);
15423
+ return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
15424
+ }
15425
+ function isTerminal(err) {
15426
+ return classifyError(err) === "conflict";
15427
+ }
15428
+ function failureOf(err) {
15429
+ const code = err !== null && typeof err === "object" ? err.code : void 0;
15430
+ return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
15431
+ }
15432
+ async function burnJournaled(deps, entry, token) {
15433
+ let current = entry;
15434
+ if (current.burnedTokenHex === null) {
15435
+ if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
15436
+ const burned = await deps.engine.burn(
15437
+ { token, reasonBytes: hexToBytes4(current.reasonHex) },
15438
+ { transferId: current.burnId }
15439
+ );
15440
+ if (burned.blob.tokenId !== current.tokenId) {
15441
+ throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
15442
+ }
15443
+ current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
15444
+ await deps.burnJournal.upsert(current);
14861
15445
  }
14862
- expire(id, coinId) {
14863
- const entries = this.queues.get(coinId);
14864
- if (!entries) return;
14865
- const index = entries.findIndex((e) => e.id === id);
14866
- if (index === -1) return;
14867
- const [entry] = entries.splice(index, 1);
14868
- entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
14869
- if (entries.length === 0) this.queues.delete(coinId);
15446
+ await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
15447
+ await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
15448
+ current = { ...current, settled: true };
15449
+ await deps.burnJournal.upsert(current);
15450
+ deps.refreshView();
15451
+ return current;
15452
+ }
15453
+
15454
+ // modules/payments-v2/burn-hold.ts
15455
+ var BurnHold = class {
15456
+ constructor(deps) {
15457
+ this.deps = deps;
14870
15458
  }
14871
- // Whole-token semantics: only fully-free tokens enter the selector pool. What
14872
- // the reservations hold back is counted too — the refusal has to name it (#737).
14873
- freeView(coinId) {
14874
- const freeView = [];
14875
- let freeTotal = 0n;
14876
- let pinnedTotal = 0n;
14877
- const holders = /* @__PURE__ */ new Set();
14878
- const unproven = this.deps.ledger.unprovenReason();
14879
- for (const entry of this.deps.getPool(coinId)) {
14880
- if (entry.amount <= 0n) continue;
14881
- const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
14882
- if (holder === void 0) {
14883
- freeView.push(entry);
14884
- freeTotal += entry.amount;
14885
- } else {
14886
- pinnedTotal += entry.amount;
14887
- holders.add(holder);
14888
- }
15459
+ async run(burnId, tokenId, burn) {
15460
+ try {
15461
+ this.deps.queue.planWhole(burnId, tokenId);
15462
+ } catch (err) {
15463
+ return refused(burnId, tokenId, err);
14889
15464
  }
14890
- return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
14891
- }
14892
- expectedChangeTotal(coinId) {
14893
- let total = 0n;
14894
- for (const change of this.expectedChange.values()) {
14895
- if (change.coinId === coinId) total += change.amount;
15465
+ this.deps.view.markInFlightMany([tokenId]);
15466
+ let burned = false;
15467
+ try {
15468
+ const result = await burn();
15469
+ burned = result.success;
15470
+ return result;
15471
+ } finally {
15472
+ await this.settle(burnId, tokenId, burned);
14896
15473
  }
14897
- return total;
14898
15474
  }
14899
- reservePlan(reservationId, plan, freeView) {
14900
- const pool = new Set(freeView.map((e) => e.tokenId));
14901
- const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
14902
- for (const tokenId of tokenIds) {
14903
- if (!pool.has(tokenId)) {
14904
- throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
14905
- }
15475
+ async settle(burnId, tokenId, burned) {
15476
+ if (burned) {
15477
+ this.deps.ledger.commit(burnId);
15478
+ this.deps.track(this.refreshThenRelease(tokenId));
15479
+ return;
14906
15480
  }
14907
- this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
15481
+ const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
15482
+ if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
15483
+ this.deps.ledger.cancel(burnId);
15484
+ this.deps.view.releaseMany([tokenId]);
15485
+ this.deps.queue.notifyChange("");
15486
+ return;
15487
+ }
15488
+ this.deps.pins.adopt(burnId);
14908
15489
  }
14909
- totalQueued() {
14910
- let total = 0;
14911
- for (const entries of this.queues.values()) total += entries.length;
14912
- return total;
15490
+ async refreshThenRelease(tokenId) {
15491
+ try {
15492
+ await this.deps.view.delta();
15493
+ this.deps.view.releaseMany([tokenId]);
15494
+ } finally {
15495
+ this.deps.queue.notifyChange("");
15496
+ }
14913
15497
  }
14914
15498
  };
14915
- function parseAmount(raw) {
14916
- if (!/^\d+$/.test(raw)) {
14917
- throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
14918
- }
14919
- const amount = BigInt(raw);
14920
- if (amount <= 0n) {
14921
- throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
14922
- }
14923
- return amount;
14924
- }
14925
15499
 
14926
15500
  // modules/payments-v2/compose.ts
14927
15501
  function supportsDeterministicMint(engine) {
@@ -14930,16 +15504,8 @@ function supportsDeterministicMint(engine) {
14930
15504
  function composeFacadeParts(deps, hooks) {
14931
15505
  const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
14932
15506
  const ledger = new ReservationLedger();
14933
- const view = new InventoryView({
14934
- port: deps.storagePort,
14935
- kv: deps.kv,
14936
- emit: (event) => deps.emit(event, {}),
14937
- // #737: a reserved token is unselectable, so it is never confirmed balance.
14938
- // #738: while the held-set is unproven, EVERY token reads pinned — the report
14939
- // must not call a token spendable that the queue is about to refuse to spend.
14940
- isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
14941
- ...deps.now !== void 0 ? { now: deps.now } : {}
14942
- });
15507
+ const verdicts = buildVerdicts(deps, hooks);
15508
+ const view = buildView(deps, hooks, ledger, verdicts);
14943
15509
  const queue = new SpendQueue({
14944
15510
  ledger,
14945
15511
  getPool: (coinId) => view.pool(coinId),
@@ -14954,27 +15520,57 @@ function composeFacadeParts(deps, hooks) {
14954
15520
  ...deps.now !== void 0 ? { now: deps.now } : {},
14955
15521
  ...deps.newId !== void 0 ? { newId: deps.newId } : {}
14956
15522
  });
14957
- const machineDeps = buildMachineDeps(deps, hooks, historyStore);
15523
+ const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
14958
15524
  const machineStores = createMachineStores(deps.kv);
14959
15525
  const heldStates = /* @__PURE__ */ new Map();
14960
15526
  const refreshView = coalesced(() => view.delta(), hooks.track);
15527
+ const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
14961
15528
  return {
14962
15529
  ownPubkeyBytes,
14963
15530
  view,
15531
+ verdicts,
14964
15532
  refreshView,
14965
15533
  ledger,
14966
- pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
15534
+ pins,
14967
15535
  queue,
15536
+ burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
14968
15537
  historyStore,
14969
15538
  machineStores,
14970
15539
  machineDeps,
14971
15540
  machine: new TransferMachine(machineDeps),
14972
15541
  heldStates,
14973
- receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
15542
+ receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
14974
15543
  requests: buildRequests(deps, hooks),
14975
15544
  restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
14976
15545
  };
14977
15546
  }
15547
+ function buildView(deps, hooks, ledger, verdicts) {
15548
+ const view = new InventoryView({
15549
+ port: deps.storagePort,
15550
+ kv: deps.kv,
15551
+ trust: verdicts,
15552
+ emit: (event) => {
15553
+ deps.emit(event, {});
15554
+ hooks.track(verdicts.review(view.claimHolders()));
15555
+ },
15556
+ // #737: a reserved token is unselectable, so it is never confirmed balance.
15557
+ // #738: while the held-set is unproven, EVERY token reads pinned — the report
15558
+ // must not call a token spendable that the queue is about to refuse to spend.
15559
+ isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
15560
+ ...deps.now !== void 0 ? { now: deps.now } : {}
15561
+ });
15562
+ return view;
15563
+ }
15564
+ function buildVerdicts(deps, hooks) {
15565
+ return new TokenVerdicts({
15566
+ kv: deps.kv,
15567
+ claims: deps.claims ?? new CoinClaims(),
15568
+ engine: () => hooks.engine(),
15569
+ getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
15570
+ changed: () => deps.emit("inventory:updated", {}),
15571
+ ...deps.now !== void 0 ? { now: deps.now } : {}
15572
+ });
15573
+ }
14978
15574
  function buildPins(deps, hooks, parts) {
14979
15575
  return new IntentPins({
14980
15576
  ledger: parts.ledger,
@@ -15002,8 +15598,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
15002
15598
  }
15003
15599
  };
15004
15600
  }
15005
- function buildMachineDeps(deps, hooks, historyStore) {
15601
+ function buildMachineDeps(deps, hooks, historyStore, verdicts) {
15006
15602
  return {
15603
+ recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
15007
15604
  engine: () => hooks.engine(),
15008
15605
  storage: deps.storagePort,
15009
15606
  delivery: deps.deliveryPort,
@@ -15036,10 +15633,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
15036
15633
  }
15037
15634
  };
15038
15635
  }
15039
- function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
15636
+ function buildReceive(deps, hooks, parts, refreshView) {
15637
+ const { historyStore, heldStates, verdicts } = parts;
15040
15638
  return new Receive({
15041
15639
  delivery: deps.deliveryPort,
15042
15640
  engine: () => hooks.engine(),
15641
+ claims: deps.claims ?? new CoinClaims(),
15642
+ accepted: (token) => verdicts.accept(token),
15043
15643
  view: {
15044
15644
  heldState: (tokenId) => heldStates.get(tokenId) ?? null,
15045
15645
  store: async (entry) => {
@@ -15089,40 +15689,6 @@ function buildRequests(deps, hooks) {
15089
15689
  });
15090
15690
  }
15091
15691
 
15092
- // modules/payments-v2/machine/payload.ts
15093
- function buildPayload(recipientPubkey, request, spend, spentStates) {
15094
- return {
15095
- v: 2,
15096
- kind: "coin",
15097
- recipient: recipientPubkey,
15098
- coinId: request.coinId,
15099
- amount: request.amount,
15100
- ...request.memo !== void 0 ? { memo: request.memo } : {},
15101
- direct: [...spend.plan.direct],
15102
- ...spend.plan.split !== void 0 ? {
15103
- split: {
15104
- tokenId: spend.plan.split.tokenId,
15105
- splitAmount: spend.plan.split.splitAmount.toString(),
15106
- remainderAmount: spend.plan.split.remainderAmount.toString()
15107
- }
15108
- } : {},
15109
- spentStates
15110
- };
15111
- }
15112
- function messageOf(err) {
15113
- return err instanceof Error ? err.message : String(err);
15114
- }
15115
- function buildWholePayload(recipientPubkey, request, spentStates) {
15116
- return {
15117
- v: 2,
15118
- kind: "whole",
15119
- recipient: recipientPubkey,
15120
- ...request.memo !== void 0 ? { memo: request.memo } : {},
15121
- direct: [request.tokenId],
15122
- spentStates
15123
- };
15124
- }
15125
-
15126
15692
  // modules/payments-v2/mint-params.ts
15127
15693
  function mintParams(recipientPubkey, coinId, amount) {
15128
15694
  return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
@@ -15192,6 +15758,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
15192
15758
  }
15193
15759
  return false;
15194
15760
  }
15761
+ async function seedHeldStates(storagePort, heldStates) {
15762
+ let page = await storagePort.listInventory();
15763
+ for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
15764
+ for (const item of page.items) {
15765
+ if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
15766
+ }
15767
+ if (!page.more) return;
15768
+ page = await storagePort.listInventory(page.cursor);
15769
+ }
15770
+ }
15195
15771
  async function replayCoinMints(deps) {
15196
15772
  let resolved = 0;
15197
15773
  for (const entry of await deps.mintJournal.list()) {
@@ -15296,7 +15872,8 @@ async function mintJournaled(deps, entry) {
15296
15872
  }
15297
15873
 
15298
15874
  // modules/payments-v2/mint-custom.ts
15299
- var HEX32 = /^[0-9a-f]{64}$/;
15875
+ var HEX322 = /^[0-9a-f]{64}$/;
15876
+ var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
15300
15877
  function assertRequest(request) {
15301
15878
  if (request.tokenType.length !== 32) {
15302
15879
  throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
@@ -15307,19 +15884,27 @@ function assertRequest(request) {
15307
15884
  if (request.data.length === 0) {
15308
15885
  throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
15309
15886
  }
15887
+ if (request.justification !== void 0 && request.justification.length === 0) {
15888
+ throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
15889
+ }
15890
+ if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
15891
+ throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
15892
+ }
15310
15893
  for (const asset of request.assets) {
15311
- if (!HEX32.test(asset.coinId) || asset.amount <= 0n) {
15894
+ if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
15312
15895
  throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
15313
15896
  }
15314
15897
  }
15315
15898
  }
15316
15899
  async function runCustomMintUnderJournal(deps, input) {
15900
+ let entry;
15317
15901
  try {
15318
15902
  assertRequest(input.request);
15903
+ entry = journalEntryOf2(input.mintId, input.request, deps.now());
15904
+ deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
15319
15905
  } catch (err) {
15320
15906
  return { success: false, error: messageOf(err) };
15321
15907
  }
15322
- const entry = journalEntryOf2(input.mintId, input.request, deps.now());
15323
15908
  await deps.customMintJournal.upsert(entry);
15324
15909
  let tokenId;
15325
15910
  try {
@@ -15359,24 +15944,25 @@ function journalEntryOf2(mintId, request, createdAt) {
15359
15944
  createdAt
15360
15945
  };
15361
15946
  }
15947
+ function mintParamsOf(deps, entry, mintJustificationVerifiers) {
15948
+ return {
15949
+ recipientPubkey: deps.ownPubkeyBytes,
15950
+ data: hexToBytes4(entry.dataHex),
15951
+ tokenType: hexToBytes4(entry.tokenTypeHex),
15952
+ salt: hexToBytes4(entry.saltHex),
15953
+ ...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
15954
+ ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
15955
+ };
15956
+ }
15362
15957
  async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
15363
- const token = await deps.engine.mintDataToken(
15364
- {
15365
- recipientPubkey: deps.ownPubkeyBytes,
15366
- data: hexToBytes4(entry.dataHex),
15367
- tokenType: hexToBytes4(entry.tokenTypeHex),
15368
- salt: hexToBytes4(entry.saltHex),
15369
- ...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
15370
- ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
15371
- },
15372
- { transferId: entry.mintId }
15373
- );
15958
+ const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
15374
15959
  if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
15375
15960
  throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
15376
15961
  }
15377
15962
  if (entry.tokenId === "") {
15378
15963
  await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
15379
15964
  }
15965
+ if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
15380
15966
  await finalizeMint(deps, {
15381
15967
  mintId: entry.mintId,
15382
15968
  token,
@@ -15386,108 +15972,6 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
15386
15972
  return token.blob.tokenId;
15387
15973
  }
15388
15974
 
15389
- // modules/payments-v2/burn.ts
15390
- var HEX322 = /^[0-9a-f]{64}$/;
15391
- async function heldToken2(deps, tokenId) {
15392
- const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
15393
- if (bytes === void 0) {
15394
- throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
15395
- }
15396
- return deps.engine.decodeToken({ tokenId, token: bytes });
15397
- }
15398
- function assetsOf(token) {
15399
- return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
15400
- }
15401
- async function runBurnUnderJournal(deps, input) {
15402
- const { burnId } = input;
15403
- const { tokenId, reasonBytes } = input.request;
15404
- if (!HEX322.test(tokenId) || reasonBytes.length === 0) {
15405
- return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty" };
15406
- }
15407
- let token;
15408
- try {
15409
- token = await heldToken2(deps, tokenId);
15410
- if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
15411
- throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
15412
- }
15413
- } catch (err) {
15414
- return { success: false, burnId, tokenId, error: messageOf(err) };
15415
- }
15416
- const entry = {
15417
- burnId,
15418
- tokenId,
15419
- reasonHex: bytesToHex3(reasonBytes),
15420
- burnedTokenHex: null,
15421
- settled: false,
15422
- assets: assetsOf(token),
15423
- createdAt: deps.now()
15424
- };
15425
- await deps.burnJournal.upsert(entry);
15426
- try {
15427
- const settled = await burnJournaled(deps, entry, token, { applied: false });
15428
- return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
15429
- } catch (err) {
15430
- deps.armHeartbeat();
15431
- return { success: false, burnId, tokenId, error: messageOf(err) };
15432
- }
15433
- }
15434
- async function replayBurns(deps) {
15435
- let progressed = 0;
15436
- for (const entry of await deps.burnJournal.list()) {
15437
- if (entry.settled || deps.isActiveOp(entry.burnId)) continue;
15438
- try {
15439
- const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
15440
- const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
15441
- await burnJournaled(deps, entry, token, { applied });
15442
- progressed += 1;
15443
- } catch {
15444
- continue;
15445
- }
15446
- }
15447
- return progressed;
15448
- }
15449
- async function pendingBurns(deps) {
15450
- return (await deps.burnJournal.list()).map((entry) => ({
15451
- burnId: entry.burnId,
15452
- tokenId: entry.tokenId,
15453
- reasonBytes: hexToBytes4(entry.reasonHex),
15454
- burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
15455
- settled: entry.settled,
15456
- createdAt: entry.createdAt
15457
- }));
15458
- }
15459
- async function acknowledgeBurn(deps, burnId) {
15460
- const entry = await deps.burnJournal.getByKey(burnId);
15461
- if (entry === void 0) return;
15462
- if (!entry.settled) {
15463
- throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
15464
- }
15465
- await deps.burnJournal.removeByKey(burnId);
15466
- }
15467
- async function burnJournaled(deps, entry, token, state) {
15468
- let current = entry;
15469
- if (current.burnedTokenHex === null) {
15470
- if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
15471
- const burned = await deps.engine.burn(
15472
- { token, reasonBytes: hexToBytes4(current.reasonHex) },
15473
- { transferId: current.burnId }
15474
- );
15475
- if (burned.blob.tokenId !== current.tokenId) {
15476
- throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
15477
- }
15478
- current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
15479
- await deps.burnJournal.upsert(current);
15480
- }
15481
- if (!state.applied) {
15482
- await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [] });
15483
- }
15484
- await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
15485
- current = { ...current, settled: true };
15486
- await deps.burnJournal.upsert(current);
15487
- deps.refreshView();
15488
- return current;
15489
- }
15490
-
15491
15975
  // modules/payments-v2/send-whole.ts
15492
15976
  async function materializeWholeSpend(deps, input) {
15493
15977
  const { engine } = deps;
@@ -15683,6 +16167,7 @@ var PaymentsFacade = class {
15683
16167
  track: (op) => this.trackTail(op)
15684
16168
  });
15685
16169
  this.view = parts.view;
16170
+ this.verdicts = parts.verdicts;
15686
16171
  this.wakeRefresh = parts.refreshView;
15687
16172
  this.ledger = parts.ledger;
15688
16173
  this.queue = parts.queue;
@@ -15696,6 +16181,7 @@ var PaymentsFacade = class {
15696
16181
  this.restoreDeps = parts.restoreDeps;
15697
16182
  this.requests = parts.requests;
15698
16183
  this.pins = parts.pins;
16184
+ this.burnHold = parts.burnHold;
15699
16185
  deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
15700
16186
  this.heartbeat = new ConvergenceHeartbeat({
15701
16187
  now: () => this.nowMs(),
@@ -15719,6 +16205,7 @@ var PaymentsFacade = class {
15719
16205
  wakeRefresh;
15720
16206
  prewarmed = new PrewarmCache();
15721
16207
  view;
16208
+ verdicts;
15722
16209
  ledger;
15723
16210
  queue;
15724
16211
  machine;
@@ -15745,6 +16232,7 @@ var PaymentsFacade = class {
15745
16232
  activeMoneyOps = /* @__PURE__ */ new Set();
15746
16233
  /** #737: the ledger holds exactly the sources of the still-open intents. */
15747
16234
  pins;
16235
+ burnHold;
15748
16236
  /** Per facade, so per address: an NFT reading is never served to another wallet. */
15749
16237
  nftCache = new NftCache();
15750
16238
  // ── lifecycle ──────────────────────────────────────────────────────────────
@@ -15769,7 +16257,8 @@ var PaymentsFacade = class {
15769
16257
  await this.deps.session.start();
15770
16258
  await this.pins.sync().catch(() => void 0);
15771
16259
  this.trackTail(this.runConvergencePass());
15772
- this.trackTail(this.seedHeldStates());
16260
+ this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
16261
+ await this.verdicts.start().catch(() => void 0);
15773
16262
  await this.view.fullPull().catch(() => void 0);
15774
16263
  this.receiveLoop.start(this.deps.receivePollMs);
15775
16264
  }
@@ -15778,6 +16267,7 @@ var PaymentsFacade = class {
15778
16267
  this.started = false;
15779
16268
  this.heartbeat.stop();
15780
16269
  this.receiveLoop.stop();
16270
+ this.verdicts.stop();
15781
16271
  for (const unsubscribe of this.unsubscribers) unsubscribe();
15782
16272
  this.unsubscribers = [];
15783
16273
  await this.deps.session.stop();
@@ -15794,12 +16284,17 @@ var PaymentsFacade = class {
15794
16284
  }
15795
16285
  // ── reads ──────────────────────────────────────────────────────────────────
15796
16286
  async assets(coinId) {
16287
+ return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
16288
+ }
16289
+ unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
16290
+ async heldAssets(coinId) {
15797
16291
  const all = await this.view.assets(this.deps.registry, this.deps.price);
15798
16292
  return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
15799
16293
  }
15800
16294
  tokens(filter) {
15801
- return this.view.tokens(this.deps.registry, filter);
16295
+ return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
15802
16296
  }
16297
+ unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
15803
16298
  coinless() {
15804
16299
  return this.view.coinless(this.deps.registry);
15805
16300
  }
@@ -15860,9 +16355,9 @@ var PaymentsFacade = class {
15860
16355
  return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
15861
16356
  }
15862
16357
  mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
15863
- burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
15864
- pendingBurns = () => this.track(pendingBurns(this.mintDeps()));
15865
- acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.mintDeps(), burnId));
16358
+ burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
16359
+ pendingBurns = () => this.track(pendingBurns(this.machineStores));
16360
+ acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
15866
16361
  // §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
15867
16362
  async resumeNow() {
15868
16363
  if (!this.started) return;
@@ -15905,7 +16400,8 @@ var PaymentsFacade = class {
15905
16400
  isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
15906
16401
  tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
15907
16402
  emit: this.deps.emit,
15908
- now: () => this.nowMs()
16403
+ now: () => this.nowMs(),
16404
+ accepted: (token) => this.verdicts.accept(token)
15909
16405
  };
15910
16406
  }
15911
16407
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
@@ -16243,16 +16739,6 @@ var PaymentsFacade = class {
16243
16739
  newId() {
16244
16740
  return (this.deps.newId ?? randomUUID)();
16245
16741
  }
16246
- async seedHeldStates() {
16247
- let page = await this.deps.storagePort.listInventory();
16248
- for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
16249
- for (const item of page.items) {
16250
- if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
16251
- }
16252
- if (!page.more) return;
16253
- page = await this.deps.storagePort.listInventory(page.cursor);
16254
- }
16255
- }
16256
16742
  track(op) {
16257
16743
  const settled = op.then(() => void 0, () => void 0);
16258
16744
  this.pendingOps.add(settled);
@@ -17496,6 +17982,7 @@ function composePaymentsV2(spec) {
17496
17982
  engineRef: spec.engineRef,
17497
17983
  kv,
17498
17984
  registry: host.registry,
17985
+ claims: CoinClaims.fromPlugins(spec.plugins),
17499
17986
  ...host.price !== null ? { price: host.price } : {},
17500
17987
  emit: host.emit,
17501
17988
  resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
@@ -20544,7 +21031,8 @@ var Sphere = class _Sphere {
20544
21031
  // deliver the NEXT address's Unicity ID — a name the sending key does
20545
21032
  // not own (sphere#487 review).
20546
21033
  nametag: () => this.getNametagForAddress(facadeAddressId)
20547
- }
21034
+ },
21035
+ ...this._plugins ? { plugins: this._plugins } : {}
20548
21036
  });
20549
21037
  this._paymentsV2Active = { index, facade };
20550
21038
  await facade.start();