@pafi-dev/issuer 0.21.0 → 0.22.0

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.
package/README.md CHANGED
@@ -14,6 +14,42 @@ interfaces. Don't bundle into a browser app — use
14
14
 
15
15
  ---
16
16
 
17
+ ## v0.20.x — Bundler-driven gas estimate
18
+
19
+ **v0.20.0** — operator fee gasUnits now come from a Pimlico bundler
20
+ estimate cached server-side at PAFI's sponsor-relayer
21
+ (`POST /v1/estimate-gas-fee`). Eliminates the hardcoded
22
+ `SCENARIO_GAS_UNITS` table at the consumer layer.
23
+
24
+ - **New**: `FeeManagerConfig.bundlerClient?: BundlerEstimatorClient`. When
25
+ set, `feeManager.estimateGasFee({ partialUserOp, scenario,
26
+ contractAddress })` fetches gas units from the wired estimator
27
+ instead of running the legacy `gasUnits × premium` math.
28
+ Backwards-compatible — omit `bundlerClient` and the legacy hardcoded
29
+ path runs.
30
+ - **New**: `createPafiEstimatorClient({ baseUrl, apiKey, issuerId })`
31
+ HTTP adapter — POSTs to PAFI sponsor-relayer's
32
+ `/v1/estimate-gas-fee`. Pimlico key never touches issuer infra; the
33
+ bundler call happens server-side at PAFI.
34
+ - **New**: `RelayService.previewMintUserOp` / `previewBurnUserOp` —
35
+ build a dummy partial UserOp (placeholder 65-byte sig, no HSM call)
36
+ for the bundler estimate. `PTClaimHandler` / `PTRedeemHandler` wire
37
+ these automatically.
38
+ - **Changed**: `DEFAULT_PREMIUM_BPS` 12_000 → 10_000 (100%, no
39
+ double-pad). PAFI sponsor-relayer applies its own 110% premium
40
+ upstream; adding another SDK-side pad would over-charge users by
41
+ ~21% vs. actual gas.
42
+ - **Changed**: `PTClaimHandler` now passes the FeeManager-computed
43
+ `feeAmount` explicitly into `RelayService.prepareMint`, so the
44
+ in-batch PT transfer matches the value returned in the response.
45
+ Pre-v0.20 the handler relied on `RelayService.resolveFee` falling
46
+ back to `quoteOperatorFeePt` (which uses the legacy 120% premium) —
47
+ causing a ~20% mismatch between displayed fee and actual transfer.
48
+
49
+ **v0.21.0** — bug fix follow-up to v0.20: closes the resolveFee
50
+ mismatch in `PTClaimHandler` (above). Strongly recommended over
51
+ v0.20.0 for any new consumer wiring.
52
+
17
53
  ## v0.15.x — Uniswap V3 migration (breaking)
18
54
 
19
55
  **v0.15.0** — initial V3 migration:
