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