@pafi-dev/issuer 0.20.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/dist/index.js CHANGED
@@ -1391,6 +1391,10 @@ var DEFAULT_BATCH_SIZE2 = 2000n;
1391
1391
  var DEFAULT_POLL_INTERVAL_MS2 = 5e3;
1392
1392
  var BurnIndexer = class {
1393
1393
  provider;
1394
+ /**
1395
+ * The PointToken this indexer watches. Exposed so callers can key
1396
+ * leader-election locks / cursor stores by token (audit H-04 fix).
1397
+ */
1394
1398
  pointTokenAddress;
1395
1399
  ledger;
1396
1400
  cursorStore;
@@ -1550,6 +1554,45 @@ var BurnIndexer = class {
1550
1554
  }
1551
1555
  };
1552
1556
 
1557
+ // src/indexer/postgresSingletonLock.ts
1558
+ function makePostgresSingletonLock(runner) {
1559
+ return {
1560
+ async acquire(key) {
1561
+ const lockId = hashKeyToInt64(key);
1562
+ const rows = await runner.query(
1563
+ "SELECT pg_try_advisory_lock($1::bigint) AS got",
1564
+ [lockId]
1565
+ );
1566
+ const got = rows[0]?.got === true;
1567
+ if (!got) return null;
1568
+ return {
1569
+ async release() {
1570
+ try {
1571
+ await runner.query("SELECT pg_advisory_unlock($1::bigint)", [
1572
+ lockId
1573
+ ]);
1574
+ } catch {
1575
+ }
1576
+ }
1577
+ };
1578
+ }
1579
+ };
1580
+ }
1581
+ function hashKeyToInt64(key) {
1582
+ const FNV_OFFSET = 0xcbf29ce484222325n;
1583
+ const FNV_PRIME = 0x100000001b3n;
1584
+ const MASK_64 = (1n << 64n) - 1n;
1585
+ let hash = FNV_OFFSET;
1586
+ for (let i = 0; i < key.length; i++) {
1587
+ hash ^= BigInt(key.charCodeAt(i));
1588
+ hash = hash * FNV_PRIME & MASK_64;
1589
+ }
1590
+ const SIGNED_MAX = (1n << 63n) - 1n;
1591
+ const TWO64 = 1n << 64n;
1592
+ const signed = hash > SIGNED_MAX ? hash - TWO64 : hash;
1593
+ return signed.toString();
1594
+ }
1595
+
1553
1596
  // src/api/handlers.ts
1554
1597
  import { getAddress as getAddress5 } from "viem";
1555
1598
  import {
@@ -1915,8 +1958,59 @@ var IssuerApiHandlers = class _IssuerApiHandlers {
1915
1958
  }
1916
1959
  };
1917
1960
 
1918
- // src/api/handlers/ptRedeemHandler.ts
1961
+ // src/api/pointTokenDomainResolver.ts
1919
1962
  import { getAddress as getAddress6 } from "viem";
