@pafi-dev/issuer 0.3.0-beta.1 → 0.3.0-beta.11

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
@@ -1,201 +1,3 @@
1
- // src/ledger/memoryLedger.ts
2
- import { getAddress } from "viem";
3
- var MemoryPointLedger = class {
4
- balances = /* @__PURE__ */ new Map();
5
- locks = /* @__PURE__ */ new Map();
6
- nextLockId = 1;
7
- now;
8
- constructor(opts = {}) {
9
- this.now = opts.now ?? (() => Date.now());
10
- }
11
- // -------------------------------------------------------------------------
12
- // Read
13
- // -------------------------------------------------------------------------
14
- async getBalance(userAddress, tokenAddress) {
15
- const user = getAddress(userAddress);
16
- const token = normalizeToken(tokenAddress);
17
- this.purgeExpired();
18
- const total = this.balances.get(balanceKey(user, token)) ?? 0n;
19
- const locked = this.lockedTotalFor(user, token);
20
- return total - locked;
21
- }
22
- async getLockedRequests(userAddress, tokenAddress) {
23
- const user = getAddress(userAddress);
24
- const token = normalizeToken(tokenAddress);
25
- this.purgeExpired();
26
- const out = [];
27
- for (const lock of this.locks.values()) {
28
- if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
29
- out.push({ ...lock });
30
- }
31
- }
32
- return out;
33
- }
34
- // -------------------------------------------------------------------------
35
- // Write
36
- // -------------------------------------------------------------------------
37
- async creditBalance(userAddress, amount, _reason, tokenAddress) {
38
- if (amount <= 0n) {
39
- throw new Error("MemoryPointLedger: credit amount must be positive");
40
- }
41
- const user = getAddress(userAddress);
42
- const token = normalizeToken(tokenAddress);
43
- const key = balanceKey(user, token);
44
- const current = this.balances.get(key) ?? 0n;
45
- this.balances.set(key, current + amount);
46
- }
47
- async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
48
- if (amount <= 0n) {
49
- throw new Error("MemoryPointLedger: lock amount must be positive");
50
- }
51
- if (lockDurationMs <= 0) {
52
- throw new Error("MemoryPointLedger: lockDurationMs must be positive");
53
- }
54
- const user = getAddress(userAddress);
55
- const token = normalizeToken(tokenAddress);
56
- this.purgeExpired();
57
- const total = this.balances.get(balanceKey(user, token)) ?? 0n;
58
- const alreadyLocked = this.lockedTotalFor(user, token);
59
- const available = total - alreadyLocked;
60
- if (available < amount) {
61
- throw new Error(
62
- `MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
63
- );
64
- }
65
- const lockId = `lock-${this.nextLockId++}`;
66
- const now = this.now();
67
- const lock = {
68
- lockId,
69
- userAddress: user,
70
- amount,
71
- status: "PENDING",
72
- createdAt: now,
73
- expiresAt: now + lockDurationMs
74
- };
75
- if (tokenAddress !== void 0) {
76
- lock.tokenAddress = getAddress(tokenAddress);
77
- }
78
- this.locks.set(lockId, lock);
79
- return lockId;
80
- }
81
- async releaseLock(lockId) {
82
- const lock = this.locks.get(lockId);
83
- if (!lock) return;
84
- if (lock.status === "PENDING") {
85
- this.locks.delete(lockId);
86
- }
87
- }
88
- async deductBalance(userAddress, amount, txHash, tokenAddress) {
89
- if (amount <= 0n) {
90
- throw new Error("MemoryPointLedger: deduct amount must be positive");
91
- }
92
- const user = getAddress(userAddress);
93
- const token = normalizeToken(tokenAddress);
94
- const key = balanceKey(user, token);
95
- const current = this.balances.get(key) ?? 0n;
96
- if (current < amount) {
97
- throw new Error(
98
- `MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
99
- );
100
- }
101
- this.balances.set(key, current - amount);
102
- for (const lock of this.locks.values()) {
103
- if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
104
- lock.status = "MINTED";
105
- lock.txHash = txHash;
106
- return;
107
- }
108
- }
109
- }
110
- async updateMintStatus(lockId, status, txHash) {
111
- const lock = this.locks.get(lockId);
112
- if (!lock) {
113
- throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
114
- }
115
- lock.status = status;
116
- if (txHash) lock.txHash = txHash;
117
- }
118
- // -------------------------------------------------------------------------
119
- // v1.4 — Reverse flow (PT burn → off-chain credit)
120
- // -------------------------------------------------------------------------
121
- pendingCredits = /* @__PURE__ */ new Map();
122
- nextCreditId = 1;
123
- async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
124
- if (amount <= 0n) {
125
- throw new Error(
126
- "MemoryPointLedger: pending credit amount must be positive"
127
- );
128
- }
129
- if (durationMs <= 0) {
130
- throw new Error("MemoryPointLedger: durationMs must be positive");
131
- }
132
- const user = getAddress(userAddress);
133
- const lockId = `credit-${this.nextCreditId++}`;
134
- const now = this.now();
135
- this.pendingCredits.set(lockId, {
136
- lockId,
137
- userAddress: user,
138
- amount,
139
- tokenAddress: tokenAddress !== void 0 ? getAddress(tokenAddress) : void 0,
140
- createdAt: now,
141
- expiresAt: now + durationMs,
142
- status: "PENDING"
143
- });
144
- return lockId;
145
- }
146
- async resolveCreditByBurnTx(lockId, txHash) {
147
- const credit = this.pendingCredits.get(lockId);
148
- if (!credit) {
149
- throw new Error(
150
- `MemoryPointLedger: unknown pending credit lockId ${lockId}`
151
- );
152
- }
153
- if (credit.status === "RESOLVED") {
154
- if (credit.txHash === txHash) return;
155
- throw new Error(
156
- `MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
157
- );
158
- }
159
- const token = normalizeToken(credit.tokenAddress);
160
- const key = balanceKey(credit.userAddress, token);
161
- const current = this.balances.get(key) ?? 0n;
162
- this.balances.set(key, current + credit.amount);
163
- credit.status = "RESOLVED";
164
- credit.txHash = txHash;
165
- }
166
- // -------------------------------------------------------------------------
167
- // Internal helpers
168
- // -------------------------------------------------------------------------
169
- /**
170
- * Auto-expire any PENDING lock past its expiry. Called lazily on every
171
- * read/write so the in-memory state stays self-cleaning without a timer.
172
- */
173
- purgeExpired() {
174
- const now = this.now();
175
- for (const lock of this.locks.values()) {
176
- if (lock.status === "PENDING" && lock.expiresAt <= now) {
177
- lock.status = "EXPIRED";
178
- }
179
- }
180
- }
181
- lockedTotalFor(userAddress, tokenKey) {
182
- let total = 0n;
183
- for (const lock of this.locks.values()) {
184
- if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
185
- total += lock.amount;
186
- }
187
- }
188
- return total;
189
- }
190
- };
191
- var DEFAULT_TOKEN_KEY = "default";
192
- function normalizeToken(tokenAddress) {
193
- return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : getAddress(tokenAddress);
194
- }
195
- function balanceKey(user, tokenKey) {
196
- return `${user}|${tokenKey}`;
197
- }
198
-
199
1
  // src/policy/defaultPolicy.ts
200
2
  var DefaultPolicyEngine = class {
201
3
  ledger;
@@ -211,6 +13,13 @@ var DefaultPolicyEngine = class {
211
13
  }
212
14
  if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
213
15
  if (opts.resolveIssuer) this.resolveIssuer = opts.resolveIssuer;
16
+ if (!opts.mintingOracleAddress || !opts.provider || !opts.verifyMintCap || !opts.resolveIssuer) {
17
+ if (process.env.NODE_ENV === "production") {
18
+ throw new Error(
19
+ "[PAFI] DefaultPolicyEngine: on-chain MintingOracle cap check is required in production. Configure mintingOracleAddress, provider, verifyMintCap, and resolveIssuer."
20
+ );
21
+ }
22
+ }
214
23
  }
215
24
  async evaluate(request) {
216
25
  if (request.amount <= 0n) {
@@ -247,34 +56,9 @@ var DefaultPolicyEngine = class {
247
56
  }
248
57
  };
249
58
 
250
- // src/signer/privateKeySigner.ts
251
- import { createWalletClient, http } from "viem";
252
- import { privateKeyToAccount } from "viem/accounts";
253
- import {
254
- signMintRequest
255
- } from "@pafi-dev/core";
256
- var PrivateKeySigner = class {
257
- account;
258
- walletClient;
259
- constructor(opts) {
260
- this.account = privateKeyToAccount(opts.privateKey);
261
- this.walletClient = createWalletClient({
262
- account: this.account,
263
- chain: opts.chain,
264
- transport: http(opts.rpcUrl)
265
- });
266
- }
267
- async signMintRequest(domain, message) {
268
- return signMintRequest(this.walletClient, domain, message);
269
- }
270
- async getAddress() {
271
- return this.account.address;
272
- }
273
- };
274
-
275
59
  // src/auth/memorySessionStore.ts
276
60
  import { randomBytes } from "crypto";
277
- import { getAddress as getAddress2 } from "viem";
61
+ import { getAddress } from "viem";
278
62
  var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
279
63
  var MemorySessionStore = class {
280
64
  nonces = /* @__PURE__ */ new Map();
@@ -284,6 +68,11 @@ var MemorySessionStore = class {
284
68
  nonceTtlMs;
285
69
  now;
286
70
  constructor(opts = {}) {
71
+ if (process.env.NODE_ENV === "production") {
72
+ console.error(
73
+ "[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."
74
+ );
75
+ }
287
76
  this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
288
77
  this.now = opts.now ?? (() => Date.now());
289
78
  }
@@ -310,7 +99,7 @@ var MemorySessionStore = class {
310
99
  this.purgeExpiredSessions();
311
100
  const normalized = {
312
101
  ...session,
313
- userAddress: getAddress2(session.userAddress)
102
+ userAddress: getAddress(session.userAddress)
314
103
  };
315
104
  this.sessions.set(session.tokenId, normalized);
316
105
  }
@@ -328,7 +117,7 @@ var MemorySessionStore = class {
328
117
  this.sessions.delete(tokenId);
329
118
  }
330
119
  async revokeAllSessions(userAddress) {
331
- const key = getAddress2(userAddress);
120
+ const key = getAddress(userAddress);
332
121
  for (const [tokenId, session] of this.sessions.entries()) {
333
122
  if (session.userAddress === key) {
334
123
  this.sessions.delete(tokenId);
@@ -374,7 +163,7 @@ var NonceManager = class {
374
163
  // src/auth/loginVerifier.ts
375
164
  import { randomBytes as randomBytes2 } from "crypto";
376
165
  import { SignJWT, jwtVerify, errors as joseErrors } from "jose";
377
- import { getAddress as getAddress3 } from "viem";
166
+ import { getAddress as getAddress2 } from "viem";
378
167
  import { parseLoginMessage, verifyLoginMessage } from "@pafi-dev/core";
379
168
 
380
169
  // src/auth/errors.ts
@@ -398,8 +187,8 @@ var AuthService = class {
398
187
  nonceManager;
399
188
  now;
400
189
  constructor(config) {
401
- if (!config.jwtSecret || config.jwtSecret.length < 16) {
402
- throw new Error("AuthService: jwtSecret must be at least 16 chars");
190
+ if (!config.jwtSecret || config.jwtSecret.length < 32) {
191
+ throw new Error("AuthService: jwtSecret must be at least 32 characters for HS256 security");
403
192
  }
404
193
  this.sessionStore = config.sessionStore;
405
194
  this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
@@ -428,6 +217,12 @@ var AuthService = class {
428
217
  const msg = err instanceof Error ? err.message : String(err);
429
218
  throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
430
219
  }
220
+ if (parsed.expirationTime == null) {
221
+ throw new AuthError(
222
+ "INVALID_MESSAGE",
223
+ "login message must include expirationTime"
224
+ );
225
+ }
431
226
  if (parsed.domain !== this.domain) {
432
227
  throw new AuthError(
433
228
  "DOMAIN_MISMATCH",
@@ -464,7 +259,7 @@ var AuthService = class {
464
259
  "Nonce is unknown, expired, or already used"
465
260
  );
466
261
  }
467
- const userAddress = getAddress3(verifyResult.address);
262
+ const userAddress = getAddress2(verifyResult.address);
468
263
  const tokenId = randomBytes2(16).toString("hex");
469
264
  const issuedAt = now;
470
265
  const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
@@ -492,7 +287,11 @@ var AuthService = class {
492
287
  if (payload.jti) {
493
288
  await this.sessionStore.revokeSession(payload.jti);
494
289
  }
495
- } catch {
290
+ } catch (err) {
291
+ const msg = err instanceof Error ? err.message : String(err);
292
+ if (!msg.includes("not found") && !msg.includes("expired")) {
293
+ console.error("[PAFI] AuthService logout: session store error", err);
294
+ }
496
295
  }
497
296
  }
498
297
  /**
@@ -531,7 +330,7 @@ var AuthService = class {
531
330
  throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
532
331
  }
533
332
  return {
534
- userAddress: getAddress3(userAddress),
333
+ userAddress: getAddress2(userAddress),
535
334
  chainId,
536
335
  tokenId
537
336
  };
@@ -582,209 +381,120 @@ var RelayError = class extends Error {
582
381
  };
583
382
 
584
383
  // src/relay/relayService.ts
585
- import { encodeFunctionData } from "viem";
586
384
  import {
587
- relayAbi,
588
- encodeMintAndSwap,
589
- simulateMintAndSwap as coreSimulateMintAndSwap,
590
- SimulationError,
591
- RELAYER_V2_ABI,
385
+ encodeFunctionData,
386
+ erc20Abi
387
+ } from "viem";
388
+ import {
592
389
  POINT_TOKEN_V2_ABI,
593
- buildPartialUserOperation
390
+ buildPartialUserOperation,
391
+ signMintRequest
594
392
  } from "@pafi-dev/core";
595
- var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
596
393
  var RelayService = class {
597
- relayAddress;
598
- operatorWallet;
599
- provider;
600
- confirmationTimeoutMs;
601
- simulateBeforeSubmit;
602
- constructor(config) {
603
- if (!config.relayAddress) {
604
- throw new Error("RelayService: relayAddress is required");
605
- }
606
- if (!config.operatorWallet) {
607
- throw new Error("RelayService: operatorWallet is required");
608
- }
609
- this.relayAddress = config.relayAddress;
610
- this.operatorWallet = config.operatorWallet;
611
- if (config.provider) this.provider = config.provider;
612
- this.confirmationTimeoutMs = config.confirmationTimeoutMs ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
613
- this.simulateBeforeSubmit = config.simulateBeforeSubmit ?? config.provider !== void 0;
614
- }
615
- /** Address the operator wallet is broadcasting from (for logging). */
616
- operatorAddress() {
617
- return this.operatorWallet.account?.address;
618
- }
619
394
  /**
620
- * Build calldata for the Relay `mintAndSwap` function. Kept public so
621
- * callers (e.g. the MintingGateway) can log or persist the encoded call
622
- * for audit before broadcasting.
395
+ * Build an unsigned UserOp for Scenario 1 (Mint) sig-gated
396
+ * `PointToken.mint(to, amount, deadline, minterSig)`.
623
397
  */
624
- encodeCall(params) {
625
- try {
626
- return encodeMintAndSwap(params.mint, params.swap);
627
- } catch (err) {
398
+ async prepareMint(params) {
399
+ if (!params.batchExecutorAddress) {
628
400
  throw new RelayError(
629
401
  "ENCODE_FAILED",
630
- `Failed to encode mintAndSwap calldata: ${errorMessage(err)}`,
631
- err
402
+ "prepareMint: batchExecutorAddress required"
632
403
  );
633
404
  }
634
- }
635
- /**
636
- * Submit a `mintAndSwap` transaction. Flow:
637
- *
638
- * 1. (optional) pre-flight simulate via provider
639
- * 2. writeContract through the operator wallet
640
- * 3. (optional) wait for the receipt and surface gasUsed / status
641
- *
642
- * Throws a typed `RelayError` on any failure so the MintingGateway can
643
- * decide whether to release the ledger lock (`SUBMIT_FAILED` and
644
- * `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
645
- * need manual review because the tx may still land).
646
- *
647
- * @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
648
- * `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
649
- * still needs to finalize Relayer v2 ABI before the replacements
650
- * can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
651
- */
652
- async submitMintAndSwap(params) {
653
- if (this.simulateBeforeSubmit && this.provider) {
654
- const operatorAddr = this.operatorWallet.account?.address;
655
- if (operatorAddr) {
656
- try {
657
- await coreSimulateMintAndSwap(
658
- this.provider,
659
- this.relayAddress,
660
- params.mint,
661
- params.swap,
662
- operatorAddr
663
- );
664
- } catch (err) {
665
- const reason = err instanceof SimulationError ? err.reason : errorMessage(err);
666
- throw new RelayError(
667
- "SIMULATION_FAILED",
668
- `mintAndSwap would revert: ${reason}`,
669
- err
670
- );
671
- }
672
- }
405
+ if (!params.userAddress) {
406
+ throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
673
407
  }
674
- let txHash;
675
- try {
676
- txHash = await this.operatorWallet.writeContract({
677
- address: this.relayAddress,
678
- abi: relayAbi,
679
- functionName: "mintAndSwap",
680
- args: [params.mint, params.swap],
681
- ...this.operatorWallet.account ? { account: this.operatorWallet.account } : {}
682
- });
683
- } catch (err) {
408
+ if (!params.pointTokenAddress) {
684
409
  throw new RelayError(
685
- "SUBMIT_FAILED",
686
- `Failed to broadcast mintAndSwap: ${errorMessage(err)}`,
687
- err
410
+ "ENCODE_FAILED",
411
+ "prepareMint: pointTokenAddress required"
688
412
  );
689
413
  }
690
- if (!this.provider) {
691
- return { txHash };
414
+ if (params.amount <= 0n) {
415
+ throw new RelayError("ENCODE_FAILED", "prepareMint: amount must be positive");
692
416
  }
693
- try {
694
- const receipt = await this.provider.waitForTransactionReceipt({
695
- hash: txHash,
696
- timeout: this.confirmationTimeoutMs
697
- });
698
- if (receipt.status !== "success") {
699
- throw new RelayError(
700
- "TX_REVERTED",
701
- `mintAndSwap reverted on-chain (tx=${txHash})`
702
- );
703
- }
704
- return {
705
- txHash,
706
- blockNumber: receipt.blockNumber,
707
- gasUsed: receipt.gasUsed,
708
- status: receipt.status
709
- };
710
- } catch (err) {
711
- if (err instanceof RelayError) throw err;
417
+ if (!params.issuerSignerWallet) {
712
418
  throw new RelayError(
713
- "TIMEOUT",
714
- `Timed out waiting for mintAndSwap receipt (tx=${txHash}): ${errorMessage(err)}`,
715
- err
419
+ "ENCODE_FAILED",
420
+ "prepareMint: issuerSignerWallet required (for MintRequest EIP-712 signature)"
716
421
  );
717
422
  }
718
- }
719
- // ==========================================================================
720
- // v1.4 — Sponsored UserOp preparation (beta with mocked SC contracts)
721
- // ==========================================================================
722
- //
723
- // These two methods build unsigned `PartialUserOperation` payloads for
724
- // the Frontend to sign (via Privy) and submit to the Bundler. The
725
- // Issuer Backend no longer broadcasts — that's the Frontend's job.
726
- //
727
- // Uses mocked Relayer v2 + PointToken ABIs from `@pafi-dev/core/contracts`.
728
- // When SC delivers real ABIs, the imports swap but these method bodies
729
- // stay the same (calldata encoder is ABI-driven).
730
- // ==========================================================================
731
- /**
732
- * Build an unsigned UserOp for Scenario 1 (Mint).
733
- *
734
- * Flow:
735
- * 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
736
- * 2. Optionally append a PT fee transfer from user → feeRecipient
737
- * (fee recovery happens on-chain via BatchExecutor, not via an
738
- * operator wallet)
739
- * 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
740
- * 4. Return a `PartialUserOperation` ready for:
741
- * - gas estimation (Bundler)
742
- * - paymaster sponsorship (PAFI Backend)
743
- * - user signature (Privy)
744
- */
745
- prepareMint(params) {
746
- if (!params.relayerAddress) {
747
- throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
423
+ if (params.deadline <= 0n) {
424
+ throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
748
425
  }
749
- if (!params.batchExecutorAddress) {
426
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
427
+ if (params.deadline <= nowSecs) {
428
+ throw new RelayError("ENCODE_FAILED", "prepareMint: deadline is in the past");
429
+ }
430
+ const MAX_DEADLINE_WINDOW = 3600n;
431
+ if (params.deadline > nowSecs + MAX_DEADLINE_WINDOW) {
750
432
  throw new RelayError(
751
433
  "ENCODE_FAILED",
752
- "prepareMint: batchExecutorAddress required"
434
+ "prepareMint: deadline exceeds maximum allowed window (1 hour)"
753
435
  );
754
436
  }
755
- if (!params.userAddress) {
756
- throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
437
+ let minterSig;
438
+ try {
439
+ const sig = await signMintRequest(
440
+ params.issuerSignerWallet,
441
+ params.domain,
442
+ {
443
+ to: params.userAddress,
444
+ amount: params.amount,
445
+ nonce: params.mintRequestNonce,
446
+ deadline: params.deadline
447
+ }
448
+ );
449
+ minterSig = sig.serialized;
450
+ } catch (err) {
451
+ throw new RelayError(
452
+ "ENCODE_FAILED",
453
+ `prepareMint: failed to sign MintRequest: ${errorMessage(err)}`,
454
+ err
455
+ );
757
456
  }
758
457
  let mintCallData;
759
458
  try {
760
459
  mintCallData = encodeFunctionData({
761
- abi: RELAYER_V2_ABI,
460
+ abi: POINT_TOKEN_V2_ABI,
762
461
  functionName: "mint",
763
- args: [params.mintRequest, params.userSignature, params.issuerSignature]
462
+ args: [params.userAddress, params.amount, params.deadline, minterSig]
764
463
  });
765
464
  } catch (err) {
766
465
  throw new RelayError(
767
466
  "ENCODE_FAILED",
768
- `prepareMint: failed to encode Relayer.mint: ${errorMessage(err)}`,
467
+ `prepareMint: failed to encode PointToken.mint: ${errorMessage(err)}`,
769
468
  err
770
469
  );
771
470
  }
772
471
  const operations = [
773
472
  {
774
- target: params.relayerAddress,
473
+ target: params.pointTokenAddress,
775
474
  value: 0n,
776
475
  data: mintCallData
777
476
  }
778
477
  ];
779
- if (params.mintRequest.feeAmount > 0n) {
478
+ if (params.feeAmount && params.feeAmount > 0n) {
479
+ if (!params.feeRecipient) {
480
+ throw new RelayError(
481
+ "ENCODE_FAILED",
482
+ "prepareMint: feeRecipient required when feeAmount > 0"
483
+ );
484
+ }
485
+ if (params.feeRecipient === "0x0000000000000000000000000000000000000000") {
486
+ throw new RelayError(
487
+ "ENCODE_FAILED",
488
+ "prepareMint: feeRecipient must not be zero address"
489
+ );
490
+ }
780
491
  operations.push({
781
492
  target: params.pointTokenAddress,
782
493
  value: 0n,
783
494
  data: encodeFunctionData({
784
- abi: POINT_TOKEN_V2_ABI,
785
- functionName: "balanceOf",
786
- // placeholder — real impl uses transfer
787
- args: [params.mintRequest.feeRecipient]
495
+ abi: erc20Abi,
496
+ functionName: "transfer",
497
+ args: [params.feeRecipient, params.feeAmount]
788
498
  })
789
499
  });
790
500
  }
@@ -793,7 +503,7 @@ var RelayService = class {
793
503
  nonce: params.aaNonce,
794
504
  operations,
795
505
  gasLimits: {
796
- callGasLimit: params.callGasLimit ?? 500000n,
506
+ callGasLimit: params.callGasLimit ?? 300000n,
797
507
  verificationGasLimit: params.verificationGasLimit ?? 150000n,
798
508
  preVerificationGas: params.preVerificationGas ?? 50000n
799
509
  }
@@ -803,13 +513,12 @@ var RelayService = class {
803
513
  * Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
804
514
  *
805
515
  * Two modes:
806
- * - `mode: 'burn'` — direct `PointToken.burn(amount)`; `msg.sender`
807
- * via EIP-7702 delegation is the user, so no signature needed
808
- * on-chain (the BurnConsent was already verified off-chain by
809
- * the issuer backend before we got here)
810
- * - `mode: 'burnWithSig'` `PointToken.burnWithSig(consent, sig)`;
811
- * used when the issuer hasn't verified the consent and the
812
- * contract has to do it on-chain
516
+ * - `mode: 'burn'` — direct `PointToken.burn(from, amount)`; only
517
+ * usable if the caller (via EIP-7702) is whitelisted as a burner.
518
+ * Rare in v1.4; kept for admin/operator tools.
519
+ * - `mode: 'burnWithSig'` `PointToken.burn(from, amount, deadline,
520
+ * burnerSig)`. Caller provides a pre-signed `BurnRequest` + sig
521
+ * bytes (typically from `PTRedeemHandler`).
813
522
  */
814
523
  prepareBurn(params) {
815
524
  if (!params.pointTokenAddress) {
@@ -824,19 +533,24 @@ var RelayService = class {
824
533
  let burnCallData;
825
534
  try {
826
535
  if (params.mode === "burnWithSig") {
827
- if (!params.burnConsent || !params.consentSignature) {
828
- throw new Error("burnWithSig requires burnConsent + consentSignature");
536
+ if (!params.burnRequest || !params.burnerSignature) {
537
+ throw new Error("burnWithSig requires burnRequest + burnerSignature");
829
538
  }
830
539
  burnCallData = encodeFunctionData({
831
540
  abi: POINT_TOKEN_V2_ABI,
832
- functionName: "burnWithSig",
833
- args: [params.burnConsent, params.consentSignature]
541
+ functionName: "burn",
542
+ args: [
543
+ params.burnRequest.from,
544
+ params.burnRequest.amount,
545
+ params.burnRequest.deadline,
546
+ params.burnerSignature
547
+ ]
834
548
  });
835
549
  } else {
836
550
  burnCallData = encodeFunctionData({
837
551
  abi: POINT_TOKEN_V2_ABI,
838
552
  functionName: "burn",
839
- args: [params.amount]
553
+ args: [params.userAddress, params.amount]
840
554
  });
841
555
  }
842
556
  } catch (err) {
@@ -872,11 +586,14 @@ function errorMessage(err) {
872
586
  // src/relay/feeManager.ts
873
587
  var DEFAULT_GAS_UNITS = 500000n;
874
588
  var DEFAULT_PREMIUM_BPS = 12e3;
875
- var FeeManager = class {
589
+ var FeeManager = class _FeeManager {
876
590
  provider;
877
591
  gasUnits;
878
592
  gasPremiumBps;
879
593
  quoteNativeToFee;
594
+ cachedFee = null;
595
+ cacheExpiresAt = 0;
596
+ static CACHE_TTL_MS = 1e4;
880
597
  constructor(config) {
881
598
  if (!config.provider) throw new Error("FeeManager: provider required");
882
599
  if (!config.quoteNativeToFee)
@@ -899,262 +616,20 @@ var FeeManager = class {
899
616
  * currency depends on how the caller wired `quoteNativeToFee`.
900
617
  */
901
618
  async estimateGasFee() {
619
+ const now = Date.now();
620
+ if (this.cachedFee !== null && now < this.cacheExpiresAt) {
621
+ return this.cachedFee;
622
+ }
902
623
  const gasPrice = await this.provider.getGasPrice();
903
624
  const nativeCost = gasPrice * this.gasUnits;
904
625
  const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
905
- return this.quoteNativeToFee(withPremium);
626
+ const fee = await this.quoteNativeToFee(withPremium);
627
+ this.cachedFee = fee;
628
+ this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
629
+ return fee;
906
630
  }
907
631
  };
908
632
 
909
- // src/gateway/types.ts
910
- var MintingGatewayError = class extends Error {
911
- code;
912
- /**
913
- * True if the ledger lock was released before this error was thrown,
914
- * meaning the user can safely retry. False means the funds are still
915
- * locked (e.g. tx may still land on-chain) and retry would double-spend.
916
- */
917
- safeToRetry;
918
- cause;
919
- constructor(code, message, opts) {
920
- super(message);
921
- this.name = "MintingGatewayError";
922
- this.code = code;
923
- this.safeToRetry = opts.safeToRetry;
924
- if (opts.cause !== void 0) this.cause = opts.cause;
925
- }
926
- };
927
-
928
- // src/gateway/mintingGateway.ts
929
- import {
930
- verifyReceiverConsent,
931
- encodeExtData
932
- } from "@pafi-dev/core";
933
- var DEFAULT_LOCK_BUFFER_MS = 6e4;
934
- var MintingGateway = class {
935
- ledger;
936
- policy;
937
- signer;
938
- relayService;
939
- now;
940
- defaultLockBufferMs;
941
- constructor(config) {
942
- if (!config.ledger) throw new Error("MintingGateway: ledger required");
943
- if (!config.policy) throw new Error("MintingGateway: policy required");
944
- if (!config.signer) throw new Error("MintingGateway: signer required");
945
- if (!config.relayService)
946
- throw new Error("MintingGateway: relayService required");
947
- this.ledger = config.ledger;
948
- this.policy = config.policy;
949
- this.signer = config.signer;
950
- this.relayService = config.relayService;
951
- this.now = config.now ?? (() => Date.now());
952
- this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
953
- }
954
- /**
955
- * @deprecated Since 0.3.0 — will be renamed to `processMint()` once
956
- * the SC team finalizes Relayer v2 ABI. The new flow drops the
957
- * swap steps entirely (no more single-call mint+swap); users swap
958
- * separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
959
- */
960
- async processMintAndCashOut(request) {
961
- const { receiverConsent, receiverSignature } = request;
962
- if (!receiverConsent || !receiverSignature) {
963
- throw new MintingGatewayError(
964
- "INVALID_REQUEST",
965
- "receiverConsent and receiverSignature are required",
966
- { safeToRetry: true }
967
- );
968
- }
969
- if (receiverConsent.amount <= 0n) {
970
- throw new MintingGatewayError(
971
- "INVALID_REQUEST",
972
- "consent amount must be positive",
973
- { safeToRetry: true }
974
- );
975
- }
976
- if (receiverConsent.originalReceiver !== request.userAddress) {
977
- throw new MintingGatewayError(
978
- "INVALID_REQUEST",
979
- "consent.originalReceiver must equal request.userAddress",
980
- { safeToRetry: true }
981
- );
982
- }
983
- const nowSec = BigInt(Math.floor(this.now() / 1e3));
984
- if (receiverConsent.deadline <= nowSec) {
985
- throw new MintingGatewayError(
986
- "CONSENT_EXPIRED",
987
- "ReceiverConsent deadline has already passed",
988
- { safeToRetry: true }
989
- );
990
- }
991
- const consentResult = await verifyReceiverConsent(
992
- request.domain,
993
- receiverConsent,
994
- receiverSignature,
995
- request.userAddress
996
- );
997
- if (!consentResult.isValid) {
998
- throw new MintingGatewayError(
999
- "INVALID_CONSENT_SIGNATURE",
1000
- `ReceiverConsent signature did not recover to ${request.userAddress}`,
1001
- { safeToRetry: true }
1002
- );
1003
- }
1004
- const policyDecision = await this.policy.evaluate({
1005
- userAddress: request.userAddress,
1006
- amount: receiverConsent.amount,
1007
- pointTokenAddress: request.pointTokenAddress,
1008
- chainId: request.chainId
1009
- });
1010
- if (!policyDecision.approved) {
1011
- const code = policyDecision.reason?.toLowerCase().includes("insufficient") ? "INSUFFICIENT_BALANCE" : "POLICY_REJECTED";
1012
- throw new MintingGatewayError(
1013
- code,
1014
- policyDecision.reason ?? "Minting request rejected by policy engine",
1015
- { safeToRetry: true }
1016
- );
1017
- }
1018
- const lockDurationMs = request.lockDurationMs ?? this.computeLockDurationMs(receiverConsent.deadline);
1019
- let lockId;
1020
- try {
1021
- lockId = await this.ledger.lockForMinting(
1022
- request.userAddress,
1023
- receiverConsent.amount,
1024
- lockDurationMs,
1025
- request.pointTokenAddress
1026
- );
1027
- } catch (err) {
1028
- throw new MintingGatewayError(
1029
- "INSUFFICIENT_BALANCE",
1030
- `Failed to lock ledger balance: ${errorMessage2(err)}`,
1031
- { safeToRetry: true, cause: err }
1032
- );
1033
- }
1034
- try {
1035
- let minterSignature;
1036
- try {
1037
- minterSignature = await this.signer.signMintRequest(request.domain, {
1038
- to: request.userAddress,
1039
- amount: receiverConsent.amount,
1040
- nonce: receiverConsent.nonce,
1041
- deadline: receiverConsent.deadline
1042
- });
1043
- } catch (err) {
1044
- await this.releaseLockSafely(lockId);
1045
- throw new MintingGatewayError(
1046
- "SIGNER_FAILED",
1047
- `Issuer signer failed: ${errorMessage2(err)}`,
1048
- { safeToRetry: true, cause: err }
1049
- );
1050
- }
1051
- const mintParams = {
1052
- pointToken: request.pointTokenAddress,
1053
- receiver: request.userAddress,
1054
- amount: receiverConsent.amount,
1055
- deadline: receiverConsent.deadline,
1056
- minterSig: minterSignature.serialized,
1057
- receiverSig: receiverSignature,
1058
- extData: receiverConsent.extData
1059
- };
1060
- const swapParams = {
1061
- path: request.swapPath,
1062
- deadline: request.swapDeadline
1063
- };
1064
- let relayResult;
1065
- try {
1066
- relayResult = await this.relayService.submitMintAndSwap({
1067
- mint: mintParams,
1068
- swap: swapParams
1069
- });
1070
- } catch (err) {
1071
- await this.handleRelayFailure(err, lockId);
1072
- }
1073
- const result = {
1074
- txHash: relayResult.txHash,
1075
- lockId
1076
- };
1077
- if (relayResult.blockNumber !== void 0) {
1078
- result.blockNumber = relayResult.blockNumber;
1079
- }
1080
- if (relayResult.gasUsed !== void 0) {
1081
- result.gasUsed = relayResult.gasUsed;
1082
- }
1083
- return result;
1084
- } catch (err) {
1085
- if (err instanceof MintingGatewayError) throw err;
1086
- await this.releaseLockSafely(lockId);
1087
- throw new MintingGatewayError(
1088
- "RELAY_SUBMIT_FAILED",
1089
- `Unexpected error: ${errorMessage2(err)}`,
1090
- { safeToRetry: true, cause: err }
1091
- );
1092
- }
1093
- }
1094
- // ---------------------------------------------------------------------------
1095
- // Internals
1096
- // ---------------------------------------------------------------------------
1097
- computeLockDurationMs(consentDeadlineSec) {
1098
- const nowMs = this.now();
1099
- const deadlineMs = Number(consentDeadlineSec) * 1e3;
1100
- const remaining = Math.max(0, deadlineMs - nowMs);
1101
- return remaining + this.defaultLockBufferMs;
1102
- }
1103
- /**
1104
- * Map a RelayError to a MintingGatewayError, releasing the lock only
1105
- * when the tx definitely did not land. `TX_REVERTED` and `TIMEOUT`
1106
- * leave the lock in place because the tx may still be in the mempool
1107
- * or already mined — releasing would enable a double-spend on retry.
1108
- * Always throws.
1109
- */
1110
- async handleRelayFailure(err, lockId) {
1111
- if (err instanceof RelayError) {
1112
- switch (err.code) {
1113
- case "ENCODE_FAILED":
1114
- case "SIMULATION_FAILED":
1115
- case "SUBMIT_FAILED":
1116
- case "NOT_CONFIGURED":
1117
- await this.releaseLockSafely(lockId);
1118
- throw new MintingGatewayError(
1119
- err.code === "SIMULATION_FAILED" ? "RELAY_SIMULATION_FAILED" : "RELAY_SUBMIT_FAILED",
1120
- err.message,
1121
- { safeToRetry: true, cause: err }
1122
- );
1123
- case "TX_REVERTED":
1124
- throw new MintingGatewayError("RELAY_REVERTED", err.message, {
1125
- safeToRetry: false,
1126
- cause: err
1127
- });
1128
- case "TIMEOUT":
1129
- throw new MintingGatewayError("RELAY_TIMEOUT", err.message, {
1130
- safeToRetry: false,
1131
- cause: err
1132
- });
1133
- }
1134
- }
1135
- await this.releaseLockSafely(lockId);
1136
- throw new MintingGatewayError(
1137
- "RELAY_SUBMIT_FAILED",
1138
- `Unexpected relay error: ${errorMessage2(err)}`,
1139
- { safeToRetry: true, cause: err }
1140
- );
1141
- }
1142
- /**
1143
- * Release a lock, swallowing any secondary error. We never want a lock
1144
- * release failure to mask the original error — the lock will auto-expire
1145
- * via TTL anyway.
1146
- */
1147
- async releaseLockSafely(lockId) {
1148
- try {
1149
- await this.ledger.releaseLock(lockId);
1150
- } catch {
1151
- }
1152
- }
1153
- };
1154
- function errorMessage2(err) {
1155
- return err instanceof Error ? err.message : String(err);
1156
- }
1157
-
1158
633
  // src/indexer/types.ts
1159
634
  var InMemoryCursorStore = class {
1160
635
  cursor;
@@ -1167,7 +642,7 @@ var InMemoryCursorStore = class {
1167
642
  };
1168
643
 
1169
644
  // src/indexer/pointIndexer.ts
1170
- import { getAddress as getAddress4, parseAbiItem } from "viem";
645
+ import { getAddress as getAddress3, parseAbiItem } from "viem";
1171
646
  var TRANSFER_EVENT = parseAbiItem(
1172
647
  "event Transfer(address indexed from, address indexed to, uint256 value)"
1173
648
  );
@@ -1238,7 +713,8 @@ var PointIndexer = class {
1238
713
  return;
1239
714
  }
1240
715
  await this.processBlockRange(from, safeHead);
1241
- } catch {
716
+ } catch (err) {
717
+ console.error("[PAFI] PointIndexer tick error:", err);
1242
718
  }
1243
719
  this.scheduleNext();
1244
720
  }
@@ -1288,10 +764,10 @@ var PointIndexer = class {
1288
764
  for (const log of logs) {
1289
765
  const args = log.args;
1290
766
  if (!args.from || !args.to || args.value === void 0) continue;
1291
- if (getAddress4(args.from) !== ZERO_ADDRESS) continue;
767
+ if (getAddress3(args.from) !== ZERO_ADDRESS) continue;
1292
768
  if (log.blockNumber === null || log.transactionHash === null) continue;
1293
769
  out.push({
1294
- to: getAddress4(args.to),
770
+ to: getAddress3(args.to),
1295
771
  amount: args.value,
1296
772
  blockNumber: log.blockNumber,
1297
773
  txHash: log.transactionHash,
@@ -1347,7 +823,7 @@ function pickMatchingLock(locks, amount) {
1347
823
  }
1348
824
 
1349
825
  // src/indexer/burnIndexer.ts
1350
- import { getAddress as getAddress5, parseAbiItem as parseAbiItem2 } from "viem";
826
+ import { getAddress as getAddress4, parseAbiItem as parseAbiItem2 } from "viem";
1351
827
  var TRANSFER_EVENT2 = parseAbiItem2(
1352
828
  "event Transfer(address indexed from, address indexed to, uint256 value)"
1353
829
  );
@@ -1364,18 +840,7 @@ var BurnIndexer = class {
1364
840
  confirmations;
1365
841
  batchSize;
1366
842
  pollIntervalMs;
1367
- /**
1368
- * Caller-supplied matcher. Return the lockId to resolve for a given
1369
- * burn event, or `undefined` to skip. Runs synchronously via the
1370
- * ledger's query path.
1371
- *
1372
- * Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
1373
- * lock id `burn-${from}-${amount}` — the in-memory ledger assigns
1374
- * incrementing IDs so callers with the memory ledger must provide a
1375
- * custom matcher. Real DB-backed ledgers override this to JOIN on
1376
- * their `pending_credits` table.
1377
- */
1378
- matchLockId = async () => void 0;
843
+ matchLockId;
1379
844
  running = false;
1380
845
  timer;
1381
846
  constructor(config) {
@@ -1393,6 +858,12 @@ var BurnIndexer = class {
1393
858
  );
1394
859
  this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
1395
860
  this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
861
+ if (!config.matchLockId) {
862
+ throw new Error(
863
+ "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."
864
+ );
865
+ }
866
+ this.matchLockId = config.matchLockId;
1396
867
  }
1397
868
  start() {
1398
869
  if (this.running) return;
@@ -1422,7 +893,8 @@ var BurnIndexer = class {
1422
893
  return;
1423
894
  }
1424
895
  await this.processBlockRange(from, safeHead);
1425
- } catch {
896
+ } catch (err) {
897
+ console.error("[PAFI] BurnIndexer tick error:", err);
1426
898
  }
1427
899
  this.scheduleNext();
1428
900
  }
@@ -1467,10 +939,10 @@ var BurnIndexer = class {
1467
939
  for (const log of logs) {
1468
940
  const args = log.args;
1469
941
  if (!args.from || !args.to || args.value === void 0) continue;
1470
- if (getAddress5(args.to) !== ZERO_ADDRESS2) continue;
942
+ if (getAddress4(args.to) !== ZERO_ADDRESS2) continue;
1471
943
  if (log.blockNumber === null || log.transactionHash === null) continue;
1472
944
  out.push({
1473
- from: getAddress5(args.from),
945
+ from: getAddress4(args.from),
1474
946
  amount: args.value,
1475
947
  blockNumber: log.blockNumber,
1476
948
  txHash: log.transactionHash,
@@ -1485,20 +957,27 @@ var BurnIndexer = class {
1485
957
  * log + skip.
1486
958
  */
1487
959
  async finalize(evt) {
960
+ const txHash = evt.txHash;
1488
961
  const lockId = await this.matchLockId(evt);
1489
- if (!lockId) return;
962
+ if (lockId === void 0) {
963
+ console.warn(
964
+ "[PAFI] BurnIndexer: matchLockId returned undefined for burn tx " + txHash + ". This burn will NOT be credited. Implement matchLockId to map burn events to lock IDs."
965
+ );
966
+ return;
967
+ }
1490
968
  if (!this.ledger.resolveCreditByBurnTx) {
1491
969
  return;
1492
970
  }
1493
971
  try {
1494
972
  await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
1495
- } catch {
973
+ } catch (err) {
974
+ console.error("[PAFI] BurnIndexer finalize error \u2014 credit may be lost:", err);
1496
975
  }
1497
976
  }
1498
977
  };
1499
978
 
1500
979
  // src/api/handlers.ts
1501
- import { getAddress as getAddress6 } from "viem";
980
+ import { getAddress as getAddress5 } from "viem";
1502
981
  import {
1503
982
  getMintRequestNonce,
1504
983
  getPointTokenBalance,
@@ -1509,7 +988,6 @@ import {
1509
988
  } from "@pafi-dev/core";
1510
989
  var IssuerApiHandlers = class {
1511
990
  authService;
1512
- gateway;
1513
991
  ledger;
1514
992
  provider;
1515
993
  /**
@@ -1517,18 +995,14 @@ var IssuerApiHandlers = class {
1517
995
  * validate the request's `pointTokenAddress` against this set.
1518
996
  */
1519
997
  supportedTokens;
1520
- /** First supported token — used as default when a handler doesn't
1521
- * receive a `pointTokenAddress` in the request (shouldn't happen in
1522
- * practice, but keeps type-narrowing happy). */
1523
- defaultToken;
1524
998
  chainId;
1525
999
  contracts;
1526
1000
  pafiWebUrl;
1527
1001
  feeManager;
1528
1002
  poolsProvider;
1003
+ claim;
1529
1004
  constructor(config) {
1530
1005
  this.authService = config.authService;
1531
- this.gateway = config.gateway;
1532
1006
  this.ledger = config.ledger;
1533
1007
  this.provider = config.provider;
1534
1008
  const raw = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
@@ -1537,14 +1011,14 @@ var IssuerApiHandlers = class {
1537
1011
  "IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
1538
1012
  );
1539
1013
  }
1540
- const normalized = raw.map((a) => getAddress6(a));
1014
+ const normalized = raw.map((a) => getAddress5(a));
1541
1015
  this.supportedTokens = new Set(normalized);
1542
- this.defaultToken = normalized[0];
1543
1016
  this.chainId = config.chainId;
1544
1017
  this.contracts = config.contracts;
1545
1018
  if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
1546
1019
  if (config.feeManager) this.feeManager = config.feeManager;
1547
1020
  if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
1021
+ if (config.claim) this.claim = config.claim;
1548
1022
  }
1549
1023
  // =========================================================================
1550
1024
  // Public handlers (no auth required)
@@ -1559,6 +1033,12 @@ var IssuerApiHandlers = class {
1559
1033
  if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
1560
1034
  throw new Error("handleLogin: message and signature are required");
1561
1035
  }
1036
+ if (body.message.length > 4096) {
1037
+ throw new Error("message too long");
1038
+ }
1039
+ if (body.signature.length > 260) {
1040
+ throw new Error("signature too long");
1041
+ }
1562
1042
  const result = await this.authService.login(body.message, body.signature);
1563
1043
  return {
1564
1044
  token: result.token,
@@ -1573,9 +1053,12 @@ var IssuerApiHandlers = class {
1573
1053
  * needs to build EIP-712 messages and interact with on-chain.
1574
1054
  */
1575
1055
  async handleConfig(chainId) {
1056
+ if (!Number.isInteger(chainId) || chainId <= 0) {
1057
+ throw new Error("invalid chainId");
1058
+ }
1576
1059
  if (chainId !== this.chainId) {
1577
1060
  throw new Error(
1578
- `handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
1061
+ `handleConfig: unsupported chainId ${chainId}`
1579
1062
  );
1580
1063
  }
