@pafi-dev/issuer 0.5.27 → 0.5.29
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 +156 -1
- package/dist/index.cjs +45 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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.
|
|
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
|
@@ -2069,6 +2069,51 @@ var PafiBackendClient = class {
|
|
|
2069
2069
|
}
|
|
2070
2070
|
throw lastError;
|
|
2071
2071
|
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Fetch ERC-4337 UserOp receipt via PAFI's authenticated bundler proxy.
|
|
2074
|
+
* Returns `null` when the bundler hasn't seen the userOp yet — caller
|
|
2075
|
+
* should keep polling. Used by status endpoints to short-circuit the
|
|
2076
|
+
* on-chain indexer when several PENDING locks share the same amount.
|
|
2077
|
+
*/
|
|
2078
|
+
async getUserOpReceipt(userOpHash) {
|
|
2079
|
+
const fetchFn = this.config.fetchImpl ?? fetch;
|
|
2080
|
+
const url = `${this.config.url}/bundler/receipt`;
|
|
2081
|
+
let response;
|
|
2082
|
+
try {
|
|
2083
|
+
response = await fetchFn(url, {
|
|
2084
|
+
method: "POST",
|
|
2085
|
+
headers: {
|
|
2086
|
+
"Content-Type": "application/json",
|
|
2087
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
2088
|
+
"X-Issuer-Id": this.config.issuerId
|
|
2089
|
+
},
|
|
2090
|
+
body: JSON.stringify({ userOpHash })
|
|
2091
|
+
});
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
throw new PafiBackendError(
|
|
2094
|
+
"NETWORK_ERROR",
|
|
2095
|
+
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
2096
|
+
0
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
const text = await response.text();
|
|
2100
|
+
let json = {};
|
|
2101
|
+
try {
|
|
2102
|
+
json = JSON.parse(text);
|
|
2103
|
+
} catch {
|
|
2104
|
+
}
|
|
2105
|
+
if (!response.ok) {
|
|
2106
|
+
const code = json.code ?? "INTERNAL_ERROR";
|
|
2107
|
+
const message = json.message ?? `HTTP ${response.status}`;
|
|
2108
|
+
throw new PafiBackendError(code, message, response.status, json);
|
|
2109
|
+
}
|
|
2110
|
+
if (json.pending) return null;
|
|
2111
|
+
return {
|
|
2112
|
+
success: json.success,
|
|
2113
|
+
txHash: json.txHash,
|
|
2114
|
+
blockNumber: json.blockNumber
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2072
2117
|
async relayUserOperation(request) {
|
|
2073
2118
|
const fetchFn = this.config.fetchImpl ?? fetch;
|
|
2074
2119
|
const url = `${this.config.url}/bundler/relay`;
|