1963
+ var NAME_ABI = [
1964
+ {
1965
+ type: "function",
1966
+ name: "name",
1967
+ stateMutability: "view",
1968
+ inputs: [],
1969
+ outputs: [{ type: "string" }]
1970
+ }
1971
+ ];
1972
+ var PointTokenDomainResolver = class {
1973
+ provider;
1974
+ overrides;
1975
+ cache = /* @__PURE__ */ new Map();
1976
+ constructor(config) {
1977
+ this.provider = config.provider;
1978
+ this.overrides = /* @__PURE__ */ new Map();
1979
+ if (config.overrides) {
1980
+ for (const [addr, name] of Object.entries(config.overrides)) {
1981
+ this.overrides.set(getAddress6(addr).toLowerCase(), name);
1982
+ }
1983
+ }
1984
+ }
1985
+ async resolve(pointTokenAddress) {
1986
+ const key = getAddress6(pointTokenAddress).toLowerCase();
1987
+ const cached = this.cache.get(key);
1988
+ if (cached !== void 0) return cached;
1989
+ const override = this.overrides.get(key);
1990
+ if (override !== void 0) {
1991
+ this.cache.set(key, override);
1992
+ return override;
1993
+ }
1994
+ const name = await this.provider.readContract({
1995
+ address: pointTokenAddress,
1996
+ abi: NAME_ABI,
1997
+ functionName: "name"
1998
+ });
1999
+ this.cache.set(key, name);
2000
+ return name;
2001
+ }
2002
+ /** Invalidate one address (after deploy / proxy upgrade) or all. */
2003
+ invalidate(pointTokenAddress) {
2004
+ if (pointTokenAddress) {
2005
+ this.cache.delete(getAddress6(pointTokenAddress).toLowerCase());
2006
+ } else {
2007
+ this.cache.clear();
2008
+ }
2009
+ }
2010
+ };
2011
+
2012
+ // src/api/handlers/ptRedeemHandler.ts
2013
+ import { getAddress as getAddress7 } from "viem";
1920
2014
  import {
1921
2015
  signBurnRequest,
1922
2016
  POINT_TOKEN_ABI as POINT_TOKEN_ABI2,
@@ -1942,10 +2036,9 @@ var PTRedeemHandler = class {
1942
2036
  relayService;
1943
2037
  provider;
1944
2038
  feeService;
1945
- pointTokenAddress;
1946
2039
  batchExecutorAddress;
1947
2040
  chainId;
1948
- domain;
2041
+ domainResolver;
1949
2042
  burnerSignerWallet;
1950
2043
  redeemLockDurationMs;
1951
2044
  signatureDeadlineSeconds;
@@ -1982,10 +2075,9 @@ var PTRedeemHandler = class {
1982
2075
  this.relayService = config.relayService;
1983
2076
  this.provider = config.provider;
1984
2077
  this.feeService = config.feeService;
1985
- this.pointTokenAddress = getAddress6(config.pointTokenAddress);
1986
- this.batchExecutorAddress = getAddress6(config.batchExecutorAddress);
2078
+ this.batchExecutorAddress = getAddress7(config.batchExecutorAddress);
1987
2079
  this.chainId = config.chainId;
1988
- this.domain = config.domain;
2080
+ this.domainResolver = config.domainResolver;
1989
2081
  this.burnerSignerWallet = config.burnerSignerWallet;
1990
2082
  if (this.burnerSignerWallet?.account?.type === "local") {
1991
2083
  console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
@@ -1998,7 +2090,7 @@ var PTRedeemHandler = class {
1998
2090
  }
1999
2091
  }
2000
2092
  async handle(request) {
2001
- if (getAddress6(request.authenticatedAddress) !== getAddress6(request.userAddress)) {
2093
+ if (getAddress7(request.authenticatedAddress) !== getAddress7(request.userAddress)) {
2002
2094
  throw new PTRedeemError(
2003
2095
  "UNAUTHORIZED",
2004
2096
  `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
@@ -2007,11 +2099,12 @@ var PTRedeemHandler = class {
2007
2099
  if (request.amount <= 0n) {
2008
2100
  throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
2009
2101
  }
2102
+ const pointTokenAddress = getAddress7(request.pointTokenAddress);
2010
2103
  if (this.redemptionService) {
2011
2104
  const decision = await this.redemptionService.evaluate(
2012
2105
  request.userAddress,
2013
2106
  request.amount,
2014
- this.pointTokenAddress
2107
+ pointTokenAddress
2015
2108
  );
2016
2109
  if (!decision.allowed) {
2017
2110
  const denial = decision.denial;
@@ -2025,7 +2118,7 @@ var PTRedeemHandler = class {
2025
2118
  let burnNonce;
2026
2119
  try {
2027
2120
  burnNonce = await this.provider.readContract({
2028
- address: this.pointTokenAddress,
2121
+ address: pointTokenAddress,
2029
2122
  abi: POINT_TOKEN_ABI2,
2030
2123
  functionName: "burnRequestNonces",
2031
2124
  args: [request.userAddress]
@@ -2036,27 +2129,27 @@ var PTRedeemHandler = class {
2036
2129
  `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
2037
2130
  );
2038
2131
  }
2039
- const userKey = getAddress6(request.userAddress).toLowerCase();
2040
- let userNonces = this.inFlightNonces.get(userKey);
2132
+ const nonceKey = `${getAddress7(request.userAddress).toLowerCase()}:${pointTokenAddress.toLowerCase()}`;
2133
+ let userNonces = this.inFlightNonces.get(nonceKey);
2041
2134
  if (!userNonces) {
2042
2135
  userNonces = /* @__PURE__ */ new Set();
2043
- this.inFlightNonces.set(userKey, userNonces);
2136
+ this.inFlightNonces.set(nonceKey, userNonces);
2044
2137
  }
2045
2138
  if (userNonces.has(burnNonce)) {
2046
2139
  throw new PTRedeemError(
2047
2140
  "NONCE_IN_FLIGHT",
2048
- `A burn request for nonce ${burnNonce} is already in progress for ${request.userAddress}. Retry after the current request completes.`
2141
+ `A burn request for nonce ${burnNonce} is already in progress for ${request.userAddress} on ${pointTokenAddress}. Retry after the current request completes.`
2049
2142
  );
2050
2143
  }
2051
2144
  userNonces.add(burnNonce);
2052
2145
  try {
2053
- return await this._handleAfterNonceLock(request, burnNonce);
2146
+ return await this._handleAfterNonceLock(request, burnNonce, pointTokenAddress);
2054
2147
  } finally {
2055
2148
  userNonces.delete(burnNonce);
2056
- if (userNonces.size === 0) this.inFlightNonces.delete(userKey);
2149
+ if (userNonces.size === 0) this.inFlightNonces.delete(nonceKey);
2057
2150
  }
2058
2151
  }
2059
- async _handleAfterNonceLock(request, burnNonce) {
2152
+ async _handleAfterNonceLock(request, burnNonce, pointTokenAddress) {
2060
2153
  const previewDeadline = BigInt(
2061
2154
  Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
2062
2155
  );
@@ -2067,13 +2160,13 @@ var PTRedeemHandler = class {
2067
2160
  const previewUserOp = this.relayService.previewBurnUserOp({
2068
2161
  userAddress: request.userAddress,
2069
2162
  aaNonce: burnNonce,
2070
- pointTokenAddress: this.pointTokenAddress,
2163
+ pointTokenAddress,
2071
2164
  amount: request.amount,
2072
2165
  deadline: previewDeadline
2073
2166
  });
2074
2167
  fee = await this.feeService.estimateGasFee({
2075
2168
  scenario: "burn",
2076
- contractAddress: this.pointTokenAddress,
2169
+ contractAddress: pointTokenAddress,
2077
2170
  partialUserOp: {
2078
2171
  sender: previewUserOp.sender,
2079
2172
  nonce: previewUserOp.nonce,
@@ -2092,7 +2185,7 @@ var PTRedeemHandler = class {
2092
2185
  }
2093
2186
  const onChainBalance = await getPointTokenBalance2(
2094
2187
  this.provider,
2095
- this.pointTokenAddress,
2188
+ pointTokenAddress,
2096
2189
  request.userAddress
2097
2190
  );
2098
2191
  if (onChainBalance < request.amount) {
@@ -2102,10 +2195,11 @@ var PTRedeemHandler = class {
2102
2195
  );
2103
2196
  }
2104
2197
  const deadline = previewDeadline;
2198
+ const domainName = await this.domainResolver.resolve(pointTokenAddress);
2105
2199
  const domain = {
2106
- name: this.domain.name,
2200
+ name: domainName,
2107
2201
  chainId: this.chainId,
2108
- verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
2202
+ verifyingContract: pointTokenAddress
2109
2203
  };
2110
2204
  const sponsoredBurnAmount = request.amount - fee;
2111
2205
  const sponsoredBurnRequest = {
@@ -2127,14 +2221,14 @@ var PTRedeemHandler = class {
2127
2221
  request.userAddress,
2128
2222
  sponsoredBurnAmount,
2129
2223
  this.redeemLockDurationMs,
2130
- this.pointTokenAddress
2224
+ pointTokenAddress
2131
2225
  );
2132
2226
  try {
2133
2227
  const sponsoredUserOp = await this.relayService.prepareBurn({
2134
2228
  mode: "burnWithSig",
2135
2229
  userAddress: request.userAddress,
2136
2230
  aaNonce: request.aaNonce,
2137
- pointTokenAddress: this.pointTokenAddress,
2231
+ pointTokenAddress,
2138
2232
  batchExecutorAddress: this.batchExecutorAddress,
2139
2233
  burnRequest: sponsoredBurnRequest,
2140
2234
  burnerSignature: sponsoredSig,
@@ -2162,7 +2256,7 @@ var PTRedeemHandler = class {
2162
2256
  request.userAddress,
2163
2257
  request.amount,
2164
2258
  this.redeemLockDurationMs,
2165
- this.pointTokenAddress
2259
+ pointTokenAddress
2166
2260
  );
2167
2261
  let fallbackUserOp;
2168
2262
  try {
@@ -2170,7 +2264,7 @@ var PTRedeemHandler = class {
2170
2264
  mode: "burnWithSig",
2171
2265
  userAddress: request.userAddress,
2172
2266
  aaNonce: request.aaNonce,
2173
- pointTokenAddress: this.pointTokenAddress,
2267
+ pointTokenAddress,
2174
2268
  batchExecutorAddress: this.batchExecutorAddress,
2175
2269
  burnRequest: fallbackBurnRequest,
2176
2270
  burnerSignature: fallbackSig,
@@ -2195,7 +2289,7 @@ var PTRedeemHandler = class {
2195
2289
  await this.redemptionService.recordSuccessfulInitiate({
2196
2290
  user: request.userAddress,
2197
2291
  amountPt: request.amount,
2198
- pointTokenAddress: this.pointTokenAddress,
2292
+ pointTokenAddress,
2199
2293
  reservationId: sponsoredLockId
2200
2294
  }).catch(() => {
2201
2295
  });
@@ -2318,7 +2412,7 @@ async function handleRedeemStatus(params) {
2318
2412
  }
2319
2413
 
2320
2414
  // src/api/mobileHandlers.ts
2321
- import { getAddress as getAddress7 } from "viem";
2415
+ import { getAddress as getAddress8 } from "viem";
2322
2416
  import {
2323
2417
  ENTRY_POINT_V08,
2324
2418
  parseEip7702DelegatedAddress
@@ -2670,7 +2764,7 @@ async function handleMobileSubmit(params) {
2670
2764
  if (!entry) {
2671
2765
  throw new PendingUserOpNotFoundError(params.lockId);
2672
2766
  }
2673
- if (getAddress7(entry.sender) !== getAddress7(params.authenticatedAddress)) {
2767
+ if (getAddress8(entry.sender) !== getAddress8(params.authenticatedAddress)) {
2674
2768
  throw new PendingUserOpForbiddenError(params.lockId);
2675
2769
  }
2676
2770
  const variant = params.variant ?? "sponsored";
@@ -2686,7 +2780,7 @@ async function handleMobileSubmit(params) {
2686
2780
  }
2687
2781
 
2688
2782
  // src/api/handlers/ptClaimHandler.ts
2689
- import { getAddress as getAddress8 } from "viem";
2783
+ import { getAddress as getAddress9 } from "viem";
2690
2784
  import {
2691
2785
  decodeBatchExecuteCalls,
2692
2786
  getContractAddresses as getContractAddresses3
@@ -2735,7 +2829,7 @@ var PTClaimHandler = class {
2735
2829
  };
2736
2830
  }
2737
2831
  async handle(request) {
2738
- if (getAddress8(request.authenticatedAddress) !== getAddress8(request.userAddress)) {
2832
+ if (getAddress9(request.authenticatedAddress) !== getAddress9(request.userAddress)) {
2739
2833
  throw new PTClaimError(
2740
2834
  "VALIDATION_FAILED",
2741
2835
  `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
@@ -2790,8 +2884,11 @@ var PTClaimHandler = class {
2790
2884
  callData: previewUserOp.callData
2791
2885
  }
2792
2886
  }) : 0n;
2887
+ const domainName = await this.cfg.domainResolver.resolve(
2888
+ request.pointTokenAddress
2889
+ );
2793
2890
  const domain = {
2794
- name: this.cfg.pointTokenDomainName,
2891
+ name: domainName,
2795
2892
  chainId: request.chainId,
2796
2893
  verifyingContract: request.pointTokenAddress
2797
2894
  };
@@ -2807,8 +2904,16 @@ var PTClaimHandler = class {
2807
2904
  domain,
2808
2905
  mintRequestNonce: request.mintRequestNonce,
2809
2906
  deadline: signatureDeadline,
2810
- mintFeeWrapperAddress: resolvedWrapper
2811
- // No feeAmount/feeRecipient RelayService auto-resolves.
2907
+ mintFeeWrapperAddress: resolvedWrapper,
2908
+ // Pass the bundler-estimated `feeAmount` explicitly so the
2909
+ // RelayService skips its legacy `quoteOperatorFeePt` path
2910
+ // (which uses the SDK's old 12_000 bps premium default).
2911
+ // Without this, the response's `feeAmount` (from FeeManager,
2912
+ // 100% premium on top of PAFI's 110% server-side estimate)
2913
+ // would diverge from the actual PT.transfer amount in the
2914
+ // UserOp batch (`quoteOperatorFeePt`'s 120%), and the user
2915
+ // would see one value while the wallet transferred another.
2916
+ feeAmount
2812
2917
  });
2813
2918
  } catch (err) {
2814
2919
  throw new PTClaimError(
@@ -3006,7 +3111,7 @@ import {
3006
3111
  getContractAddresses as getContractAddresses5,
3007
3112
  serializeUserOpToJsonRpc as serializeUserOpToJsonRpc2
3008
3113
  } from "@pafi-dev/core";
3009
- import { getAddress as getAddress9 } from "viem";
3114
+ import { getAddress as getAddress10 } from "viem";
3010
3115
  var DEFAULT_DELEGATE_GAS = {
3011
3116
  callGasLimit: 100000n,
3012
3117
  verificationGasLimit: 150000n,
@@ -3093,7 +3198,7 @@ async function handleDelegateSubmit(params) {
3093
3198
  if (!entry) {
3094
3199
  throw new PendingUserOpNotFoundError(params.lockId);
3095
3200
  }
3096
- if (getAddress9(entry.sender) !== getAddress9(params.authenticatedAddress)) {
3201
+ if (getAddress10(entry.sender) !== getAddress10(params.authenticatedAddress)) {
3097
3202
  throw new PendingUserOpForbiddenError(params.lockId);
3098
3203
  }
3099
3204
  if (!entry.eip7702Auth) {
@@ -3114,7 +3219,7 @@ async function handleDelegateSubmit(params) {
3114
3219
 
3115
3220
  // src/api/issuerApiAdapter.ts
3116
3221
  import { randomUUID } from "crypto";
3117
- import { getAddress as getAddress10 } from "viem";
3222
+ import { getAddress as getAddress11 } from "viem";
3118
3223
  import {
3119
3224
  buildAndSignSponsorAuth,
3120
3225
  decodeBatchExecuteCalls as decodeBatchExecuteCalls3,
@@ -3179,7 +3284,7 @@ var IssuerApiAdapter = class {
3179
3284
  async pools(authenticatedAddress, chainId, pointTokenAddress) {
3180
3285
  const result = await this.cfg.issuerService.api.handlePools(
3181
3286
  authenticatedAddress,
3182
- { chainId, pointTokenAddress: getAddress10(pointTokenAddress) }
3287
+ { chainId, pointTokenAddress: getAddress11(pointTokenAddress) }
3183
3288
  );
3184
3289
  return { pools: result.pools };
3185
3290
  }
@@ -3188,8 +3293,8 @@ var IssuerApiAdapter = class {
3188
3293
  authenticatedAddress,
3189
3294
  {
3190
3295
  chainId,
3191
- userAddress: getAddress10(userAddress),
3192
- pointTokenAddress: getAddress10(pointTokenAddress)
3296
+ userAddress: getAddress11(userAddress),
3297
+ pointTokenAddress: getAddress11(pointTokenAddress)
3193
3298
  }
3194
3299
  );
3195
3300
  return {
@@ -3210,7 +3315,7 @@ var IssuerApiAdapter = class {
3210
3315
  "ptClaimHandler",
3211
3316
  "claim"
3212
3317
  );
3213
- const pointTokenAddress = getAddress10(input.pointTokenAddress);
3318
+ const pointTokenAddress = getAddress11(input.pointTokenAddress);
3214
3319
  const result = await ptClaimHandler.handle({
3215
3320
  authenticatedAddress: input.authenticatedAddress,
3216
3321
  userAddress: input.authenticatedAddress,
@@ -3237,9 +3342,11 @@ var IssuerApiAdapter = class {
3237
3342
  }
3238
3343
  async redeem(input) {
3239
3344
  this.assertRedeemHandler();
3345
+ const pointTokenAddress = getAddress11(input.pointTokenAddress);
3240
3346
  const response = await this.cfg.ptRedeemHandler.handle({
3241
3347
  userAddress: input.authenticatedAddress,
3242
3348
  authenticatedAddress: input.authenticatedAddress,
3349
+ pointTokenAddress,
3243
3350
  amount: input.amount,
3244
3351
  aaNonce: input.aaNonce,
3245
3352
  chainId: input.chainId
@@ -3305,7 +3412,7 @@ var IssuerApiAdapter = class {
3305
3412
  "ptClaimHandler",
3306
3413
  "claimPrepare"
3307
3414
  );
3308
- const pointTokenAddress = getAddress10(input.pointTokenAddress);
3415
+ const pointTokenAddress = getAddress11(input.pointTokenAddress);
3309
3416
  const claimResult = await ptClaimHandler.handle({
3310
3417
  authenticatedAddress: input.authenticatedAddress,
3311
3418
  userAddress: input.authenticatedAddress,
@@ -3351,10 +3458,11 @@ var IssuerApiAdapter = class {
3351
3458
  }
3352
3459
  async redeemPrepare(input) {
3353
3460
  this.assertRedeemHandler();
3354
- const pointTokenAddress = getAddress10(input.pointTokenAddress);
3461
+ const pointTokenAddress = getAddress11(input.pointTokenAddress);
3355
3462
  const redeemResponse = await this.cfg.ptRedeemHandler.handle({
3356
3463
  userAddress: input.authenticatedAddress,
3357
3464
  authenticatedAddress: input.authenticatedAddress,
3465
+ pointTokenAddress,
3358
3466
  amount: input.amount,
3359
3467
  aaNonce: input.aaNonce,
3360
3468
  chainId: input.chainId
@@ -4102,7 +4210,7 @@ var PafiBackendClient = class {
4102
4210
  };
4103
4211
 
4104
4212
  // src/config.ts
4105
- import { getAddress as getAddress11 } from "viem";
4213
+ import { getAddress as getAddress12 } from "viem";
4106
4214
  import { getContractAddresses as getContractAddresses7 } from "@pafi-dev/core";
4107
4215
 
4108
4216
  // src/redemption/evaluator.ts
@@ -4412,7 +4520,7 @@ var RedemptionService = class {
4412
4520
  };
4413
4521
 
4414
4522
  // src/config.ts
4415
- function createIssuerService(config) {
4523
+ async function createIssuerService(config) {
4416
4524
  if (!config.provider) {
4417
4525
  throw new Error("createIssuerService: provider is required");
4418
4526
  }
@@ -4431,7 +4539,7 @@ function createIssuerService(config) {
4431
4539
  "createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
4432
4540
  );
4433
4541
  }
4434
- const tokenAddresses = rawAddresses.map((a) => getAddress11(a));
4542
+ const tokenAddresses = rawAddresses.map((a) => getAddress12(a));
4435
4543
  const ledger = config.ledger;
4436
4544
  const sessionStore = config.sessionStore ?? new MemorySessionStore();
4437
4545
  const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
@@ -4531,9 +4639,26 @@ function createIssuerService(config) {
4531
4639
  handlersConfig.mintFeeWrapperAddress = resolvedWrapperAddress;
4532
4640
  }
4533
4641
  const handlers = new IssuerApiHandlers(handlersConfig);
4642
+ const indexerLeaderLocks = [];
4534
4643
  if (config.indexer?.autoStart) {
4535
- for (const idx of indexers.values()) {
4536
- idx.start();
4644
+ const lock = config.indexer.singletonLock;
4645
+ if (!lock) {
4646
+ console.warn(
4647
+ "[@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."
4648
+ );
4649
+ for (const idx of indexers.values()) {
4650
+ idx.start();
4651
+ }
4652
+ } else {
4653
+ for (const [tokenAddr, idx] of indexers.entries()) {
4654
+ const key = `pafi-issuer:point-indexer:${tokenAddr.toLowerCase()}`;
4655
+ const handle = await lock.acquire(key);
4656
+ if (!handle) {
4657
+ continue;
4658
+ }
4659
+ idx.start();
4660
+ indexerLeaderLocks.push(handle);
4661
+ }
4537
4662
  }
4538
4663
  }
4539
4664
  return {
@@ -4544,13 +4669,14 @@ function createIssuerService(config) {
4544
4669
  relay: relayService,
4545
4670
  fee: feeManager,
4546
4671
  indexers,
4672
+ indexerLeaderLocks,
4547
4673
  api: handlers,
4548
4674
  redemption
4549
4675
  };
4550
4676
  }
4551
4677
 
4552
4678
  // src/issuer-state/validator.ts
4553
- import { getAddress as getAddress12 } from "viem";
4679
+ import { getAddress as getAddress13 } from "viem";
4554
4680
  import {
4555
4681
  POINT_TOKEN_ABI as POINT_TOKEN_ABI3,
4556
4682
  issuerRegistryAbi,
@@ -4582,7 +4708,7 @@ var IssuerStateValidator = class _IssuerStateValidator {
4582
4708
  */
4583
4709
  invalidate(pointToken) {
4584
4710
  if (pointToken) {
4585
- const key = getAddress12(pointToken);
4711
+ const key = getAddress13(pointToken);
4586
4712
  this.pointTokenIssuerCache.delete(key);
4587
4713
  this.stateCache.delete(key);
4588
4714
  this.inflight.delete(key);
@@ -4597,7 +4723,7 @@ var IssuerStateValidator = class _IssuerStateValidator {
4597
4723
  * The issuer field is set at `initialize()` and never changes.
4598
4724
  */
4599
4725
  async getIssuerAddressForPointToken(pointToken) {
4600
- const key = getAddress12(pointToken);
4726
+ const key = getAddress13(pointToken);
4601
4727
  const cached = this.pointTokenIssuerCache.get(key);
4602
4728
  if (cached) return cached;
4603
4729
  const issuer = await this.provider.readContract({
@@ -4605,15 +4731,15 @@ var IssuerStateValidator = class _IssuerStateValidator {
4605
4731
  abi: POINT_TOKEN_ABI3,
4606
4732
  functionName: "issuer"
4607
4733
  });
4608
- this.pointTokenIssuerCache.set(key, getAddress12(issuer));
4609
- return getAddress12(issuer);
4734
+ this.pointTokenIssuerCache.set(key, getAddress13(issuer));
4735
+ return getAddress13(issuer);
4610
4736
  }
4611
4737
  /**
4612
4738
  * Read registry record + totalSupply, with 30s cache and in-flight
4613
4739
  * deduplication. Does NOT throw on inactive/missing — returns raw state.
4614
4740
  */
4615
4741
  async getIssuerState(pointToken) {
4616
- const tokenAddr = getAddress12(pointToken);
4742
+ const tokenAddr = getAddress13(pointToken);
4617
4743
  const now = Date.now();
4618
4744
  const cached = this.stateCache.get(tokenAddr);
4619
4745
  if (cached && cached.expiresAt > now) return cached.value;
@@ -4756,7 +4882,7 @@ var MemoryRedemptionHistoryStore = class {
4756
4882
  };
4757
4883
 
4758
4884
  // src/index.ts
4759
- var PAFI_ISSUER_SDK_VERSION = true ? "0.20.0" : "dev";
4885
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.22.0" : "dev";
4760
4886
  export {
4761
4887
  AdapterMisconfiguredError,
4762
4888
  AuthError,
@@ -4795,6 +4921,7 @@ export {
4795
4921
  PerpDepositError,
4796
4922
  PerpDepositHandler,
4797
4923
  PointIndexer,
4924
+ PointTokenDomainResolver,
4798
4925
  PolicyProvider,
4799
4926
  REDEMPTION_HISTORY_WINDOW_SEC,
4800
4927
  RedemptionService,
@@ -4821,6 +4948,7 @@ export {
4821
4948
  handleMobilePrepare,
4822
4949
  handleMobileSubmit,
4823
4950
  handleRedeemStatus,
4951
+ makePostgresSingletonLock,
4824
4952
  mergePaymasterFields,
4825
4953
  payloadFromGenericError,
4826
4954
  payloadFromHttpException,