@pafi-dev/issuer 0.5.28 → 0.5.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,13 @@ ledger, policy engine, EIP-712 issuer signing, relay submission, and mint/burn e
15
15
 
16
16
  - Node.js >= 18
17
17
  - TypeScript >= 5.0
18
- - `viem` ^2.0.0 and `@pafi-dev/core` ^0.4.0 (peer dependencies)
18
+ - `viem` ^2.0.0 and `@pafi-dev/core` ^0.5.16 (peer dependencies)
19
+
20
+ > **Latest:** `0.5.28` — adds bundler-receipt fallback for status polling
21
+ > and propagates Pimlico's re-estimated gas + bundler-required gas price
22
+ > through `SponsorshipResponse`. Required to fix `AA34` paymaster-sig
23
+ > errors and the "status stuck PENDING" indexer race. See
24
+ > [Changelog](#changelog).
19
25
 
20
26
  ---
21
27
 
@@ -240,13 +246,91 @@ const sponsorship = await pafiClient.requestSponsorship({
240
246
  },
241
247
  });
242
248
 
249
+ // Paymaster fields
243
250
  // sponsorship.paymaster
244
251
  // sponsorship.paymasterData
245
252
  // sponsorship.paymasterVerificationGasLimit
246
253
  // sponsorship.paymasterPostOpGasLimit
254
+ // Pimlico's re-estimated values — MUST overwrite the userOp's matching
255
+ // fields BEFORE computing the userOpHash, or both AA24 (sender sig) and
256
+ // AA34 (paymaster sig) fire on the bundler. See "Critical: apply gas
257
+ // overrides" below.
258
+ // sponsorship.callGasLimit?
259
+ // sponsorship.verificationGasLimit?
260
+ // sponsorship.preVerificationGas?
261
+ // sponsorship.maxFeePerGas? // bundler-required floor
262
+ // sponsorship.maxPriorityFeePerGas?
247
263
  // sponsorship.expiresAt