1581
1064
  const contracts = {
@@ -1638,14 +1121,14 @@ var IssuerApiHandlers = class {
1638
1121
  `handleUser: unsupported chainId ${request.chainId}`
1639
1122
  );
1640
1123
  }
1641
- const normalizedAuthed = getAddress6(userAddress);
1642
- const normalizedRequest = getAddress6(request.userAddress);
1124
+ const normalizedAuthed = getAddress5(userAddress);
1125
+ const normalizedRequest = getAddress5(request.userAddress);
1643
1126
  if (normalizedAuthed !== normalizedRequest) {
1644
1127
  throw new Error(
1645
1128
  "handleUser: request userAddress must match authenticated user"
1646
1129
  );
1647
1130
  }
1648
- const pointToken = getAddress6(request.pointTokenAddress);
1131
+ const pointToken = getAddress5(request.pointTokenAddress);
1649
1132
  if (!this.supportedTokens.has(pointToken)) {
1650
1133
  throw new Error(
1651
1134
  `handleUser: unsupported pointToken ${pointToken}`
@@ -1688,19 +1171,32 @@ var IssuerApiHandlers = class {
1688
1171
  `handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
1689
1172
  );
1690
1173
  }
1691
- const pointToken = getAddress6(request.pointTokenAddress);
1174
+ const pointToken = getAddress5(request.pointTokenAddress);
1692
1175
  if (!this.supportedTokens.has(pointToken)) {
1693
1176
  throw new Error(
1694
1177
  `handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
1695
1178
  );
1696
1179
  }
1180
+ const consent = request.receiverConsent;
1181
+ if (getAddress5(consent.originalReceiver) !== getAddress5(userAddress)) {
1182
+ throw new Error(
1183
+ "handleBuildConsentTypedData: receiverConsent.originalReceiver must match authenticated user"
1184
+ );
1185
+ }
1186
+ if (consent.amount <= 0n) {
1187
+ throw new Error("handleBuildConsentTypedData: amount must be positive");
1188
+ }
1189
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
1190
+ if (consent.deadline <= nowSecs) {
1191
+ throw new Error("handleBuildConsentTypedData: deadline is in the past");
1192
+ }
1697
1193
  const name = await getTokenName(this.provider, pointToken);
1698
1194
  const domain = {
1699
1195
  name,
1700
1196
  verifyingContract: pointToken,
1701
1197
  chainId: this.chainId
1702
1198
  };
1703
- const typedData = buildReceiverConsentTypedData(domain, request.receiverConsent);
1199
+ const typedData = buildReceiverConsentTypedData(domain, consent);
1704
1200
  return {
1705
1201
  typedData: {
1706
1202
  domain: typedData.domain,
@@ -1711,54 +1207,97 @@ var IssuerApiHandlers = class {
1711
1207
  };
1712
1208
  }
1713
1209
  /**
1714
- * `POST /claim-and-swap`
1210
+ * `POST /claim`
1715
1211
  *
1716
- * @deprecated Since 0.3.0 the single-call mint-then-swap flow is
1717
- * retired in v1.4. Use the new `handleClaim()` (mint only) and let
1718
- * the user swap separately on PAFI Web. See
1719
- * [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
1720
- * removed in 2.0.
1212
+ * Policy gate + ledger lock + MintRequest signing in a single atomic
1213
+ * step. Returns an unsigned UserOp the frontend attaches paymaster data
1214
+ * to and submits via EIP-7702 + Bundler.
1721
1215
  *
1722
- * Legacy behavior: the terminal handler forwards the verified
1723
- * consent to the MintingGateway, which runs the 11-step flow.
1216
+ * Order of operations:
1217
+ * 1. Validate request fields.
1218
+ * 2. policy.evaluate() — throws if denied; cannot be bypassed.
1219
+ * 3. ledger.lockForMinting() — reserves the balance.
1220
+ * 4. Read on-chain mintRequestNonce + token name in parallel.
1221
+ * 5. relayService.prepareMint() — sign MintRequest + encode UserOp.
1222
+ * 6. On any error after step 3, release the lock before re-throwing.
1724
1223
  */
1725
- async handleClaimAndSwap(userAddress, request) {
1224
+ async handleClaim(userAddress, request) {
1225
+ if (!this.claim) {
1226
+ throw new Error("handleClaim: claim is not configured on this issuer");
1227
+ }
1726
1228
  if (request.chainId !== this.chainId) {
1727
- throw new Error(
1728
- `handleClaimAndSwap: unsupported chainId ${request.chainId}`
1729
- );
1229
+ throw new Error(`handleClaim: unsupported chainId ${request.chainId}`);
1730
1230
  }
1731
- const pointToken = getAddress6(request.pointTokenAddress);
1231
+ const pointToken = getAddress5(request.pointTokenAddress);
1732
1232
  if (!this.supportedTokens.has(pointToken)) {
1733
- throw new Error(
1734
- `handleClaimAndSwap: unsupported pointToken ${pointToken}`
1735
- );
1233
+ throw new Error(`handleClaim: unsupported pointToken ${pointToken}`);
1736
1234
  }
1737
- const result = await this.gateway.processMintAndCashOut({
1738
- userAddress: getAddress6(userAddress),
1235
+ if (request.amount <= 0n) {
1236
+ throw new Error("handleClaim: amount must be positive");
1237
+ }
1238
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
1239
+ if (request.deadline <= nowSecs) {
1240
+ throw new Error("handleClaim: deadline is in the past");
1241
+ }
1242
+ const { policy, relayService, issuerSignerWallet, batchExecutorAddress } = this.claim;
1243
+ const lockDurationMs = this.claim.lockDurationMs ?? 15 * 60 * 1e3;
1244
+ const normalizedUser = getAddress5(userAddress);
1245
+ const decision = await policy.evaluate({
1246
+ userAddress: normalizedUser,
1247
+ amount: request.amount,
1739
1248
  pointTokenAddress: pointToken,
1740
- chainId: request.chainId,
1741
- domain: request.domain,
1742
- receiverConsent: request.receiverConsent,
1743
- receiverSignature: request.receiverSignature,
1744
- swapPath: request.swapPath,
1745
- swapDeadline: request.swapDeadline
1249
+ chainId: this.chainId
1746
1250
  });
1747
- const response = {
1748
- txHash: result.txHash,
1749
- lockId: result.lockId
1750
- };
1751
- if (result.blockNumber !== void 0)
1752
- response.blockNumber = result.blockNumber;
1753
- if (result.gasUsed !== void 0) response.gasUsed = result.gasUsed;
1754
- return response;
1251
+ if (!decision.approved) {
1252
+ throw new Error(`handleClaim: policy denied \u2014 ${decision.reason ?? "no reason given"}`);
1253
+ }
1254
+ const lockId = await this.ledger.lockForMinting(
1255
+ normalizedUser,
1256
+ request.amount,
1257
+ lockDurationMs,
1258
+ pointToken
1259
+ );
1260
+ try {
1261
+ const [mintRequestNonce, tokenName] = await Promise.all([
1262
+ getMintRequestNonce(this.provider, pointToken, normalizedUser),
1263
+ getTokenName(this.provider, pointToken)
1264
+ ]);
1265
+ const domain = {
1266
+ name: tokenName,
1267
+ verifyingContract: pointToken,
1268
+ chainId: this.chainId
1269
+ };
1270
+ const userOp = await relayService.prepareMint({
1271
+ userAddress: normalizedUser,
1272
+ aaNonce: request.aaNonce,
1273
+ batchExecutorAddress,
1274
+ pointTokenAddress: pointToken,
1275
+ amount: request.amount,
1276
+ issuerSignerWallet,
1277
+ domain,
1278
+ mintRequestNonce,
1279
+ deadline: request.deadline,
1280
+ feeAmount: request.feeAmount,
1281
+ feeRecipient: request.feeRecipient
1282
+ });
1283
+ return {
1284
+ lockId,
1285
+ userOp,
1286
+ expiresInSeconds: Math.floor(lockDurationMs / 1e3)
1287
+ };
1288
+ } catch (err) {
1289
+ await this.ledger.releaseLock(lockId).catch(() => {
1290
+ });
1291
+ throw err;
1292
+ }
1755
1293
  }
1756
1294
  };
1757
1295
 
1758
1296
  // src/api/handlers/ptRedeemHandler.ts
1759
- import { getAddress as getAddress7 } from "viem";
1760
- import { verifyBurnConsent } from "@pafi-dev/core";
1297
+ import { getAddress as getAddress6 } from "viem";
1298
+ import { signBurnRequest, POINT_TOKEN_V2_ABI as POINT_TOKEN_V2_ABI2, getPointTokenBalance as getPointTokenBalance2 } from "@pafi-dev/core";
1761
1299
  var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1300
+ var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
1762
1301
  var PTRedeemError = class extends Error {
1763
1302
  constructor(code, message) {
1764
1303
  super(message);
@@ -1770,11 +1309,14 @@ var PTRedeemError = class extends Error {
1770
1309
  var PTRedeemHandler = class {
1771
1310
  ledger;
1772
1311
  relayService;
1312
+ provider;
1773
1313
  pointTokenAddress;
1774
1314
  batchExecutorAddress;
1775
1315
  chainId;
1776
1316
  domain;
1317
+ burnerSignerWallet;
1777
1318
  redeemLockDurationMs;
1319
+ signatureDeadlineSeconds;
1778
1320
  now;
1779
1321
  constructor(config) {
1780
1322
  if (!config.ledger.reservePendingCredit) {
@@ -1783,46 +1325,88 @@ var PTRedeemHandler = class {
1783
1325
  "PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
1784
1326
  );
1785
1327
  }
1328
+ if (!config.burnerSignerWallet) {
1329
+ throw new PTRedeemError(
1330
+ "SIGNING_FAILED",
1331
+ "PTRedeemHandler requires burnerSignerWallet (issuer burner signer)"
1332
+ );
1333
+ }
1786
1334
  this.ledger = config.ledger;
1787
1335
  this.relayService = config.relayService;
1788
- this.pointTokenAddress = getAddress7(config.pointTokenAddress);
1789
- this.batchExecutorAddress = getAddress7(config.batchExecutorAddress);
1336
+ this.provider = config.provider;
1337
+ this.pointTokenAddress = getAddress6(config.pointTokenAddress);
1338
+ this.batchExecutorAddress = getAddress6(config.batchExecutorAddress);
1790
1339
  this.chainId = config.chainId;
1791
1340
  this.domain = config.domain;
1341
+ this.burnerSignerWallet = config.burnerSignerWallet;
1342
+ if (this.burnerSignerWallet?.account?.type === "local") {
1343
+ console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
1344
+ }
1792
1345
  this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1346
+ this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
1793
1347
  this.now = config.now ?? (() => Date.now());
1794
1348
  }
1795
1349
  async handle(request) {
1350
+ if (getAddress6(request.authenticatedAddress) !== getAddress6(request.userAddress)) {
1351
+ throw new PTRedeemError(
1352
+ "UNAUTHORIZED",
1353
+ `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
1354
+ );
1355
+ }
1796
1356
  if (request.amount <= 0n) {
1797
- throw new PTRedeemError("INVALID_CONSENT", "redeem amount must be positive");
1357
+ throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
1798
1358
  }
1799
- if (request.consent.amount !== request.amount) {
1359
+ let burnNonce;
1360
+ try {
1361
+ burnNonce = await this.provider.readContract({
1362
+ address: this.pointTokenAddress,
1363
+ abi: POINT_TOKEN_V2_ABI2,
1364
+ functionName: "burnRequestNonces",
1365
+ args: [request.userAddress]
1366
+ });
1367
+ } catch (err) {
1800
1368
  throw new PTRedeemError(
1801
- "AMOUNT_MISMATCH",
1802
- `consent.amount (${request.consent.amount}) must match request.amount (${request.amount})`
1369
+ "NONCE_READ_FAILED",
1370
+ `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
1803
1371
  );
1804
1372
  }
1805
- const nowSeconds = BigInt(Math.floor(this.now() / 1e3));
1806
- if (request.consent.deadline <= nowSeconds) {
1373
+ const onChainBalance = await getPointTokenBalance2(
1374
+ this.provider,
1375
+ this.pointTokenAddress,
1376
+ request.userAddress
1377
+ );
1378
+ if (onChainBalance < request.amount) {
1807
1379
  throw new PTRedeemError(
1808
- "EXPIRED_CONSENT",
1809
- `consent deadline (${request.consent.deadline}) already passed`
1380
+ "INVALID_AMOUNT",
1381
+ `insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
1810
1382
  );
1811
1383
  }
1812
- const verification = await verifyBurnConsent(
1813
- {
1814
- name: this.domain.name,
1815
- chainId: this.chainId,
1816
- verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
1817
- },
1818
- request.consent,
1819
- request.consentSignature,
1820
- request.userAddress
1384
+ const deadline = BigInt(
1385
+ Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1821
1386
  );
1822
- if (!verification.isValid) {
1387
+ const domain = {
1388
+ name: this.domain.name,
1389
+ chainId: this.chainId,
1390
+ verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
1391
+ };
1392
+ const burnRequest = {
1393
+ from: request.userAddress,
1394
+ amount: request.amount,
1395
+ nonce: burnNonce,
1396
+ deadline
1397
+ };
1398
+ let burnerSignature;
1399
+ try {
1400
+ const sig = await signBurnRequest(
1401
+ this.burnerSignerWallet,
1402
+ domain,
1403
+ burnRequest
1404
+ );
1405
+ burnerSignature = sig.serialized;
1406
+ } catch (err) {
1823
1407
  throw new PTRedeemError(
1824
- "SIGNATURE_MISMATCH",
1825
- `signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
1408
+ "SIGNING_FAILED",
1409
+ `failed to sign BurnRequest: ${err instanceof Error ? err.message : String(err)}`
1826
1410
  );
1827
1411
  }
1828
1412
  const lockId = await this.ledger.reservePendingCredit(
@@ -1837,33 +1421,21 @@ var PTRedeemHandler = class {
1837
1421
  aaNonce: request.aaNonce,
1838
1422
  pointTokenAddress: this.pointTokenAddress,
1839
1423
  batchExecutorAddress: this.batchExecutorAddress,
1840
- burnConsent: request.consent,
1841
- consentSignature: parseSigStruct(request.consentSignature)
1424
+ burnRequest,
1425
+ burnerSignature
1842
1426
  });
1843
1427
  return {
1844
1428
  lockId,
1845
1429
  userOp,
1846
- expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
1430
+ expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3),
1431
+ signatureDeadline: deadline
1847
1432
  };
1848
1433
  }
1849
1434
  };
1850
- function parseSigStruct(serialized) {
1851
- const raw = serialized.slice(2);
1852
- if (raw.length !== 130) {
1853
- throw new PTRedeemError(
1854
- "INVALID_CONSENT",
1855
- `signature must be 65 bytes, got ${raw.length / 2}`
1856
- );
1857
- }
1858
- const r = `0x${raw.slice(0, 64)}`;
1859
- const s = `0x${raw.slice(64, 128)}`;
1860
- const v = parseInt(raw.slice(128, 130), 16);
1861
- return { v, r, s };
1862
- }
1863
1435
 
1864
1436
  // src/api/handlers/topUpRedemptionHandler.ts
1865
- import { getAddress as getAddress8 } from "viem";
1866
- import { getPointTokenBalance as getPointTokenBalance2 } from "@pafi-dev/core";
1437
+ import { getAddress as getAddress7 } from "viem";
1438
+ import { getPointTokenBalance as getPointTokenBalance3 } from "@pafi-dev/core";
1867
1439
  var TopUpRedemptionError = class extends Error {
1868
1440
  constructor(code, message) {
1869
1441
  super(message);
@@ -1881,9 +1453,15 @@ var TopUpRedemptionHandler = class {
1881
1453
  this.ledger = config.ledger;
1882
1454
  this.ptRedeemHandler = config.ptRedeemHandler;
1883
1455
  this.provider = config.provider;
1884
- this.pointTokenAddress = getAddress8(config.pointTokenAddress);
1456
+ this.pointTokenAddress = getAddress7(config.pointTokenAddress);
1885
1457
  }
1886
1458
  async handle(request) {
1459
+ if (getAddress7(request.authenticatedAddress) !== getAddress7(request.userAddress)) {
1460
+ throw new TopUpRedemptionError(
1461
+ "UNAUTHORIZED",
1462
+ `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
1463
+ );
1464
+ }
1887
1465
  const offChainBalance = await this.ledger.getBalance(
1888
1466
  request.userAddress,
1889
1467
  this.pointTokenAddress
@@ -1892,7 +1470,7 @@ var TopUpRedemptionHandler = class {
1892
1470
  return { action: "NO_TOP_UP_NEEDED", offChainBalance };
1893
1471
  }
1894
1472
  const shortfall = request.requiredAmount - offChainBalance;
1895
- const onChainBalance = await getPointTokenBalance2(
1473
+ const onChainBalance = await getPointTokenBalance3(
1896
1474
  this.provider,
1897
1475
  this.pointTokenAddress,
1898
1476
  request.userAddress
@@ -1905,24 +1483,11 @@ var TopUpRedemptionHandler = class {
1905
1483
  shortfall
1906
1484
  };
1907
1485
  }
1908
- if (request.redeemRequest.consent.amount < shortfall) {
1909
- throw new TopUpRedemptionError(
1910
- "CONSENT_AMOUNT_TOO_LOW",
1911
- `consent.amount (${request.redeemRequest.consent.amount}) must cover shortfall (${shortfall})`
1912
- );
1913
- }
1914
- if (request.redeemRequest.consent.amount !== shortfall) {
1915
- throw new TopUpRedemptionError(
1916
- "CONSENT_AMOUNT_TOO_LOW",
1917
- `consent.amount (${request.redeemRequest.consent.amount}) must equal shortfall (${shortfall}) exactly \u2014 re-sign with correct amount`
1918
- );
1919
- }
1920
1486
  const redeem = await this.ptRedeemHandler.handle({
1487
+ authenticatedAddress: request.authenticatedAddress,
1921
1488
  userAddress: request.userAddress,
1922
1489
  amount: shortfall,
1923
- consent: request.redeemRequest.consent,
1924
- consentSignature: request.redeemRequest.consentSignature,
1925
- aaNonce: request.redeemRequest.aaNonce
1490
+ aaNonce: request.aaNonce
1926
1491
  });
1927
1492
  return {
1928
1493
  action: "TOP_UP_STARTED",
@@ -1933,6 +1498,7 @@ var TopUpRedemptionHandler = class {
1933
1498
  };
1934
1499
 
1935
1500
  // src/pools/subgraphPoolsProvider.ts
1501
+ import { isAddress } from "viem";
1936
1502
  var DEFAULT_CACHE_TTL_MS = 3e4;
1937
1503
  var POOL_QUERY = `
1938
1504
  query GetPoolForPointToken($id: ID!) {
@@ -1955,6 +1521,19 @@ function createSubgraphPoolsProvider(config) {
1955
1521
  "createSubgraphPoolsProvider: subgraphUrl is required"
1956
1522
  );
1957
1523
  }
1524
+ try {
1525
+ const parsed = new URL(config.subgraphUrl);
1526
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
1527
+ throw new Error("subgraphUrl must use HTTPS in production");
1528
+ }
1529
+ } catch (err) {
1530
+ if (err instanceof TypeError) {
1531
+ throw new Error(
1532
+ `subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
1533
+ );
1534
+ }
1535
+ throw err;
1536
+ }
1958
1537
  const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
1959
1538
  const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1960
1539
  const now = config.now ?? (() => Date.now());
@@ -2023,6 +1602,26 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress)
2023
1602
  return [];
2024
1603
  }
2025
1604
  const { pool } = token;
1605
+ if (!isAddress(pool.hooks)) {
1606
+ console.error(
1607
+ "[PAFI] SubgraphPoolsProvider: invalid hooks address in response:",
1608
+ pool.hooks,
1609
+ "\u2014 skipping pool"
1610
+ );
1611
+ return [];
1612
+ }
1613
+ if (!isAddress(pool.token0.id) || !isAddress(pool.token1.id)) {
1614
+ console.error(
1615
+ "[PAFI] SubgraphPoolsProvider: invalid token address in response \u2014 skipping pool"
1616
+ );
1617
+ return [];
1618
+ }
1619
+ if (!Number.isFinite(Number(pool.feeTier)) || !Number.isFinite(Number(pool.tickSpacing))) {
1620
+ console.error(
1621
+ "[PAFI] SubgraphPoolsProvider: invalid feeTier/tickSpacing \u2014 skipping pool"
1622
+ );
1623
+ return [];
1624
+ }
2026
1625
  const [currency0, currency1] = sortCurrencies(
2027
1626
  pool.token0.id,
2028
1627
  pool.token1.id
@@ -2058,6 +1657,19 @@ function createSubgraphNativeUsdtQuoter(config) {
2058
1657
  "createSubgraphNativeUsdtQuoter: subgraphUrl is required"
2059
1658
  );
2060
1659
  }
1660
+ try {
1661
+ const parsed = new URL(config.subgraphUrl);
1662
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
1663
+ throw new Error("subgraphUrl must use HTTPS in production");
1664
+ }
1665
+ } catch (err) {
1666
+ if (err instanceof TypeError) {
1667
+ throw new Error(
1668
+ `subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
1669
+ );
1670
+ }
1671
+ throw err;
1672
+ }
2061
1673
  const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
2062
1674
  const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
2063
1675
  const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
@@ -2134,6 +1746,14 @@ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
2134
1746
  );
2135
1747
  return null;
2136
1748
  }
1749
+ const MIN_REASONABLE_ETH_PRICE = 100;
1750
+ const MAX_REASONABLE_ETH_PRICE = 1e5;
1751
+ if (parsed < MIN_REASONABLE_ETH_PRICE || parsed > MAX_REASONABLE_ETH_PRICE) {
1752
+ console.warn(
1753
+ `[PAFI] SubgraphNativeUsdtQuoter: ETH/USD price ${parsed} is outside reasonable range. Using fallback.`
1754
+ );
1755
+ return null;
1756
+ }
2137
1757
  return parsed;
2138
1758
  }
2139
1759
  function toUsdtPerNative(priceFloat, usdtDecimals) {
@@ -2144,7 +1764,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
2144
1764
  }
2145
1765
 
2146
1766
  // src/balance/balanceAggregator.ts
2147
- import { getPointTokenBalance as getPointTokenBalance3 } from "@pafi-dev/core";
1767
+ import { getPointTokenBalance as getPointTokenBalance4 } from "@pafi-dev/core";
2148
1768
  var BalanceAggregator = class {
2149
1769
  provider;
2150
1770
  ledger;
@@ -2165,7 +1785,7 @@ var BalanceAggregator = class {
2165
1785
  async getCombinedBalance(user, pointToken) {
2166
1786
  const [offChain, onChain] = await Promise.all([
2167
1787
  this.ledger.getBalance(user, pointToken),
2168
- getPointTokenBalance3(this.provider, pointToken, user)
1788
+ getPointTokenBalance4(this.provider, pointToken, user)
2169
1789
  ]);
2170
1790
  return {
2171
1791
  offChain,
@@ -2203,28 +1823,11 @@ var PafiBackendError = class extends Error {
2203
1823
  code;
2204
1824
  httpStatus;
2205
1825
  details;
2206
- /**
2207
- * Seconds to wait before retry. Populated from the server body
2208
- * (e.g. rate limit returns the number of seconds until UTC midnight).
2209
- */
2210
1826
  retryAfter;
2211
- /**
2212
- * `safeToRetry` as reported by the server body. Prefer this over the
2213
- * code-based heuristic when available — the server knows more about
2214
- * whether the same request will succeed on retry.
2215
- */
2216
1827
  serverSafeToRetry;
2217
- /**
2218
- * Whether the caller can safely retry the same request.
2219
- *
2220
- * If the server provided `safeToRetry` in the body, trust that.
2221
- * Otherwise fall back to a code-based heuristic.
2222
- */
2223
1828
  get safeToRetry() {
2224
1829
  if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
2225
1830
  switch (this.code) {
2226
- case "PAYMASTER_UNAVAILABLE":
2227
- case "PAYMASTER_TIMEOUT":
2228
1831
  case "RATE_LIMITER_UNAVAILABLE":
2229
1832
  case "INTERNAL_ERROR":
2230
1833
  case "TIMEOUT":
@@ -2233,196 +1836,22 @@ var PafiBackendError = class extends Error {
2233
1836
  case "RATE_LIMIT_EXCEEDED":
2234
1837
  case "RATE_LIMIT_EXCEEDED_DAILY":
2235
1838
  case "RATE_LIMIT_EXCEEDED_PER_USER":
1839
+ case "ISSUER_BUDGET_EXCEEDED":
2236
1840
  return true;
2237
- // after retryAfter
2238
1841
  default:
2239
1842
  return false;
2240
1843
  }
2241
1844
  }
2242
1845
  };
2243
1846
 
2244
- // src/pafi-backend/pafiBackendClient.ts
2245
- var DEFAULT_TIMEOUT_MS = 1e4;
2246
- var RETRY_DEFAULTS = {
2247
- maxAttempts: 1,
2248
- initialDelayMs: 500,
2249
- maxDelayMs: 1e4,
2250
- maxRetryAfterMs: 3e4
2251
- };
2252
- var PafiBackendClient = class {
2253
- url;
2254
- issuerId;
2255
- apiKey;
2256
- fetchImpl;
2257
- timeoutMs;
2258
- retry;
2259
- constructor(config) {
2260
- if (!config.url) {
2261
- throw new Error("PafiBackendClient: url is required");
2262
- }
2263
- if (!config.issuerId) {
2264
- throw new Error("PafiBackendClient: issuerId is required");
2265
- }
2266
- if (!config.apiKey) {
2267
- throw new Error("PafiBackendClient: apiKey is required");
2268
- }
2269
- this.url = config.url.replace(/\/+$/, "");
2270
- this.issuerId = config.issuerId;
2271
- this.apiKey = config.apiKey;
2272
- this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
2273
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2274
- this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
2275
- if (!this.fetchImpl) {
2276
- throw new Error(
2277
- "PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
2278
- );
2279
- }
2280
- if (this.retry.maxAttempts < 1) {
2281
- throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
2282
- }
2283
- }
2284
- /**
2285
- * Request paymaster sponsorship for a pre-built UserOperation.
2286
- * See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
2287
- *
2288
- * Retries automatically on transient failures (5xx, timeouts, network
2289
- * errors, and errors the server flags with `safeToRetry: true`) up to
2290
- * `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
2291
- *
2292
- * @throws PafiBackendError on final failure after exhausting retries
2293
- */
2294
- async requestSponsorship(req) {
2295
- return this.postWithRetry(
2296
- "/paymaster/sponsor",
2297
- req
2298
- );
2299
- }
2300
- // -------------------------------------------------------------------------
2301
- // Internals
2302
- // -------------------------------------------------------------------------
2303
- async postWithRetry(path, body) {
2304
- let lastError;
2305
- for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
2306
- try {
2307
- return await this.post(path, body);
2308
- } catch (err) {
2309
- if (!(err instanceof PafiBackendError)) throw err;
2310
- lastError = err;
2311
- const isLastAttempt = attempt >= this.retry.maxAttempts;
2312
- if (isLastAttempt || !err.safeToRetry) throw err;
2313
- const delay = this.computeBackoff(attempt, err.retryAfter);
2314
- if (delay === null) throw err;
2315
- await this.sleep(delay);
2316
- }
2317
- }
2318
- throw lastError;
2319
- }
2320
- /**
2321
- * Pick the delay before the next retry.
2322
- * - If the server sent `retryAfter` (seconds), honor it (capped by
2323
- * `maxRetryAfterMs`) — returns null if the server wait exceeds the
2324
- * cap, signalling the caller should give up.
2325
- * - Otherwise: exponential backoff with ±20% jitter, capped at
2326
- * `maxDelayMs`.
2327
- */
2328
- computeBackoff(attempt, retryAfter) {
2329
- if (retryAfter !== void 0) {
2330
- const serverMs = retryAfter * 1e3;
2331
- if (serverMs > this.retry.maxRetryAfterMs) return null;
2332
- return serverMs;
2333
- }
2334
- const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
2335
- const capped = Math.min(exp, this.retry.maxDelayMs);
2336
- const jitter = capped * (0.8 + Math.random() * 0.4);
2337
- return Math.round(jitter);
2338
- }
2339
- sleep(ms) {
2340
- return new Promise((resolve) => setTimeout(resolve, ms));
2341
- }
2342
- async post(path, body) {
2343
- const controller = new AbortController();
2344
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2345
- let response;
2346
- try {
2347
- response = await this.fetchImpl(`${this.url}${path}`, {
2348
- method: "POST",
2349
- headers: {
2350
- "Content-Type": "application/json",
2351
- "Authorization": `Bearer ${this.apiKey}`,
2352
- "X-Issuer-Id": this.issuerId
2353
- },
2354
- body: JSON.stringify(body, this.bigintReplacer),
2355
- signal: controller.signal
2356
- });
2357
- } catch (err) {
2358
- if (err.name === "AbortError") {
2359
- throw new PafiBackendError(
2360
- "TIMEOUT",
2361
- `PAFI Backend request timed out after ${this.timeoutMs}ms`,
2362
- 0
2363
- );
2364
- }
2365
- throw new PafiBackendError(
2366
- "NETWORK_ERROR",
2367
- `PAFI Backend unreachable: ${err.message}`,
2368
- 0
2369
- );
2370
- } finally {
2371
- clearTimeout(timeoutId);
2372
- }
2373
- const text = await response.text();
2374
- if (!response.ok) {
2375
- let code = "INTERNAL_ERROR";
2376
- let message = text || response.statusText;
2377
- let details;
2378
- let retryAfter;
2379
- let serverSafeToRetry;
2380
- try {
2381
- const parsed = JSON.parse(text);
2382
- code = parsed.code ?? code;
2383
- message = parsed.message ?? message;
2384
- details = parsed.details;
2385
- if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
2386
- if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
2387
- } catch {
2388
- }
2389
- throw new PafiBackendError(code, message, response.status, details, {
2390
- ...retryAfter !== void 0 ? { retryAfter } : {},
2391
- ...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
2392
- });
2393
- }
2394
- return JSON.parse(text, this.bigintReviver);
2395
- }
2396
- /** JSON replacer that stringifies bigints. Paired with bigintReviver. */
2397
- bigintReplacer = (_key, value) => {
2398
- return typeof value === "bigint" ? value.toString() : value;
2399
- };
2400
- /**
2401
- * JSON reviver that coerces specific numeric-string fields back to
2402
- * bigint. The server must send these fields as decimal strings.
2403
- */
2404
- bigintReviver = (key, value) => {
2405
- if (typeof value === "string" && (key.endsWith("GasLimit") || key === "nonce" || key === "callGasLimit" || key === "verificationGasLimit" || key === "preVerificationGas" || key === "maxFeePerGas" || key === "maxPriorityFeePerGas" || key === "paymasterVerificationGasLimit" || key === "paymasterPostOpGasLimit") && /^\d+$/.test(value)) {
2406
- return BigInt(value);
2407
- }
2408
- return value;
2409
- };
2410
- };
2411
-
2412
1847
  // src/config.ts
2413
- import { getAddress as getAddress9 } from "viem";
1848
+ import { getAddress as getAddress8 } from "viem";
2414
1849
  function createIssuerService(config) {
2415
1850
  if (!config.provider) {
2416
1851
  throw new Error("createIssuerService: provider is required");
2417
1852
  }
2418
- if (!config.operatorWallet) {
2419
- throw new Error("createIssuerService: operatorWallet is required");
2420
- }
2421
- if (!config.signer) {
2422
- throw new Error("createIssuerService: signer is required");
2423
- }
2424
- if (!config.relayAddress) {
2425
- throw new Error("createIssuerService: relayAddress is required");
1853
+ if (!config.ledger) {
1854
+ throw new Error("createIssuerService: ledger is required");
2426
1855
  }
2427
1856
  if (!config.auth?.jwtSecret) {
2428
1857
  throw new Error("createIssuerService: auth.jwtSecret is required");
@@ -2436,8 +1865,8 @@ function createIssuerService(config) {
2436
1865
  "createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
2437
1866
  );
2438
1867
  }
2439
- const tokenAddresses = rawAddresses.map((a) => getAddress9(a));
2440
- const ledger = config.ledger ?? new MemoryPointLedger();
1868
+ const tokenAddresses = rawAddresses.map((a) => getAddress8(a));
1869
+ const ledger = config.ledger;
2441
1870
  const sessionStore = config.sessionStore ?? new MemorySessionStore();
2442
1871
  const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
2443
1872
  const authServiceConfig = {
@@ -2450,18 +1879,7 @@ function createIssuerService(config) {
2450
1879
  authServiceConfig.jwtExpiresIn = config.auth.jwtExpiresIn;
2451
1880
  }
2452
1881
  const authService = new AuthService(authServiceConfig);
2453
- const relayServiceConfig = {
2454
- relayAddress: config.relayAddress,
2455
- operatorWallet: config.operatorWallet,
2456
- provider: config.provider
2457
- };
2458
- if (config.relay?.simulateBeforeSubmit !== void 0) {
2459
- relayServiceConfig.simulateBeforeSubmit = config.relay.simulateBeforeSubmit;
2460
- }
2461
- if (config.relay?.confirmationTimeoutMs !== void 0) {
2462
- relayServiceConfig.confirmationTimeoutMs = config.relay.confirmationTimeoutMs;
2463
- }
2464
- const relayService = new RelayService(relayServiceConfig);
1882
+ const relayService = new RelayService();
2465
1883
  let feeManager;
2466
1884
  if (config.fee) {
2467
1885
  feeManager = new FeeManager({
@@ -2469,16 +1887,6 @@ function createIssuerService(config) {
2469
1887
  provider: config.provider
2470
1888
  });
2471
1889
  }
2472
- const gatewayConfig = {
2473
- ledger,
2474
- policy,
2475
- signer: config.signer,
2476
- relayService
2477
- };
2478
- if (config.gateway?.defaultLockBufferMs !== void 0) {
2479
- gatewayConfig.defaultLockBufferMs = config.gateway.defaultLockBufferMs;
2480
- }
2481
- const gateway = new MintingGateway(gatewayConfig);
2482
1890
  const indexers = /* @__PURE__ */ new Map();
2483
1891
  for (const tokenAddress of tokenAddresses) {
2484
1892
  const indexerConfig = {
@@ -2506,7 +1914,6 @@ function createIssuerService(config) {
2506
1914
  const firstIndexer = indexers.get(tokenAddresses[0]);
2507
1915
  const handlersConfig = {
2508
1916
  authService,
2509
- gateway,
2510
1917
  ledger,
2511
1918
  provider: config.provider,
2512
1919
  pointTokenAddresses: tokenAddresses,
@@ -2515,6 +1922,15 @@ function createIssuerService(config) {
2515
1922
  };
2516
1923
  if (feeManager) handlersConfig.feeManager = feeManager;
2517
1924
  if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
1925
+ if (config.claim) {
1926
+ handlersConfig.claim = {
1927
+ policy,
1928
+ relayService,
1929
+ issuerSignerWallet: config.claim.issuerSignerWallet,
1930
+ batchExecutorAddress: config.claim.batchExecutorAddress,
1931
+ lockDurationMs: config.claim.lockDurationMs
1932
+ };
1933
+ }
2518
1934
  const handlers = new IssuerApiHandlers(handlersConfig);
2519
1935
  if (config.indexer?.autoStart) {
2520
1936
  for (const idx of indexers.values()) {
@@ -2526,10 +1942,8 @@ function createIssuerService(config) {
2526
1942
  sessionStore,
2527
1943
  ledger,
2528
1944
  policy,
2529
- signer: config.signer,
2530
1945
  relayService,
2531
1946
  feeManager,
2532
- gateway,
2533
1947
  indexers,
2534
1948
  indexer: firstIndexer,
2535
1949
  handlers
@@ -2547,18 +1961,13 @@ export {
2547
1961
  FeeManager,
2548
1962
  InMemoryCursorStore,
2549
1963
  IssuerApiHandlers,
2550
- MemoryPointLedger,
2551
1964
  MemorySessionStore,
2552
- MintingGateway,
2553
- MintingGatewayError,
2554
1965
  NonceManager,
2555
1966
  PAFI_ISSUER_SDK_VERSION,
2556
1967
  PTRedeemError,
2557
1968
  PTRedeemHandler,
2558
- PafiBackendClient,
2559
1969
  PafiBackendError,
2560
1970
  PointIndexer,
2561
- PrivateKeySigner,
2562
1971
  RelayError,
2563
1972
  RelayService,
2564
1973
  TopUpRedemptionError,
@@ -2566,7 +1975,6 @@ export {
2566
1975
  authenticateRequest,
2567
1976
  createIssuerService,
2568
1977
  createSubgraphNativeUsdtQuoter,
2569
- createSubgraphPoolsProvider,
2570
- encodeExtData
1978
+ createSubgraphPoolsProvider
2571
1979
  };
2572
1980
  //# sourceMappingURL=index.js.map