@pafi-dev/issuer 0.3.0-beta.9 → 0.4.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.cjs CHANGED
@@ -28,15 +28,14 @@ __export(index_exports, {
28
28
  FeeManager: () => FeeManager,
29
29
  InMemoryCursorStore: () => InMemoryCursorStore,
30
30
  IssuerApiHandlers: () => IssuerApiHandlers,
31
- MemoryPointLedger: () => MemoryPointLedger,
32
31
  MemorySessionStore: () => MemorySessionStore,
33
32
  NonceManager: () => NonceManager,
34
33
  PAFI_ISSUER_SDK_VERSION: () => PAFI_ISSUER_SDK_VERSION,
35
34
  PTRedeemError: () => PTRedeemError,
36
35
  PTRedeemHandler: () => PTRedeemHandler,
36
+ PafiBackendClient: () => PafiBackendClient,
37
37
  PafiBackendError: () => PafiBackendError,
38
38
  PointIndexer: () => PointIndexer,
39
- PrivateKeySigner: () => PrivateKeySigner,
40
39
  RelayError: () => RelayError,
41
40
  RelayService: () => RelayService,
42
41
  TopUpRedemptionError: () => TopUpRedemptionError,
@@ -48,204 +47,6 @@ __export(index_exports, {
48
47
  });
49
48
  module.exports = __toCommonJS(index_exports);
50
49
 
51
- // src/ledger/memoryLedger.ts
52
- var import_viem = require("viem");
53
- var MemoryPointLedger = class {
54
- balances = /* @__PURE__ */ new Map();
55
- locks = /* @__PURE__ */ new Map();
56
- nextLockId = 1;
57
- now;
58
- constructor(opts = {}) {
59
- this.now = opts.now ?? (() => Date.now());
60
- }
61
- // -------------------------------------------------------------------------
62
- // Read
63
- // -------------------------------------------------------------------------
64
- async getBalance(userAddress, tokenAddress) {
65
- const user = (0, import_viem.getAddress)(userAddress);
66
- const token = normalizeToken(tokenAddress);
67
- this.purgeExpired();
68
- const total = this.balances.get(balanceKey(user, token)) ?? 0n;
69
- const locked = this.lockedTotalFor(user, token);
70
- return total - locked;
71
- }
72
- async getLockedRequests(userAddress, tokenAddress) {
73
- const user = (0, import_viem.getAddress)(userAddress);
74
- const token = normalizeToken(tokenAddress);
75
- this.purgeExpired();
76
- const out = [];
77
- for (const lock of this.locks.values()) {
78
- if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
79
- out.push({ ...lock });
80
- }
81
- }
82
- return out;
83
- }
84
- // -------------------------------------------------------------------------
85
- // Write
86
- // -------------------------------------------------------------------------
87
- async creditBalance(userAddress, amount, _reason, tokenAddress) {
88
- if (amount <= 0n) {
89
- throw new Error("MemoryPointLedger: credit amount must be positive");
90
- }
91
- const user = (0, import_viem.getAddress)(userAddress);
92
- const token = normalizeToken(tokenAddress);
93
- const key = balanceKey(user, token);
94
- const current = this.balances.get(key) ?? 0n;
95
- this.balances.set(key, current + amount);
96
- }
97
- async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
98
- if (amount <= 0n) {
99
- throw new Error("MemoryPointLedger: lock amount must be positive");
100
- }
101
- if (lockDurationMs <= 0) {
102
- throw new Error("MemoryPointLedger: lockDurationMs must be positive");
103
- }
104
- const user = (0, import_viem.getAddress)(userAddress);
105
- const token = normalizeToken(tokenAddress);
106
- this.purgeExpired();
107
- const total = this.balances.get(balanceKey(user, token)) ?? 0n;
108
- const alreadyLocked = this.lockedTotalFor(user, token);
109
- const available = total - alreadyLocked;
110
- if (available < amount) {
111
- throw new Error(
112
- `MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
113
- );
114
- }
115
- const lockId = `lock-${this.nextLockId++}`;
116
- const now = this.now();
117
- const lock = {
118
- lockId,
119
- userAddress: user,
120
- amount,
121
- status: "PENDING",
122
- createdAt: now,
123
- expiresAt: now + lockDurationMs
124
- };
125
- if (tokenAddress !== void 0) {
126
- lock.tokenAddress = (0, import_viem.getAddress)(tokenAddress);
127
- }
128
- this.locks.set(lockId, lock);
129
- return lockId;
130
- }
131
- async releaseLock(lockId) {
132
- const lock = this.locks.get(lockId);
133
- if (!lock) return;
134
- if (lock.status === "PENDING") {
135
- this.locks.delete(lockId);
136
- }
137
- }
138
- async deductBalance(userAddress, amount, txHash, tokenAddress) {
139
- if (amount <= 0n) {
140
- throw new Error("MemoryPointLedger: deduct amount must be positive");
141
- }
142
- const user = (0, import_viem.getAddress)(userAddress);
143
- const token = normalizeToken(tokenAddress);
144
- const key = balanceKey(user, token);
145
- const current = this.balances.get(key) ?? 0n;
146
- if (current < amount) {
147
- throw new Error(
148
- `MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
149
- );
150
- }
151
- this.balances.set(key, current - amount);
152
- for (const lock of this.locks.values()) {
153
- if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
154
- lock.status = "MINTED";
155
- lock.txHash = txHash;
156
- return;
157
- }
158
- }
159
- }
160
- async updateMintStatus(lockId, status, txHash) {
161
- const lock = this.locks.get(lockId);
162
- if (!lock) {
163
- throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
164
- }
165
- lock.status = status;
166
- if (txHash) lock.txHash = txHash;
167
- }
168
- // -------------------------------------------------------------------------
169
- // v1.4 — Reverse flow (PT burn → off-chain credit)
170
- // -------------------------------------------------------------------------
171
- pendingCredits = /* @__PURE__ */ new Map();
172
- nextCreditId = 1;
173
- async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
174
- if (amount <= 0n) {
175
- throw new Error(
176
- "MemoryPointLedger: pending credit amount must be positive"
177
- );
178
- }
179
- if (durationMs <= 0) {
180
- throw new Error("MemoryPointLedger: durationMs must be positive");
181
- }
182
- const user = (0, import_viem.getAddress)(userAddress);
183
- const lockId = `credit-${this.nextCreditId++}`;
184
- const now = this.now();
185
- this.pendingCredits.set(lockId, {
186
- lockId,
187
- userAddress: user,
188
- amount,
189
- tokenAddress: tokenAddress !== void 0 ? (0, import_viem.getAddress)(tokenAddress) : void 0,
190
- createdAt: now,
191
- expiresAt: now + durationMs,
192
- status: "PENDING"
193
- });
194
- return lockId;
195
- }
196
- async resolveCreditByBurnTx(lockId, txHash) {
197
- const credit = this.pendingCredits.get(lockId);
198
- if (!credit) {
199
- throw new Error(
200
- `MemoryPointLedger: unknown pending credit lockId ${lockId}`
201
- );
202
- }
203
- if (credit.status === "RESOLVED") {
204
- if (credit.txHash === txHash) return;
205
- throw new Error(
206
- `MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
207
- );
208
- }
209
- const token = normalizeToken(credit.tokenAddress);
210
- const key = balanceKey(credit.userAddress, token);
211
- const current = this.balances.get(key) ?? 0n;
212
- this.balances.set(key, current + credit.amount);
213
- credit.status = "RESOLVED";
214
- credit.txHash = txHash;
215
- }
216
- // -------------------------------------------------------------------------
217
- // Internal helpers
218
- // -------------------------------------------------------------------------
219
- /**
220
- * Auto-expire any PENDING lock past its expiry. Called lazily on every
221
- * read/write so the in-memory state stays self-cleaning without a timer.
222
- */
223
- purgeExpired() {
224
- const now = this.now();
225
- for (const lock of this.locks.values()) {
226
- if (lock.status === "PENDING" && lock.expiresAt <= now) {
227
- lock.status = "EXPIRED";
228
- }
229
- }
230
- }
231
- lockedTotalFor(userAddress, tokenKey) {
232
- let total = 0n;
233
- for (const lock of this.locks.values()) {
234
- if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
235
- total += lock.amount;
236
- }
237
- }
238
- return total;
239
- }
240
- };
241
- var DEFAULT_TOKEN_KEY = "default";
242
- function normalizeToken(tokenAddress) {
243
- return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : (0, import_viem.getAddress)(tokenAddress);
244
- }
245
- function balanceKey(user, tokenKey) {
246
- return `${user}|${tokenKey}`;
247
- }
248
-
249
50
  // src/policy/defaultPolicy.ts
250
51
  var DefaultPolicyEngine = class {
251
52
  ledger;
@@ -261,6 +62,13 @@ var DefaultPolicyEngine = class {
261
62
  }
262
63
  if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
263
64
  if (opts.resolveIssuer) this.resolveIssuer = opts.resolveIssuer;
65
+ if (!opts.mintingOracleAddress || !opts.provider || !opts.verifyMintCap || !opts.resolveIssuer) {
66
+ if (process.env.NODE_ENV === "production") {
67
+ throw new Error(
68
+ "[PAFI] DefaultPolicyEngine: on-chain MintingOracle cap check is required in production. Configure mintingOracleAddress, provider, verifyMintCap, and resolveIssuer."
69
+ );
70
+ }
71
+ }
264
72
  }
265
73
  async evaluate(request) {
266
74
  if (request.amount <= 0n) {
@@ -297,32 +105,9 @@ var DefaultPolicyEngine = class {
297
105
  }
298
106
  };
299
107
 
300
- // src/signer/privateKeySigner.ts
301
- var import_viem2 = require("viem");
302
- var import_accounts = require("viem/accounts");
303
- var import_core = require("@pafi-dev/core");
304
- var PrivateKeySigner = class {
305
- account;
306
- walletClient;
307
- constructor(opts) {
308
- this.account = (0, import_accounts.privateKeyToAccount)(opts.privateKey);
309
- this.walletClient = (0, import_viem2.createWalletClient)({
310
- account: this.account,
311
- chain: opts.chain,
312
- transport: (0, import_viem2.http)(opts.rpcUrl)
313
- });
314
- }
315
- async signMintRequest(domain, message) {
316
- return (0, import_core.signMintRequest)(this.walletClient, domain, message);
317
- }
318
- async getAddress() {
319
- return this.account.address;
320
- }
321
- };
322
-
323
108
  // src/auth/memorySessionStore.ts
324
109
  var import_node_crypto = require("crypto");
325
- var import_viem3 = require("viem");
110
+ var import_viem = require("viem");
326
111
  var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
327
112
  var MemorySessionStore = class {
328
113
  nonces = /* @__PURE__ */ new Map();
@@ -332,6 +117,11 @@ var MemorySessionStore = class {
332
117
  nonceTtlMs;
333
118
  now;
334
119
  constructor(opts = {}) {
120
+ if (process.env.NODE_ENV === "production") {
121
+ console.error(
122
+ "[PAFI] MemorySessionStore is not safe for multi-process/K8s deployments. Session revocations are NOT propagated across pods. Use a Redis-backed session store in production."
123
+ );
124
+ }
335
125
  this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
336
126
  this.now = opts.now ?? (() => Date.now());
337
127
  }
@@ -358,7 +148,7 @@ var MemorySessionStore = class {
358
148
  this.purgeExpiredSessions();
359
149
  const normalized = {
360
150
  ...session,
361
- userAddress: (0, import_viem3.getAddress)(session.userAddress)
151
+ userAddress: (0, import_viem.getAddress)(session.userAddress)
362
152
  };
363
153
  this.sessions.set(session.tokenId, normalized);
364
154
  }
@@ -376,7 +166,7 @@ var MemorySessionStore = class {
376
166
  this.sessions.delete(tokenId);
377
167
  }
378
168
  async revokeAllSessions(userAddress) {
379
- const key = (0, import_viem3.getAddress)(userAddress);
169
+ const key = (0, import_viem.getAddress)(userAddress);
380
170
  for (const [tokenId, session] of this.sessions.entries()) {
381
171
  if (session.userAddress === key) {
382
172
  this.sessions.delete(tokenId);
@@ -422,8 +212,8 @@ var NonceManager = class {
422
212
  // src/auth/loginVerifier.ts
423
213
  var import_node_crypto2 = require("crypto");
424
214
  var import_jose = require("jose");
425
- var import_viem4 = require("viem");
426
- var import_core2 = require("@pafi-dev/core");
215
+ var import_viem2 = require("viem");
216
+ var import_core = require("@pafi-dev/core");
427
217
 
428
218
  // src/auth/errors.ts
429
219
  var AuthError = class extends Error {
@@ -446,8 +236,8 @@ var AuthService = class {
446
236
  nonceManager;
447
237
  now;
448
238
  constructor(config) {
449
- if (!config.jwtSecret || config.jwtSecret.length < 16) {
450
- throw new Error("AuthService: jwtSecret must be at least 16 chars");
239
+ if (!config.jwtSecret || config.jwtSecret.length < 32) {
240
+ throw new Error("AuthService: jwtSecret must be at least 32 characters for HS256 security");
451
241
  }
452
242
  this.sessionStore = config.sessionStore;
453
243
  this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
@@ -471,11 +261,17 @@ var AuthService = class {
471
261
  async login(message, signature) {
472
262
  let parsed;
473
263
  try {
474
- parsed = (0, import_core2.parseLoginMessage)(message);
264
+ parsed = (0, import_core.parseLoginMessage)(message);
475
265
  } catch (err) {
476
266
  const msg = err instanceof Error ? err.message : String(err);
477
267
  throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
478
268
  }
269
+ if (parsed.expirationTime == null) {
270
+ throw new AuthError(
271
+ "INVALID_MESSAGE",
272
+ "login message must include expirationTime"
273
+ );
274
+ }
479
275
  if (parsed.domain !== this.domain) {
480
276
  throw new AuthError(
481
277
  "DOMAIN_MISMATCH",
@@ -498,7 +294,7 @@ var AuthService = class {
498
294
  if (parsed.expirationTime && parsed.expirationTime.getTime() <= now.getTime()) {
499
295
  throw new AuthError("MESSAGE_EXPIRED", "Login message has expired");
500
296
  }
501
- const verifyResult = await (0, import_core2.verifyLoginMessage)(message, signature);
297
+ const verifyResult = await (0, import_core.verifyLoginMessage)(message, signature);
502
298
  if (!verifyResult.valid) {
503
299
  throw new AuthError(
504
300
  "SIGNATURE_INVALID",
@@ -512,7 +308,7 @@ var AuthService = class {
512
308
  "Nonce is unknown, expired, or already used"
513
309
  );
514
310
  }
515
- const userAddress = (0, import_viem4.getAddress)(verifyResult.address);
311
+ const userAddress = (0, import_viem2.getAddress)(verifyResult.address);
516
312
  const tokenId = (0, import_node_crypto2.randomBytes)(16).toString("hex");
517
313
  const issuedAt = now;
518
314
  const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
@@ -540,7 +336,11 @@ var AuthService = class {
540
336
  if (payload.jti) {
541
337
  await this.sessionStore.revokeSession(payload.jti);
542
338
  }
543
- } catch {
339
+ } catch (err) {
340
+ const msg = err instanceof Error ? err.message : String(err);
341
+ if (!msg.includes("not found") && !msg.includes("expired")) {
342
+ console.error("[PAFI] AuthService logout: session store error", err);
343
+ }
544
344
  }
545
345
  }
546
346
  /**
@@ -579,7 +379,7 @@ var AuthService = class {
579
379
  throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
580
380
  }
581
381
  return {
582
- userAddress: (0, import_viem4.getAddress)(userAddress),
382
+ userAddress: (0, import_viem2.getAddress)(userAddress),
583
383
  chainId,
584
384
  tokenId
585
385
  };
@@ -630,8 +430,8 @@ var RelayError = class extends Error {
630
430
  };
631
431
 
632
432
  // src/relay/relayService.ts
633
- var import_viem5 = require("viem");
634
- var import_core3 = require("@pafi-dev/core");
433
+ var import_viem3 = require("viem");
434
+ var import_core2 = require("@pafi-dev/core");
635
435
  var RelayService = class {
636
436
  /**
637
437
  * Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
@@ -665,9 +465,20 @@ var RelayService = class {
665
465
  if (params.deadline <= 0n) {
666
466
  throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
667
467
  }
468
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
469
+ if (params.deadline <= nowSecs) {
470
+ throw new RelayError("ENCODE_FAILED", "prepareMint: deadline is in the past");
471
+ }
472
+ const MAX_DEADLINE_WINDOW = 3600n;
473
+ if (params.deadline > nowSecs + MAX_DEADLINE_WINDOW) {
474
+ throw new RelayError(
475
+ "ENCODE_FAILED",
476
+ "prepareMint: deadline exceeds maximum allowed window (1 hour)"
477
+ );
478
+ }
668
479
  let minterSig;
669
480
  try {
670
- const sig = await (0, import_core3.signMintRequest)(
481
+ const sig = await (0, import_core2.signMintRequest)(
671
482
  params.issuerSignerWallet,
672
483
  params.domain,
673
484
  {
@@ -687,8 +498,8 @@ var RelayService = class {
687
498
  }
688
499
  let mintCallData;
689
500
  try {
690
- mintCallData = (0, import_viem5.encodeFunctionData)({
691
- abi: import_core3.POINT_TOKEN_V2_ABI,
501
+ mintCallData = (0, import_viem3.encodeFunctionData)({
502
+ abi: import_core2.POINT_TOKEN_V2_ABI,
692
503
  functionName: "mint",
693
504
  args: [params.userAddress, params.amount, params.deadline, minterSig]
694
505
  });
@@ -713,17 +524,23 @@ var RelayService = class {
713
524
  "prepareMint: feeRecipient required when feeAmount > 0"
714
525
  );
715
526
  }
527
+ if (params.feeRecipient === "0x0000000000000000000000000000000000000000") {
528
+ throw new RelayError(
529
+ "ENCODE_FAILED",
530
+ "prepareMint: feeRecipient must not be zero address"
531
+ );
532
+ }
716
533
  operations.push({
717
534
  target: params.pointTokenAddress,
718
535
  value: 0n,
719
- data: (0, import_viem5.encodeFunctionData)({
720
- abi: import_viem5.erc20Abi,
536
+ data: (0, import_viem3.encodeFunctionData)({
537
+ abi: import_viem3.erc20Abi,
721
538
  functionName: "transfer",
722
539
  args: [params.feeRecipient, params.feeAmount]
723
540
  })
724
541
  });
725
542
  }
726
- return (0, import_core3.buildPartialUserOperation)({
543
+ return (0, import_core2.buildPartialUserOperation)({
727
544
  sender: params.userAddress,
728
545
  nonce: params.aaNonce,
729
546
  operations,
@@ -761,8 +578,8 @@ var RelayService = class {
761
578
  if (!params.burnRequest || !params.burnerSignature) {
762
579
  throw new Error("burnWithSig requires burnRequest + burnerSignature");
763
580
  }
764
- burnCallData = (0, import_viem5.encodeFunctionData)({
765
- abi: import_core3.POINT_TOKEN_V2_ABI,
581
+ burnCallData = (0, import_viem3.encodeFunctionData)({
582
+ abi: import_core2.POINT_TOKEN_V2_ABI,
766
583
  functionName: "burn",
767
584
  args: [
768
585
  params.burnRequest.from,
@@ -772,8 +589,8 @@ var RelayService = class {
772
589
  ]
773
590
  });
774
591
  } else {
775
- burnCallData = (0, import_viem5.encodeFunctionData)({
776
- abi: import_core3.POINT_TOKEN_V2_ABI,
592
+ burnCallData = (0, import_viem3.encodeFunctionData)({
593
+ abi: import_core2.POINT_TOKEN_V2_ABI,
777
594
  functionName: "burn",
778
595
  args: [params.userAddress, params.amount]
779
596
  });
@@ -792,7 +609,7 @@ var RelayService = class {
792
609
  data: burnCallData
793
610
  }
794
611
  ];
795
- return (0, import_core3.buildPartialUserOperation)({
612
+ return (0, import_core2.buildPartialUserOperation)({
796
613
  sender: params.userAddress,
797
614
  nonce: params.aaNonce,
798
615
  operations,
@@ -811,11 +628,14 @@ function errorMessage(err) {
811
628
  // src/relay/feeManager.ts
812
629
  var DEFAULT_GAS_UNITS = 500000n;
813
630
  var DEFAULT_PREMIUM_BPS = 12e3;
814
- var FeeManager = class {
631
+ var FeeManager = class _FeeManager {
815
632
  provider;
816
633
  gasUnits;
817
634
  gasPremiumBps;
818
635
  quoteNativeToFee;
636
+ cachedFee = null;
637
+ cacheExpiresAt = 0;
638
+ static CACHE_TTL_MS = 1e4;
819
639
  constructor(config) {
820
640
  if (!config.provider) throw new Error("FeeManager: provider required");
821
641
  if (!config.quoteNativeToFee)
@@ -838,10 +658,17 @@ var FeeManager = class {
838
658
  * currency depends on how the caller wired `quoteNativeToFee`.
839
659
  */
840
660
  async estimateGasFee() {
661
+ const now = Date.now();
662
+ if (this.cachedFee !== null && now < this.cacheExpiresAt) {
663
+ return this.cachedFee;
664
+ }
841
665
  const gasPrice = await this.provider.getGasPrice();
842
666
  const nativeCost = gasPrice * this.gasUnits;
843
667
  const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
844
- return this.quoteNativeToFee(withPremium);
668
+ const fee = await this.quoteNativeToFee(withPremium);
669
+ this.cachedFee = fee;
670
+ this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
671
+ return fee;
845
672
  }
846
673
  };
847
674
 
@@ -857,8 +684,8 @@ var InMemoryCursorStore = class {
857
684
  };
858
685
 
859
686
  // src/indexer/pointIndexer.ts
860
- var import_viem6 = require("viem");
861
- var TRANSFER_EVENT = (0, import_viem6.parseAbiItem)(
687
+ var import_viem4 = require("viem");
688
+ var TRANSFER_EVENT = (0, import_viem4.parseAbiItem)(
862
689
  "event Transfer(address indexed from, address indexed to, uint256 value)"
863
690
  );
864
691
  var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
@@ -928,7 +755,8 @@ var PointIndexer = class {
928
755
  return;
929
756
  }
930
757
  await this.processBlockRange(from, safeHead);
931
- } catch {
758
+ } catch (err) {
759
+ console.error("[PAFI] PointIndexer tick error:", err);
932
760
  }
933
761
  this.scheduleNext();
934
762
  }
@@ -978,10 +806,10 @@ var PointIndexer = class {
978
806
  for (const log of logs) {
979
807
  const args = log.args;
980
808
  if (!args.from || !args.to || args.value === void 0) continue;
981
- if ((0, import_viem6.getAddress)(args.from) !== ZERO_ADDRESS) continue;
809
+ if ((0, import_viem4.getAddress)(args.from) !== ZERO_ADDRESS) continue;
982
810
  if (log.blockNumber === null || log.transactionHash === null) continue;
983
811
  out.push({
984
- to: (0, import_viem6.getAddress)(args.to),
812
+ to: (0, import_viem4.getAddress)(args.to),
985
813
  amount: args.value,
986
814
  blockNumber: log.blockNumber,
987
815
  txHash: log.transactionHash,
@@ -1037,8 +865,8 @@ function pickMatchingLock(locks, amount) {
1037
865
  }
1038
866
 
1039
867
  // src/indexer/burnIndexer.ts
1040
- var import_viem7 = require("viem");
1041
- var TRANSFER_EVENT2 = (0, import_viem7.parseAbiItem)(
868
+ var import_viem5 = require("viem");
869
+ var TRANSFER_EVENT2 = (0, import_viem5.parseAbiItem)(
1042
870
  "event Transfer(address indexed from, address indexed to, uint256 value)"
1043
871
  );
1044
872
  var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
@@ -1054,18 +882,7 @@ var BurnIndexer = class {
1054
882
  confirmations;
1055
883
  batchSize;
1056
884
  pollIntervalMs;
1057
- /**
1058
- * Caller-supplied matcher. Return the lockId to resolve for a given
1059
- * burn event, or `undefined` to skip. Runs synchronously via the
1060
- * ledger's query path.
1061
- *
1062
- * Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
1063
- * lock id `burn-${from}-${amount}` — the in-memory ledger assigns
1064
- * incrementing IDs so callers with the memory ledger must provide a
1065
- * custom matcher. Real DB-backed ledgers override this to JOIN on
1066
- * their `pending_credits` table.
1067
- */
1068
- matchLockId = async () => void 0;
885
+ matchLockId;
1069
886
  running = false;
1070
887
  timer;
1071
888
  constructor(config) {
@@ -1083,6 +900,12 @@ var BurnIndexer = class {
1083
900
  );
1084
901
  this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
1085
902
  this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
903
+ if (!config.matchLockId) {
904
+ throw new Error(
905
+ "BurnIndexer: matchLockId is required. Provide a function that maps a burn event to its pending credit lockId. Without it, no on-chain burns will ever grant off-chain credits."
906
+ );
907
+ }
908
+ this.matchLockId = config.matchLockId;
1086
909
  }
1087
910
  start() {
1088
911
  if (this.running) return;
@@ -1112,7 +935,8 @@ var BurnIndexer = class {
1112
935
  return;
1113
936
  }
1114
937
  await this.processBlockRange(from, safeHead);
1115
- } catch {
938
+ } catch (err) {
939
+ console.error("[PAFI] BurnIndexer tick error:", err);
1116
940
  }
1117
941
  this.scheduleNext();
1118
942
  }
@@ -1157,10 +981,10 @@ var BurnIndexer = class {
1157
981
  for (const log of logs) {
1158
982
  const args = log.args;
1159
983
  if (!args.from || !args.to || args.value === void 0) continue;
1160
- if ((0, import_viem7.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
984
+ if ((0, import_viem5.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
1161
985
  if (log.blockNumber === null || log.transactionHash === null) continue;
1162
986
  out.push({
1163
- from: (0, import_viem7.getAddress)(args.from),
987
+ from: (0, import_viem5.getAddress)(args.from),
1164
988
  amount: args.value,
1165
989
  blockNumber: log.blockNumber,
1166
990
  txHash: log.transactionHash,
@@ -1175,21 +999,28 @@ var BurnIndexer = class {
1175
999
  * log + skip.
1176
1000
  */
1177
1001
  async finalize(evt) {
1002
+ const txHash = evt.txHash;
1178
1003
  const lockId = await this.matchLockId(evt);
1179
- if (!lockId) return;
1004
+ if (lockId === void 0) {
1005
+ console.warn(
1006
+ "[PAFI] BurnIndexer: matchLockId returned undefined for burn tx " + txHash + ". This burn will NOT be credited. Implement matchLockId to map burn events to lock IDs."
1007
+ );
1008
+ return;
1009
+ }
1180
1010
  if (!this.ledger.resolveCreditByBurnTx) {
1181
1011
  return;
1182
1012
  }
1183
1013
  try {
1184
1014
  await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
1185
- } catch {
1015
+ } catch (err) {
1016
+ console.error("[PAFI] BurnIndexer finalize error \u2014 credit may be lost:", err);
1186
1017
  }
1187
1018
  }
1188
1019
  };
1189
1020
 
1190
1021
  // src/api/handlers.ts
1191
- var import_viem8 = require("viem");
1192
- var import_core4 = require("@pafi-dev/core");
1022
+ var import_viem6 = require("viem");
1023
+ var import_core3 = require("@pafi-dev/core");
1193
1024
  var IssuerApiHandlers = class {
1194
1025
  authService;
1195
1026
  ledger;
@@ -1199,15 +1030,12 @@ var IssuerApiHandlers = class {
1199
1030
  * validate the request's `pointTokenAddress` against this set.
1200
1031
  */
1201
1032
  supportedTokens;
1202
- /** First supported token — used as default when a handler doesn't
1203
- * receive a `pointTokenAddress` in the request (shouldn't happen in
1204
- * practice, but keeps type-narrowing happy). */
1205
- defaultToken;
1206
1033
  chainId;
1207
1034
  contracts;
1208
1035
  pafiWebUrl;
1209
1036
  feeManager;
1210
1037
  poolsProvider;
1038
+ claim;
1211
1039
  constructor(config) {
1212
1040
  this.authService = config.authService;
1213
1041
  this.ledger = config.ledger;
@@ -1218,14 +1046,14 @@ var IssuerApiHandlers = class {
1218
1046
  "IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
1219
1047
  );
1220
1048
  }
1221
- const normalized = raw.map((a) => (0, import_viem8.getAddress)(a));
1049
+ const normalized = raw.map((a) => (0, import_viem6.getAddress)(a));
1222
1050
  this.supportedTokens = new Set(normalized);
1223
- this.defaultToken = normalized[0];
1224
1051
  this.chainId = config.chainId;
1225
1052
  this.contracts = config.contracts;
1226
1053
  if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
1227
1054
  if (config.feeManager) this.feeManager = config.feeManager;
1228
1055
  if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
1056
+ if (config.claim) this.claim = config.claim;
1229
1057
  }
1230
1058
  // =========================================================================
1231
1059
  // Public handlers (no auth required)
@@ -1240,6 +1068,12 @@ var IssuerApiHandlers = class {
1240
1068
  if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
1241
1069
  throw new Error("handleLogin: message and signature are required");
1242
1070
  }
1071
+ if (body.message.length > 4096) {
1072
+ throw new Error("message too long");
1073
+ }
1074
+ if (body.signature.length > 260) {
1075
+ throw new Error("signature too long");
1076
+ }
1243
1077
  const result = await this.authService.login(body.message, body.signature);
1244
1078
  return {
1245
1079
  token: result.token,
@@ -1254,9 +1088,12 @@ var IssuerApiHandlers = class {
1254
1088
  * needs to build EIP-712 messages and interact with on-chain.
1255
1089
  */
1256
1090
  async handleConfig(chainId) {
1091
+ if (!Number.isInteger(chainId) || chainId <= 0) {
1092
+ throw new Error("invalid chainId");
1093
+ }
1257
1094
  if (chainId !== this.chainId) {
1258
1095
  throw new Error(
1259
- `handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
1096
+ `handleConfig: unsupported chainId ${chainId}`
1260
1097
  );
1261
1098
  }
1262
1099
  const contracts = {
@@ -1319,25 +1156,25 @@ var IssuerApiHandlers = class {
1319
1156
  `handleUser: unsupported chainId ${request.chainId}`
1320
1157
  );
1321
1158
  }
1322
- const normalizedAuthed = (0, import_viem8.getAddress)(userAddress);
1323
- const normalizedRequest = (0, import_viem8.getAddress)(request.userAddress);
1159
+ const normalizedAuthed = (0, import_viem6.getAddress)(userAddress);
1160
+ const normalizedRequest = (0, import_viem6.getAddress)(request.userAddress);
1324
1161
  if (normalizedAuthed !== normalizedRequest) {
1325
1162
  throw new Error(
1326
1163
  "handleUser: request userAddress must match authenticated user"
1327
1164
  );
1328
1165
  }
1329
- const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
1166
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1330
1167
  if (!this.supportedTokens.has(pointToken)) {
1331
1168
  throw new Error(
1332
1169
  `handleUser: unsupported pointToken ${pointToken}`
1333
1170
  );
1334
1171
  }
1335
1172
  const [mintRequestNonce, receiverConsentNonce, offChainBalance, onChainBalance, minter] = await Promise.all([
1336
- (0, import_core4.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
1337
- (0, import_core4.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
1173
+ (0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
1174
+ (0, import_core3.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
1338
1175
  this.ledger.getBalance(normalizedAuthed, pointToken),
1339
- (0, import_core4.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
1340
- (0, import_core4.isMinter)(this.provider, pointToken, normalizedAuthed)
1176
+ (0, import_core3.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
1177
+ (0, import_core3.isMinter)(this.provider, pointToken, normalizedAuthed)
1341
1178
  ]);
1342
1179
  return {
1343
1180
  mintRequestNonce,
@@ -1369,19 +1206,32 @@ var IssuerApiHandlers = class {
1369
1206
  `handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
1370
1207
  );
1371
1208
  }
1372
- const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
1209
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1373
1210
  if (!this.supportedTokens.has(pointToken)) {
1374
1211
  throw new Error(
1375
1212
  `handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
1376
1213
  );
1377
1214
  }
1378
- const name = await (0, import_core4.getTokenName)(this.provider, pointToken);
1215
+ const consent = request.receiverConsent;
1216
+ if ((0, import_viem6.getAddress)(consent.originalReceiver) !== (0, import_viem6.getAddress)(userAddress)) {
1217
+ throw new Error(
1218
+ "handleBuildConsentTypedData: receiverConsent.originalReceiver must match authenticated user"
1219
+ );
1220
+ }
1221
+ if (consent.amount <= 0n) {
1222
+ throw new Error("handleBuildConsentTypedData: amount must be positive");
1223
+ }
1224
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
1225
+ if (consent.deadline <= nowSecs) {
1226
+ throw new Error("handleBuildConsentTypedData: deadline is in the past");
1227
+ }
1228
+ const name = await (0, import_core3.getTokenName)(this.provider, pointToken);
1379
1229
  const domain = {
1380
1230
  name,
1381
1231
  verifyingContract: pointToken,
1382
1232
  chainId: this.chainId
1383
1233
  };
1384
- const typedData = (0, import_core4.buildReceiverConsentTypedData)(domain, request.receiverConsent);
1234
+ const typedData = (0, import_core3.buildReceiverConsentTypedData)(domain, consent);
1385
1235
  return {
1386
1236
  typedData: {
1387
1237
  domain: typedData.domain,
@@ -1391,11 +1241,96 @@ var IssuerApiHandlers = class {
1391
1241
  }
1392
1242
  };
1393
1243
  }
1244
+ /**
1245
+ * `POST /claim`
1246
+ *
1247
+ * Policy gate + ledger lock + MintRequest signing in a single atomic
1248
+ * step. Returns an unsigned UserOp the frontend attaches paymaster data
1249
+ * to and submits via EIP-7702 + Bundler.
1250
+ *
1251
+ * Order of operations:
1252
+ * 1. Validate request fields.
1253
+ * 2. policy.evaluate() — throws if denied; cannot be bypassed.
1254
+ * 3. ledger.lockForMinting() — reserves the balance.
1255
+ * 4. Read on-chain mintRequestNonce + token name in parallel.
1256
+ * 5. relayService.prepareMint() — sign MintRequest + encode UserOp.
1257
+ * 6. On any error after step 3, release the lock before re-throwing.
1258
+ */
1259
+ async handleClaim(userAddress, request) {
1260
+ if (!this.claim) {
1261
+ throw new Error("handleClaim: claim is not configured on this issuer");
1262
+ }
1263
+ if (request.chainId !== this.chainId) {
1264
+ throw new Error(`handleClaim: unsupported chainId ${request.chainId}`);
1265
+ }
1266
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1267
+ if (!this.supportedTokens.has(pointToken)) {
1268
+ throw new Error(`handleClaim: unsupported pointToken ${pointToken}`);
1269
+ }
1270
+ if (request.amount <= 0n) {
1271
+ throw new Error("handleClaim: amount must be positive");
1272
+ }
1273
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
1274
+ if (request.deadline <= nowSecs) {
1275
+ throw new Error("handleClaim: deadline is in the past");
1276
+ }
1277
+ const { policy, relayService, issuerSignerWallet, batchExecutorAddress } = this.claim;
1278
+ const lockDurationMs = this.claim.lockDurationMs ?? 15 * 60 * 1e3;
1279
+ const normalizedUser = (0, import_viem6.getAddress)(userAddress);
1280
+ const decision = await policy.evaluate({
1281
+ userAddress: normalizedUser,
1282
+ amount: request.amount,
1283
+ pointTokenAddress: pointToken,
1284
+ chainId: this.chainId
1285
+ });
1286
+ if (!decision.approved) {
1287
+ throw new Error(`handleClaim: policy denied \u2014 ${decision.reason ?? "no reason given"}`);
1288
+ }
1289
+ const lockId = await this.ledger.lockForMinting(
1290
+ normalizedUser,
1291
+ request.amount,
1292
+ lockDurationMs,
1293
+ pointToken
1294
+ );
1295
+ try {
1296
+ const [mintRequestNonce, tokenName] = await Promise.all([
1297
+ (0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedUser),
1298
+ (0, import_core3.getTokenName)(this.provider, pointToken)
1299
+ ]);
1300
+ const domain = {
1301
+ name: tokenName,
1302
+ verifyingContract: pointToken,
1303
+ chainId: this.chainId
1304
+ };
1305
+ const userOp = await relayService.prepareMint({
1306
+ userAddress: normalizedUser,
1307
+ aaNonce: request.aaNonce,
1308
+ batchExecutorAddress,
1309
+ pointTokenAddress: pointToken,
1310
+ amount: request.amount,
1311
+ issuerSignerWallet,
1312
+ domain,
1313
+ mintRequestNonce,
1314
+ deadline: request.deadline,
1315
+ feeAmount: request.feeAmount,
1316
+ feeRecipient: request.feeRecipient
1317
+ });
1318
+ return {
1319
+ lockId,
1320
+ userOp,
1321
+ expiresInSeconds: Math.floor(lockDurationMs / 1e3)
1322
+ };
1323
+ } catch (err) {
1324
+ await this.ledger.releaseLock(lockId).catch(() => {
1325
+ });
1326
+ throw err;
1327
+ }
1328
+ }
1394
1329
  };
1395
1330
 
1396
1331
  // src/api/handlers/ptRedeemHandler.ts
1397
- var import_viem9 = require("viem");
1398
- var import_core5 = require("@pafi-dev/core");
1332
+ var import_viem7 = require("viem");
1333
+ var import_core4 = require("@pafi-dev/core");
1399
1334
  var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1400
1335
  var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
1401
1336
  var PTRedeemError = class extends Error {
@@ -1434,16 +1369,25 @@ var PTRedeemHandler = class {
1434
1369
  this.ledger = config.ledger;
1435
1370
  this.relayService = config.relayService;
1436
1371
  this.provider = config.provider;
1437
- this.pointTokenAddress = (0, import_viem9.getAddress)(config.pointTokenAddress);
1438
- this.batchExecutorAddress = (0, import_viem9.getAddress)(config.batchExecutorAddress);
1372
+ this.pointTokenAddress = (0, import_viem7.getAddress)(config.pointTokenAddress);
1373
+ this.batchExecutorAddress = (0, import_viem7.getAddress)(config.batchExecutorAddress);
1439
1374
  this.chainId = config.chainId;
1440
1375
  this.domain = config.domain;
1441
1376
  this.burnerSignerWallet = config.burnerSignerWallet;
1377
+ if (this.burnerSignerWallet?.account?.type === "local") {
1378
+ console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
1379
+ }
1442
1380
  this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1443
1381
  this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
1444
1382
  this.now = config.now ?? (() => Date.now());
1445
1383
  }
1446
1384
  async handle(request) {
1385
+ if ((0, import_viem7.getAddress)(request.authenticatedAddress) !== (0, import_viem7.getAddress)(request.userAddress)) {
1386
+ throw new PTRedeemError(
1387
+ "UNAUTHORIZED",
1388
+ `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
1389
+ );
1390
+ }
1447
1391
  if (request.amount <= 0n) {
1448
1392
  throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
1449
1393
  }
@@ -1451,7 +1395,7 @@ var PTRedeemHandler = class {
1451
1395
  try {
1452
1396
  burnNonce = await this.provider.readContract({
1453
1397
  address: this.pointTokenAddress,
1454
- abi: import_core5.POINT_TOKEN_V2_ABI,
1398
+ abi: import_core4.POINT_TOKEN_V2_ABI,
1455
1399
  functionName: "burnRequestNonces",
1456
1400
  args: [request.userAddress]
1457
1401
  });
@@ -1461,6 +1405,17 @@ var PTRedeemHandler = class {
1461
1405
  `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
1462
1406
  );
1463
1407
  }
1408
+ const onChainBalance = await (0, import_core4.getPointTokenBalance)(
1409
+ this.provider,
1410
+ this.pointTokenAddress,
1411
+ request.userAddress
1412
+ );
1413
+ if (onChainBalance < request.amount) {
1414
+ throw new PTRedeemError(
1415
+ "INVALID_AMOUNT",
1416
+ `insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
1417
+ );
1418
+ }
1464
1419
  const deadline = BigInt(
1465
1420
  Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1466
1421
  );
@@ -1477,7 +1432,7 @@ var PTRedeemHandler = class {
1477
1432
  };
1478
1433
  let burnerSignature;
1479
1434
  try {
1480
- const sig = await (0, import_core5.signBurnRequest)(
1435
+ const sig = await (0, import_core4.signBurnRequest)(
1481
1436
  this.burnerSignerWallet,
1482
1437
  domain,
1483
1438
  burnRequest
@@ -1514,8 +1469,8 @@ var PTRedeemHandler = class {
1514
1469
  };
1515
1470
 
1516
1471
  // src/api/handlers/topUpRedemptionHandler.ts
1517
- var import_viem10 = require("viem");
1518
- var import_core6 = require("@pafi-dev/core");
1472
+ var import_viem8 = require("viem");
1473
+ var import_core5 = require("@pafi-dev/core");
1519
1474
  var TopUpRedemptionError = class extends Error {
1520
1475
  constructor(code, message) {
1521
1476
  super(message);
@@ -1533,9 +1488,15 @@ var TopUpRedemptionHandler = class {
1533
1488
  this.ledger = config.ledger;
1534
1489
  this.ptRedeemHandler = config.ptRedeemHandler;
1535
1490
  this.provider = config.provider;
1536
- this.pointTokenAddress = (0, import_viem10.getAddress)(config.pointTokenAddress);
1491
+ this.pointTokenAddress = (0, import_viem8.getAddress)(config.pointTokenAddress);
1537
1492
  }
1538
1493
  async handle(request) {
1494
+ if ((0, import_viem8.getAddress)(request.authenticatedAddress) !== (0, import_viem8.getAddress)(request.userAddress)) {
1495
+ throw new TopUpRedemptionError(
1496
+ "UNAUTHORIZED",
1497
+ `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
1498
+ );
1499
+ }
1539
1500
  const offChainBalance = await this.ledger.getBalance(
1540
1501
  request.userAddress,
1541
1502
  this.pointTokenAddress
@@ -1544,7 +1505,7 @@ var TopUpRedemptionHandler = class {
1544
1505
  return { action: "NO_TOP_UP_NEEDED", offChainBalance };
1545
1506
  }
1546
1507
  const shortfall = request.requiredAmount - offChainBalance;
1547
- const onChainBalance = await (0, import_core6.getPointTokenBalance)(
1508
+ const onChainBalance = await (0, import_core5.getPointTokenBalance)(
1548
1509
  this.provider,
1549
1510
  this.pointTokenAddress,
1550
1511
  request.userAddress
@@ -1558,6 +1519,7 @@ var TopUpRedemptionHandler = class {
1558
1519
  };
1559
1520
  }
1560
1521
  const redeem = await this.ptRedeemHandler.handle({
1522
+ authenticatedAddress: request.authenticatedAddress,
1561
1523
  userAddress: request.userAddress,
1562
1524
  amount: shortfall,
1563
1525
  aaNonce: request.aaNonce
@@ -1571,6 +1533,7 @@ var TopUpRedemptionHandler = class {
1571
1533
  };
1572
1534
 
1573
1535
  // src/pools/subgraphPoolsProvider.ts
1536
+ var import_viem9 = require("viem");
1574
1537
  var DEFAULT_CACHE_TTL_MS = 3e4;
1575
1538
  var POOL_QUERY = `
1576
1539
  query GetPoolForPointToken($id: ID!) {
@@ -1593,6 +1556,19 @@ function createSubgraphPoolsProvider(config) {
1593
1556
  "createSubgraphPoolsProvider: subgraphUrl is required"
1594
1557
  );
1595
1558
  }
1559
+ try {
1560
+ const parsed = new URL(config.subgraphUrl);
1561
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
1562
+ throw new Error("subgraphUrl must use HTTPS in production");
1563
+ }
1564
+ } catch (err) {
1565
+ if (err instanceof TypeError) {
1566
+ throw new Error(
1567
+ `subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
1568
+ );
1569
+ }
1570
+ throw err;
1571
+ }
1596
1572
  const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
1597
1573
  const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1598
1574
  const now = config.now ?? (() => Date.now());
@@ -1661,6 +1637,26 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress)
1661
1637
  return [];
1662
1638
  }
1663
1639
  const { pool } = token;
1640
+ if (!(0, import_viem9.isAddress)(pool.hooks)) {
1641
+ console.error(
1642
+ "[PAFI] SubgraphPoolsProvider: invalid hooks address in response:",
1643
+ pool.hooks,
1644
+ "\u2014 skipping pool"
1645
+ );
1646
+ return [];
1647
+ }
1648
+ if (!(0, import_viem9.isAddress)(pool.token0.id) || !(0, import_viem9.isAddress)(pool.token1.id)) {
1649
+ console.error(
1650
+ "[PAFI] SubgraphPoolsProvider: invalid token address in response \u2014 skipping pool"
1651
+ );
1652
+ return [];
1653
+ }
1654
+ if (!Number.isFinite(Number(pool.feeTier)) || !Number.isFinite(Number(pool.tickSpacing))) {
1655
+ console.error(
1656
+ "[PAFI] SubgraphPoolsProvider: invalid feeTier/tickSpacing \u2014 skipping pool"
1657
+ );
1658
+ return [];
1659
+ }
1664
1660
  const [currency0, currency1] = sortCurrencies(
1665
1661
  pool.token0.id,
1666
1662
  pool.token1.id
@@ -1696,6 +1692,19 @@ function createSubgraphNativeUsdtQuoter(config) {
1696
1692
  "createSubgraphNativeUsdtQuoter: subgraphUrl is required"
1697
1693
  );
1698
1694
  }
1695
+ try {
1696
+ const parsed = new URL(config.subgraphUrl);
1697
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
1698
+ throw new Error("subgraphUrl must use HTTPS in production");
1699
+ }
1700
+ } catch (err) {
1701
+ if (err instanceof TypeError) {
1702
+ throw new Error(
1703
+ `subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
1704
+ );
1705
+ }
1706
+ throw err;
1707
+ }
1699
1708
  const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
1700
1709
  const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
1701
1710
  const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
@@ -1772,6 +1781,14 @@ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
1772
1781
  );
1773
1782
  return null;
1774
1783
  }
1784
+ const MIN_REASONABLE_ETH_PRICE = 100;
1785
+ const MAX_REASONABLE_ETH_PRICE = 1e5;
1786
+ if (parsed < MIN_REASONABLE_ETH_PRICE || parsed > MAX_REASONABLE_ETH_PRICE) {
1787
+ console.warn(
1788
+ `[PAFI] SubgraphNativeUsdtQuoter: ETH/USD price ${parsed} is outside reasonable range. Using fallback.`
1789
+ );
1790
+ return null;
1791
+ }
1775
1792
  return parsed;
1776
1793
  }
1777
1794
  function toUsdtPerNative(priceFloat, usdtDecimals) {
@@ -1782,7 +1799,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
1782
1799
  }
1783
1800
 
1784
1801
  // src/balance/balanceAggregator.ts
1785
- var import_core7 = require("@pafi-dev/core");
1802
+ var import_core6 = require("@pafi-dev/core");
1786
1803
  var BalanceAggregator = class {
1787
1804
  provider;
1788
1805
  ledger;
@@ -1803,7 +1820,7 @@ var BalanceAggregator = class {
1803
1820
  async getCombinedBalance(user, pointToken) {
1804
1821
  const [offChain, onChain] = await Promise.all([
1805
1822
  this.ledger.getBalance(user, pointToken),
1806
- (0, import_core7.getPointTokenBalance)(this.provider, pointToken, user)
1823
+ (0, import_core6.getPointTokenBalance)(this.provider, pointToken, user)
1807
1824
  ]);
1808
1825
  return {
1809
1826
  offChain,
@@ -1862,12 +1879,104 @@ var PafiBackendError = class extends Error {
1862
1879
  }
1863
1880
  };
1864
1881
 
1882
+ // src/pafi-backend/client.ts
1883
+ function serializeBigInt(_key, value) {
1884
+ return typeof value === "bigint" ? value.toString(10) : value;
1885
+ }
1886
+ function sleep(ms) {
1887
+ return new Promise((resolve) => setTimeout(resolve, ms));
1888
+ }
1889
+ var PafiBackendClient = class {
1890
+ config;
1891
+ constructor(config) {
1892
+ if (!config.url) throw new Error("PafiBackendClient: url is required");
1893
+ if (!config.issuerId) throw new Error("PafiBackendClient: issuerId is required");
1894
+ this.config = config;
1895
+ }
1896
+ async requestSponsorship(request) {
1897
+ const maxAttempts = this.config.retry?.maxAttempts ?? 1;
1898
+ const initialDelayMs = this.config.retry?.initialDelayMs ?? 100;
1899
+ const maxDelayMs = this.config.retry?.maxDelayMs ?? 1e4;
1900
+ const maxRetryAfterMs = this.config.retry?.maxRetryAfterMs;
1901
+ let lastError;
1902
+ let delay = initialDelayMs;
1903
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1904
+ try {
1905
+ return await this._doRequest(request);
1906
+ } catch (err) {
1907
+ if (!(err instanceof PafiBackendError)) throw err;
1908
+ lastError = err;
1909
+ if (attempt >= maxAttempts) break;
1910
+ if (!err.safeToRetry) break;
1911
+ const retryAfterMs = err.retryAfter !== void 0 ? err.retryAfter * 1e3 : void 0;
1912
+ if (maxRetryAfterMs !== void 0 && retryAfterMs !== void 0 && retryAfterMs > maxRetryAfterMs) {
1913
+ break;
1914
+ }
1915
+ await sleep(retryAfterMs ?? delay);
1916
+ delay = Math.min(delay * 2, maxDelayMs);
1917
+ }
1918
+ }
1919
+ throw lastError;
1920
+ }
1921
+ async _doRequest(request) {
1922
+ const fetchFn = this.config.fetchImpl ?? fetch;
1923
+ const url = `${this.config.url}/paymaster/sponsor`;
1924
+ const body = JSON.stringify(request, serializeBigInt);
1925
+ let response;
1926
+ try {
1927
+ response = await fetchFn(url, {
1928
+ method: "POST",
1929
+ headers: {
1930
+ "Content-Type": "application/json",
1931
+ Authorization: `Bearer ${this.config.apiKey}`,
1932
+ "X-Issuer-Id": this.config.issuerId
1933
+ },
1934
+ body
1935
+ });
1936
+ } catch (err) {
1937
+ throw new PafiBackendError(
1938
+ "NETWORK_ERROR",
1939
+ `Network error: ${err instanceof Error ? err.message : String(err)}`,
1940
+ 0
1941
+ );
1942
+ }
1943
+ const text = await response.text();
1944
+ let json = {};
1945
+ try {
1946
+ json = JSON.parse(text);
1947
+ } catch {
1948
+ }
1949
+ if (!response.ok) {
1950
+ const code = json.code ?? "INTERNAL_ERROR";
1951
+ const message = json.message ?? `HTTP ${response.status}`;
1952
+ const retryAfter = typeof json.retryAfter === "number" ? json.retryAfter : void 0;
1953
+ const safeToRetry = typeof json.safeToRetry === "boolean" ? json.safeToRetry : void 0;
1954
+ throw new PafiBackendError(code, message, response.status, json, {
1955
+ retryAfter,
1956
+ safeToRetry
1957
+ });
1958
+ }
1959
+ return {
1960
+ paymaster: json.paymaster,
1961
+ paymasterData: json.paymasterData,
1962
+ paymasterVerificationGasLimit: BigInt(
1963
+ json.paymasterVerificationGasLimit
1964
+ ),
1965
+ paymasterPostOpGasLimit: BigInt(json.paymasterPostOpGasLimit),
1966
+ expiresAt: json.expiresAt
1967
+ };
1968
+ }
1969
+ };
1970
+
1865
1971
  // src/config.ts
1866
- var import_viem11 = require("viem");
1972
+ var import_viem10 = require("viem");
1867
1973
  function createIssuerService(config) {
1868
1974
  if (!config.provider) {
1869
1975
  throw new Error("createIssuerService: provider is required");
1870
1976
  }
1977
+ if (!config.ledger) {
1978
+ throw new Error("createIssuerService: ledger is required");
1979
+ }
1871
1980
  if (!config.auth?.jwtSecret) {
1872
1981
  throw new Error("createIssuerService: auth.jwtSecret is required");
1873
1982
  }
@@ -1880,8 +1989,8 @@ function createIssuerService(config) {
1880
1989
  "createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
1881
1990
  );
1882
1991
  }
1883
- const tokenAddresses = rawAddresses.map((a) => (0, import_viem11.getAddress)(a));
1884
- const ledger = config.ledger ?? new MemoryPointLedger();
1992
+ const tokenAddresses = rawAddresses.map((a) => (0, import_viem10.getAddress)(a));
1993
+ const ledger = config.ledger;
1885
1994
  const sessionStore = config.sessionStore ?? new MemorySessionStore();
1886
1995
  const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
1887
1996
  const authServiceConfig = {
@@ -1937,6 +2046,15 @@ function createIssuerService(config) {
1937
2046
  };
1938
2047
  if (feeManager) handlersConfig.feeManager = feeManager;
1939
2048
  if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
2049
+ if (config.claim) {
2050
+ handlersConfig.claim = {
2051
+ policy,
2052
+ relayService,
2053
+ issuerSignerWallet: config.claim.issuerSignerWallet,
2054
+ batchExecutorAddress: config.claim.batchExecutorAddress,
2055
+ lockDurationMs: config.claim.lockDurationMs
2056
+ };
2057
+ }
1940
2058
  const handlers = new IssuerApiHandlers(handlersConfig);
1941
2059
  if (config.indexer?.autoStart) {
1942
2060
  for (const idx of indexers.values()) {
@@ -1968,15 +2086,14 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
1968
2086
  FeeManager,
1969
2087
  InMemoryCursorStore,
1970
2088
  IssuerApiHandlers,
1971
- MemoryPointLedger,
1972
2089
  MemorySessionStore,
1973
2090
  NonceManager,
1974
2091
  PAFI_ISSUER_SDK_VERSION,
1975
2092
  PTRedeemError,
1976
2093
  PTRedeemHandler,
2094
+ PafiBackendClient,
1977
2095
  PafiBackendError,
1978
2096
  PointIndexer,
1979
- PrivateKeySigner,
1980
2097
  RelayError,
1981
2098
  RelayService,
1982
2099
  TopUpRedemptionError,