248
264
  ```
249
265
 
266
+ ### Critical: apply gas overrides before computing the userOpHash
267
+
268
+ Pimlico's `pm_sponsorUserOperation` does two things the SDK didn't
269
+ previously echo back:
270
+
271
+ 1. **Re-estimates** `callGasLimit` / `verificationGasLimit` /
272
+ `preVerificationGas`. The paymaster signature signs over these new
273
+ values.
274
+ 2. **Quotes the bundler-required gas price** — `eth_feeHistory` on Base
275
+ regularly underestimates the bundler floor by 10–15 %, producing
276
+ `maxFeePerGas must be at least …` rejections.
277
+
278
+ Both sets of values are now returned on `SponsorshipResponse`. Apply
279
+ them to the userOp **before** calling `computeUserOpHash`:
280
+
281
+ ```ts
282
+ function applyOverrides<T extends Record<string, unknown>>(
283
+ userOp: T,
284
+ fields: Awaited<ReturnType<typeof pafiClient.requestSponsorship>> | undefined,
285
+ ): T {
286
+ if (!fields) return userOp;
287
+ const merged: Record<string, unknown> = { ...userOp };
288
+ for (const [k, v] of Object.entries(fields)) if (v !== undefined) merged[k] = v;
289
+ return merged as T;
290
+ }
291
+
292
+ const fields = await pafiClient.requestSponsorship({ /* ... */ });
293
+ const final = applyOverrides(userOp, fields);
294
+ const userOpHash = computeUserOpHash(final, chainId); // matches what the bundler will hash
295
+ ```
296
+
297
+ ### Bundler receipt fallback for status polling
298
+
299
+ `PointIndexer.deductBalance` matches `(user, token, amount, oldest
300
+ PENDING)`. When several PENDING locks share the same amount, the
301
+ indexer can resolve a *different* lock than the one the user is
302
+ currently polling — symptom: `/claim/status` returns PENDING forever
303
+ even though the on-chain mint succeeded.
304
+
305
+ `PafiBackendClient.getUserOpReceipt(userOpHash)` proxies
306
+ `eth_getUserOperationReceipt` through PAFI's authenticated `/bundler/receipt`
307
+ endpoint, letting status handlers short-circuit the indexer:
308
+
309
+ ```ts
310
+ import { PafiBackendClient } from "@pafi-dev/issuer";
311
+
312
+ // Inside POST /claim/submit, persist the bundler-returned userOpHash on
313
+ // the lock (add a `user_op_hash` column to your locked_mint_requests).
314
+ await ledger.bindMintUserOpHash(lockId, result.userOpHash);
315
+
316
+ // Inside GET /claim/status/:lockId
317
+ const lock = await ledger.getMintLock(lockId);
318
+ if (lock.status === "PENDING" && lock.userOpHash) {
319
+ const receipt = await pafiClient.getUserOpReceipt(lock.userOpHash);
320
+ if (receipt) {
321
+ const status = receipt.success ? "MINTED" : "FAILED";
322
+ await ledger.updateMintStatus(lock.id, status, receipt.txHash);
323
+ return { ...lock, status, txHash: receipt.txHash };
324
+ }
325
+ }
326
+ return lock; // bundler hasn't seen it yet — keep polling
327
+ ```
328
+
329
+ Returns `null` when the bundler hasn't confirmed yet (still in mempool /
330
+ not bundled). The indexer continues to do off-chain accounting in the
331
+ background; the receipt fallback only affects *user-visible status*,
332
+ not balance correctness.
333
+
250
334
  ---
251
335
 
252
336
  ## Error handling
@@ -302,6 +386,77 @@ try {
302
386
 
303
387
  ## Changelog
304
388
 
389
+ ### 0.5.28
390
+
391
+ `PafiBackendClient.getUserOpReceipt(userOpHash)` added.
392
+
393
+ **Why.** When several PENDING mint/redeem locks share the same amount,
394
+ `PointIndexer` / `BurnIndexer` resolve them by `(user, token, amount,
395
+ oldest PENDING)` — the userOp's actual bundler hash isn't part of the
396
+ match. A lock the user is *currently polling* can be left PENDING while
397
+ a *sibling* lock gets the on-chain `tx_hash`, so `/claim/status`
398
+ returns PENDING forever even though the mint succeeded.
399
+
400
+ The new method calls PAFI's authenticated bundler-receipt proxy and
401
+ returns `{ success, txHash, blockNumber } | null`. Status handlers can
402
+ look up the user's specific userOpHash and bypass the indexer race
403
+ entirely:
404
+
405
+ ```ts
406
+ const receipt = await pafiClient.getUserOpReceipt(lock.userOpHash);
407
+ if (receipt?.success) markMinted(lock.id, receipt.txHash);
408
+ ```
409
+
410
+ Pair this with a `user_op_hash` column on your locks table that you set
411
+ inside `/claim/submit` from the value returned by `relayUserOperation`.
412
+
413
+ ### 0.5.27
414
+
415
+ `SponsorshipResponse` now carries Pimlico's re-estimated gas + bundler
416
+ gas price.
417
+
418
+ **Why this fixes AA34 / AA24.** `pm_sponsorUserOperation` re-estimates
419
+ `callGasLimit` / `verificationGasLimit` / `preVerificationGas` AND
420
+ quotes the bundler-required `maxFeePerGas` / `maxPriorityFeePerGas`. The
421
+ paymaster signature is computed *over those new values*, not the values
422
+ the SDK originally proposed. If the caller submits the userOp with the
423
+ *original* gas fields:
424
+
425
+ - The bundler hashes the on-chain UserOp with the original values
426
+ - The paymaster signature recovers a different signer → `AA34
427
+ signature error`
428
+ - The user signature, computed over a different `userOpHash`, also
429
+ fails → `AA24 signature error` (whichever fires first depends on the
430
+ validation order)
431
+
432
+ The response now exposes:
433
+
434
+ ```ts
435
+ interface SponsorshipResponse {
436
+ paymaster: Address;
437
+ paymasterData: Hex;
438
+ paymasterVerificationGasLimit: bigint;
439
+ paymasterPostOpGasLimit: bigint;
440
+ callGasLimit?: bigint; // NEW — Pimlico re-estimate
441
+ verificationGasLimit?: bigint; // NEW
442
+ preVerificationGas?: bigint; // NEW
443
+ maxFeePerGas?: bigint; // NEW — bundler-required floor
444
+ maxPriorityFeePerGas?: bigint; // NEW
445
+ expiresAt: number;
446
+ }
447
+ ```
448
+
449
+ **Callers must merge these (skipping `undefined`) into the userOp
450
+ before computing `userOpHash`** — see the
451
+ "Critical: apply gas overrides" section above.
452
+
453
+ ### 0.5.26
454
+
455
+ `@pafi-dev/core` peer dependency bumped to `0.5.16` to pull in the v0.8
456
+ EIP-712 userOpHash + `buildUserOpTypedData()` helpers. This is what
457
+ makes the EIP-7702 mobile flow validate cleanly on Pimlico's
458
+ `Simple7702Account` (no AA24).
459
+
305
460
  ### 0.4.0
306
461
  - `PafiBackendClient` added — HTTP client for PAFI paymaster backend with retry logic and bigint serialization
307
462
  - `SponsorAuth` support — issuer signs EIP-712 authorization for FE to request paymaster sponsorship
package/dist/index.cjs CHANGED
@@ -2321,7 +2321,28 @@ function createIssuerService(config) {
2321
2321
 
2322
2322
  // src/userop-store/serialize.ts
2323
2323
  var import_core9 = require("@pafi-dev/core");
2324
- function serializeEntryToJsonRpc(entry, signature) {
2324
+ function serializeEntryToJsonRpc(entry, signature, variant = "sponsored") {
2325
+ if (variant === "fallback") {
2326
+ if (!entry.fallback) {
2327
+ throw new Error(
2328
+ "serializeEntryToJsonRpc: variant=fallback requested but the stored entry has no `fallback` branch \u2014 caller should resubmit with variant='sponsored' or re-prepare with a fee configured."
2329
+ );
2330
+ }
2331
+ return (0, import_core9.serializeUserOpToJsonRpc)(
2332
+ {
2333
+ sender: entry.sender,
2334
+ nonce: BigInt(entry.nonce),
2335
+ callData: entry.fallback.callData,
2336
+ callGasLimit: BigInt(entry.fallback.callGasLimit),
2337
+ verificationGasLimit: BigInt(entry.fallback.verificationGasLimit),
2338
+ preVerificationGas: BigInt(entry.fallback.preVerificationGas),
2339
+ maxFeePerGas: BigInt(entry.maxFeePerGas),
2340
+ maxPriorityFeePerGas: BigInt(entry.maxPriorityFeePerGas)
2341
+ // intentionally no paymaster — user pays ETH gas
2342
+ },
2343
+ signature
2344
+ );
2345
+ }
2325
2346
  return (0, import_core9.serializeUserOpToJsonRpc)(
2326
2347
  {
2327
2348
  sender: entry.sender,