package/dist/index.cjs CHANGED
@@ -84,6 +84,7 @@ __export(index_exports, {
84
84
  handleMobilePrepare: () => handleMobilePrepare,
85
85
  handleMobileSubmit: () => handleMobileSubmit,
86
86
  handleRedeemStatus: () => handleRedeemStatus,
87
+ makePostgresSingletonLock: () => makePostgresSingletonLock,
87
88
  mergePaymasterFields: () => mergePaymasterFields,
88
89
  payloadFromGenericError: () => payloadFromGenericError,
89
90
  payloadFromHttpException: () => payloadFromHttpException,
@@ -1625,6 +1626,10 @@ var DEFAULT_BATCH_SIZE2 = 2000n;
1625
1626
  var DEFAULT_POLL_INTERVAL_MS2 = 5e3;
1626
1627
  var BurnIndexer = class {
1627
1628
  provider;
1629
+ /**
1630
+ * The PointToken this indexer watches. Exposed so callers can key
1631
+ * leader-election locks / cursor stores by token (audit H-04 fix).
1632
+ */
1628
1633
  pointTokenAddress;
1629
1634
  ledger;
1630
1635
  cursorStore;
@@ -1784,6 +1789,45 @@ var BurnIndexer = class {
1784
1789
  }
1785
1790
  };
1786
1791
 
1792
+ // src/indexer/postgresSingletonLock.ts
1793
+ function makePostgresSingletonLock(runner) {
1794
+ return {
1795
+ async acquire(key) {
1796
+ const lockId = hashKeyToInt64(key);
1797
+ const rows = await runner.query(
1798
+ "SELECT pg_try_advisory_lock($1::bigint) AS got",
1799
+ [lockId]
1800
+ );
1801
+ const got = rows[0]?.got === true;
1802
+ if (!got) return null;
1803
+ return {
1804
+ async release() {
1805
+ try {
1806
+ await runner.query("SELECT pg_advisory_unlock($1::bigint)", [
1807
+ lockId
1808
+ ]);
1809
+ } catch {
1810
+ }
1811
+ }
1812
+ };
1813
+ }
1814
+ };
1815
+ }
1816
+ function hashKeyToInt64(key) {
1817
+ const FNV_OFFSET = 0xcbf29ce484222325n;
1818
+ const FNV_PRIME = 0x100000001b3n;
1819
+ const MASK_64 = (1n << 64n) - 1n;
1820
+ let hash = FNV_OFFSET;
1821
+ for (let i = 0; i < key.length; i++) {
1822
+ hash ^= BigInt(key.charCodeAt(i));
1823
+ hash = hash * FNV_PRIME & MASK_64;
1824
+ }
1825
+ const SIGNED_MAX = (1n << 63n) - 1n;
1826
+ const TWO64 = 1n << 64n;
1827
+ const signed = hash > SIGNED_MAX ? hash - TWO64 : hash;
1828
+ return signed.toString();
1829
+ }
1830
+
1787
1831
  // src/api/handlers.ts
1788
1832
  var import_viem6 = require("viem");
1789
1833
  var import_core6 = require("@pafi-dev/core");
@@ -3076,8 +3120,16 @@ var PTClaimHandler = class {
3076
3120
  domain,
3077
3121
  mintRequestNonce: request.mintRequestNonce,
3078
3122
  deadline: signatureDeadline,
3079
- mintFeeWrapperAddress: resolvedWrapper
3080
- // No feeAmount/feeRecipient RelayService auto-resolves.
3123
+ mintFeeWrapperAddress: resolvedWrapper,
3124
+ // Pass the bundler-estimated `feeAmount` explicitly so the
3125
+ // RelayService skips its legacy `quoteOperatorFeePt` path
3126
+ // (which uses the SDK's old 12_000 bps premium default).
3127
+ // Without this, the response's `feeAmount` (from FeeManager,
3128
+ // 100% premium on top of PAFI's 110% server-side estimate)
3129
+ // would diverge from the actual PT.transfer amount in the
3130
+ // UserOp batch (`quoteOperatorFeePt`'s 120%), and the user
3131
+ // would see one value while the wallet transferred another.
3132
+ feeAmount
3081
3133
  });
3082
3134
  } catch (err) {
3083
3135
  throw new PTClaimError(
@@ -4658,7 +4710,7 @@ var RedemptionService = class {
4658
4710
  };
4659
4711
 
4660
4712
  // src/config.ts
4661
- function createIssuerService(config) {
4713
+ async function createIssuerService(config) {
4662
4714
  if (!config.provider) {
4663
4715
  throw new Error("createIssuerService: provider is required");
4664
4716
  }
@@ -4777,9 +4829,26 @@ function createIssuerService(config) {
4777
4829
  handlersConfig.mintFeeWrapperAddress = resolvedWrapperAddress;
4778
4830
  }
4779
4831
  const handlers = new IssuerApiHandlers(handlersConfig);
4832
+ const indexerLeaderLocks = [];
4780
4833
  if (config.indexer?.autoStart) {
4781
- for (const idx of indexers.values()) {
4782
- idx.start();
4834
+ const lock = config.indexer.singletonLock;
4835
+ if (!lock) {
4836
+ console.warn(
4837
+ "[@pafi-dev/issuer] indexer.autoStart=true without singletonLock \u2014 this is UNSAFE in multi-replica deployments (audit finding H-04). Either set replicas=1 + INDEXER_AUTOSTART=false on non-leader pods, or pass `singletonLock: makePostgresSingletonLock(dataSource)`. This permissive path will be removed in a future major release."
4838
+ );
4839
+ for (const idx of indexers.values()) {
4840
+ idx.start();
4841
+ }
4842
+ } else {
4843
+ for (const [tokenAddr, idx] of indexers.entries()) {
4844
+ const key = `pafi-issuer:point-indexer:${tokenAddr.toLowerCase()}`;
4845
+ const handle = await lock.acquire(key);
4846
+ if (!handle) {
4847
+ continue;
4848
+ }
4849
+ idx.start();
4850
+ indexerLeaderLocks.push(handle);
4851
+ }
4783
4852
  }
4784
4853
  }
4785
4854
  return {
@@ -4790,6 +4859,7 @@ function createIssuerService(config) {
4790
4859
  relay: relayService,
4791
4860
  fee: feeManager,
4792
4861
  indexers,
4862
+ indexerLeaderLocks,
4793
4863
  api: handlers,
4794
4864
  redemption
4795
4865
  };
@@ -4997,7 +5067,7 @@ var MemoryRedemptionHistoryStore = class {
4997
5067
  };
4998
5068
 
4999
5069
  // src/index.ts
5000
- var PAFI_ISSUER_SDK_VERSION = true ? "0.21.0" : "dev";
5070
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.22.0" : "dev";
5001
5071
  // Annotate the CommonJS export names for ESM import in node:
5002
5072
  0 && (module.exports = {
5003
5073
  AdapterMisconfiguredError,
@@ -5064,6 +5134,7 @@ var PAFI_ISSUER_SDK_VERSION = true ? "0.21.0" : "dev";
5064
5134
  handleMobilePrepare,
5065
5135
  handleMobileSubmit,
5066
5136
  handleRedeemStatus,
5137
+ makePostgresSingletonLock,
5067
5138
  mergePaymasterFields,
5068
5139
  payloadFromGenericError,
5069
5140
  payloadFromHttpException,