@pafi-dev/issuer 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4093 @@
1
+ import { PafiSdkError, SdkErrorHttpStatus, PointTokenDomainConfig, PartialUserOperation, BurnRequest, PoolKey, RedemptionPolicy, RedemptionPolicySource, RedemptionPreview, RedemptionDecision, RedemptionDenialCode, UserOpTypedData, decodeBatchExecuteCalls, BROKER_HASHES, PafiErrorType, BuiltSponsorAuth, ENTRY_POINT_V08 } from '@pafi-dev/core';
2
+ export { PAFI_SUBGRAPH_URL, PafiErrorType, PafiSdkError, SDK_ERROR_HTTP_STATUS_CODE, SdkErrorHttpStatus, ValidationError, defaultErrorTypeForStatus } from '@pafi-dev/core';
3
+ export { GenericHttpExceptionDescriptor, NormalizeContext, PafiErrorEnvelope, PafiErrorPayload, buildErrorEnvelope, payloadFromGenericError, payloadFromHttpException, payloadFromPafiSdkError } from './http/index.cjs';
4
+ import { Address, Hex, PublicClient, WalletClient } from 'viem';
5
+
6
+ /**
7
+ * `PafiSdkError` + `SdkErrorHttpStatus` are exported from
8
+ * `@pafi-dev/core/errors` so core-level errors (e.g. `OracleStaleError`)
9
+ * can extend the same base. Issuer re-exports the canonical types here
10
+ * for back-compat — `instanceof PafiSdkError` from EITHER package
11
+ * catches errors thrown from EITHER package.
12
+ */
13
+
14
+ /**
15
+ * Issuer wired the SDK without a dependency the requested endpoint
16
+ * needs (e.g. `/gas-fee` called but `feeManager` not configured;
17
+ * `/pools` called but `poolsProvider` not configured). 503 because
18
+ * the endpoint genuinely can't serve the request — caller's payload
19
+ * is fine, the issuer's deployment is incomplete.
20
+ */
21
+ declare class ConfigurationError extends PafiSdkError {
22
+ readonly httpStatus: "service_unavailable";
23
+ readonly code: string;
24
+ readonly details?: Record<string, unknown>;
25
+ constructor(code: string, message: string, details?: Record<string, unknown>);
26
+ }
27
+
28
+ /**
29
+ * Lifecycle of a minting request as tracked by the issuer's point ledger.
30
+ *
31
+ * PENDING ── on-chain mint confirmed ──▶ MINTED
32
+ * │
33
+ * ├── deadline elapsed without mint ─▶ EXPIRED
34
+ * │
35
+ * └── tx reverted / cancelled ──────▶ FAILED
36
+ */
37
+ type MintingStatus = "PENDING" | "MINTED" | "EXPIRED" | "FAILED";
38
+ /** A locked-amount entry tracking an in-flight mint request. */
39
+ interface LockedMintRequest {
40
+ /** Opaque lock id (used to release / update later) */
41
+ lockId: string;
42
+ userAddress: Address;
43
+ amount: bigint;
44
+ /**
45
+ * Which PointToken this lock belongs to. Added in 0.2.0 for multi-token
46
+ * issuer support. Optional for backward compat with 0.1.x ledgers —
47
+ * single-token consumers can ignore it.
48
+ */
49
+ tokenAddress?: Address;
50
+ /** Lifecycle status */
51
+ status: MintingStatus;
52
+ /** When the lock was created (epoch ms) */
53
+ createdAt: number;
54
+ /** When the lock auto-expires if not resolved (epoch ms) */
55
+ expiresAt: number;
56
+ /** On-chain transaction hash, set once the mint is confirmed */
57
+ txHash?: Hex;
58
+ /**
59
+ * ERC-4337 userOpHash returned by the bundler at submit time. Bound
60
+ * to the lock by `bindMintUserOpHash` so status endpoints can poll
61
+ * the bundler receipt directly — bypasses `PointIndexer`'s
62
+ * amount-based matching, which races when several PENDING locks
63
+ * share the same amount.
64
+ */
65
+ userOpHash?: Hex;
66
+ }
67
+ /** A pending off-chain credit (burn → credit reverse flow). */
68
+ interface PendingCredit {
69
+ lockId: string;
70
+ userAddress: Address;
71
+ amount: bigint;
72
+ tokenAddress?: Address;
73
+ status: "PENDING" | "RESOLVED" | "EXPIRED";
74
+ createdAt: number;
75
+ expiresAt: number;
76
+ txHash?: Hex;
77
+ resolvedAt?: number;
78
+ /** Bundler-returned userOpHash. See `LockedMintRequest.userOpHash`. */
79
+ userOpHash?: Hex;
80
+ }
81
+ /**
82
+ * Issuer point ledger interface — the source of truth for off-chain user
83
+ * point balances and in-flight minting requests.
84
+ *
85
+ * Issuers replace the in-memory default with their own database-backed
86
+ * implementation (Postgres, Redis, etc.).
87
+ *
88
+ * **Multi-token support (0.2.0):**
89
+ * Every mutating method accepts an optional `tokenAddress` parameter so
90
+ * balances can be scoped per `(user, token)`. Single-token issuers can
91
+ * ignore the parameter entirely — legacy 0.1.x implementations remain
92
+ * compatible. Multi-token issuers must persist + query balances keyed by
93
+ * `(userAddress, tokenAddress)`.
94
+ */
95
+ interface IPointLedger {
96
+ /** Get a user's available off-chain point balance (excluding locked). */
97
+ getBalance(userAddress: Address, tokenAddress?: Address): Promise<bigint>;
98
+ /**
99
+ * Lock an amount for a pending mint request. Locked amounts are reserved
100
+ * but not yet deducted; they protect against double-spend during the
101
+ * EIP-712 validity window.
102
+ *
103
+ * @param lockDurationMs how long the lock should be held before auto-expiry
104
+ * @param tokenAddress which PointToken this lock is for (0.2.0+)
105
+ * @returns lockId — opaque handle used by `releaseLock` / `updateMintStatus`
106
+ * @throws if the user's available balance is below `amount`
107
+ */
108
+ lockForMinting(userAddress: Address, amount: bigint, lockDurationMs: number, tokenAddress?: Address): Promise<string>;
109
+ /** Release a previously created lock (e.g. on tx failure / cancel). */
110
+ releaseLock(lockId: string): Promise<void>;
111
+ /** Deduct balance after a confirmed on-chain mint. Idempotent on `txHash`. */
112
+ deductBalance(userAddress: Address, amount: bigint, txHash: Hex, tokenAddress?: Address): Promise<void>;
113
+ /** Credit points to a user's balance (e.g. from merchant activity). */
114
+ creditBalance(userAddress: Address, amount: bigint, reason: string, tokenAddress?: Address): Promise<void>;
115
+ /** List currently-pending locked mint requests for a user. */
116
+ getLockedRequests(userAddress: Address, tokenAddress?: Address): Promise<LockedMintRequest[]>;
117
+ /**
118
+ * Transition a lock from `PENDING` to a terminal status
119
+ * (`MINTED` | `EXPIRED` | `FAILED`). The on-chain tx hash is
120
+ * supplied when the status is `MINTED`.
121
+ *
122
+ * Terminal states are immutable: once a lock leaves `PENDING` the
123
+ * status is fixed. Calling `updateMintStatus` on a non-PENDING lock
124
+ * is a silent no-op. This protects against late-arriving writes from
125
+ * different code paths (e.g. PointIndexer.finalize and the bundler-
126
+ * receipt fallback in statusHandlers) racing to overwrite each
127
+ * other — particularly the case where a stale `EXPIRED`/`FAILED`
128
+ * write would corrupt a `MINTED` lock whose balance has already
129
+ * been deducted.
130
+ */
131
+ updateMintStatus(lockId: string, status: MintingStatus, txHash?: Hex): Promise<void>;
132
+ /**
133
+ * Reserve a pending off-chain credit before the burn tx is submitted.
134
+ *
135
+ * Returns a lockId that the burn indexer uses to correlate the
136
+ * on-chain burn event back to this credit request.
137
+ *
138
+ * Throws if the ledger doesn't support the reverse flow (legacy
139
+ * implementations) — callers should catch and fall back.
140
+ */
141
+ reservePendingCredit?(userAddress: Address, amount: bigint, durationMs: number, tokenAddress?: Address): Promise<string>;
142
+ /**
143
+ * Finalize a reserved credit when the on-chain Burn event is seen by
144
+ * the BurnIndexer. Idempotent — safe to call multiple times with the
145
+ * same txHash (no double-credit).
146
+ *
147
+ * Throws if the lockId is unknown or already resolved.
148
+ */
149
+ resolveCreditByBurnTx?(lockId: string, txHash: Hex): Promise<void>;
150
+ /**
151
+ * Persist the bundler-returned userOpHash for a mint lock at
152
+ * submit time. Called once per `/claim/submit` after the bundler
153
+ * accepts the UserOp.
154
+ */
155
+ bindMintUserOpHash?(lockId: string, userOpHash: Hex): Promise<void>;
156
+ /**
157
+ * Persist the bundler-returned userOpHash for a pending credit at
158
+ * `/redeem/submit` time.
159
+ */
160
+ bindCreditUserOpHash?(lockId: string, userOpHash: Hex): Promise<void>;
161
+ /**
162
+ * Look up a mint lock by id. Returns `null` if the lock doesn't
163
+ * exist or doesn't belong to the supplied user (when provided).
164
+ * Used by `handleClaimStatus` for status polling. OPTIONAL — when
165
+ * absent, callers must implement status endpoints themselves.
166
+ */
167
+ getMintLock?(lockId: string, userAddress?: Address): Promise<LockedMintRequest | null>;
168
+ /**
169
+ * Look up a pending credit by id. Symmetric counterpart of
170
+ * `getMintLock` for the burn/redeem flow.
171
+ */
172
+ getPendingCredit?(lockId: string, userAddress?: Address): Promise<PendingCredit | null>;
173
+ }
174
+
175
+ /**
176
+ * Input to a policy evaluation. Policy engines use this to decide whether
177
+ * a user's mint request should be approved.
178
+ */
179
+ interface PolicyEvalRequest {
180
+ userAddress: Address;
181
+ amount: bigint;
182
+ pointTokenAddress: Address;
183
+ chainId: number;
184
+ }
185
+ /** Outcome of a policy evaluation. */
186
+ interface PolicyDecision {
187
+ approved: boolean;
188
+ /**
189
+ * Human-readable reason for rejection (empty on approval). Callers surface
190
+ * this back to the user, so keep it short and non-leaky.
191
+ */
192
+ reason?: string;
193
+ }
194
+ /**
195
+ * Policy engine — evaluates whether a minting request passes issuer rules.
196
+ * Issuers extend the default implementation to add KYC, volume caps, claim
197
+ * budgets, etc.
198
+ */
199
+ interface IPolicyEngine {
200
+ evaluate(request: PolicyEvalRequest): Promise<PolicyDecision>;
201
+ }
202
+
203
+ /**
204
+ * Options for constructing a DefaultPolicyEngine.
205
+ *
206
+ * `mintingOracleAddress` and `provider` are optional — if omitted, the
207
+ * engine skips the on-chain cap check (useful for dev/testing).
208
+ *
209
+ * `verifyMintCap` is injectable so tests can simulate cap rejections
210
+ * without needing a real contract.
211
+ */
212
+ interface DefaultPolicyEngineOptions {
213
+ ledger: IPointLedger;
214
+ provider?: PublicClient;
215
+ mintingOracleAddress?: Address;
216
+ /**
217
+ * Override the on-chain cap check. Defaults to `@pafi-dev/core`'s
218
+ * `verifyMintCap`, which reverts if the issuer's declared supply would
219
+ * be exceeded. A rejected check should throw; returning normally is pass.
220
+ */
221
+ verifyMintCap?: (client: PublicClient, oracle: Address, issuer: Address, amount: bigint) => Promise<void>;
222
+ /**
223
+ * Resolve a point-token address to the issuer address for the cap check.
224
+ * Required iff `mintingOracleAddress + provider` are supplied.
225
+ */
226
+ resolveIssuer?: (pointToken: Address) => Promise<Address>;
227
+ }
228
+ /**
229
+ * Default policy engine — performs two checks:
230
+ *
231
+ * 1. **Off-chain balance** — the user must have at least `amount` of
232
+ * unlocked point balance in the issuer ledger.
233
+ * 2. **On-chain cap** — (optional) calls the MintingOracle via
234
+ * `verifyMintCap` to confirm that minting this amount would not exceed
235
+ * the issuer's declared total supply.
236
+ *
237
+ * Issuers extend this class (or implement `IPolicyEngine` directly) to add
238
+ * KYC, volume caps, claim budgets, or anti-abuse rules.
239
+ */
240
+ declare class DefaultPolicyEngine implements IPolicyEngine {
241
+ private readonly ledger;
242
+ private readonly provider?;
243
+ private readonly mintingOracleAddress?;
244
+ private readonly verifyMintCap?;
245
+ private readonly resolveIssuer?;
246
+ constructor(opts: DefaultPolicyEngineOptions);
247
+ evaluate(request: PolicyEvalRequest): Promise<PolicyDecision>;
248
+ }
249
+
250
+ /**
251
+ * A server-issued session created after a successful wallet login. The
252
+ * token id is embedded in the JWT so sessions can be revoked without
253
+ * invalidating the JWT signing key.
254
+ */
255
+ interface Session {
256
+ tokenId: string;
257
+ userAddress: Address;
258
+ chainId: number;
259
+ issuedAt: Date;
260
+ expiresAt: Date;
261
+ }
262
+ /**
263
+ * Backend store for login nonces and active sessions.
264
+ *
265
+ * The default in-memory implementation is fine for local development but
266
+ * production issuers should plug in Redis / a SQL table so sessions
267
+ * survive restarts and are visible across replicas.
268
+ */
269
+ interface ISessionStore {
270
+ /**
271
+ * Create a single-use login nonce and return it to the caller. Consumers
272
+ * must call {@link consumeNonce} exactly once when the client returns a
273
+ * signed login message quoting this nonce.
274
+ */
275
+ createNonce(): Promise<string>;
276
+ /**
277
+ * Atomically validate + consume a login nonce. Returns `true` if the
278
+ * nonce was known and unexpired (and is now removed); `false` otherwise.
279
+ */
280
+ consumeNonce(nonce: string): Promise<boolean>;
281
+ /** Persist a newly issued session. */
282
+ createSession(session: Session): Promise<void>;
283
+ /** Look up a session by token id, or `null` if unknown / revoked / expired. */
284
+ getSession(tokenId: string): Promise<Session | null>;
285
+ /** Revoke a session by token id (logout). No-op if not found. */
286
+ revokeSession(tokenId: string): Promise<void>;
287
+ /**
288
+ * Revoke every session for a user address. Used when an issuer needs to
289
+ * force re-login (e.g. after a permissions change or suspected abuse).
290
+ */
291
+ revokeAllSessions(userAddress: Address): Promise<void>;
292
+ }
293
+
294
+ interface MemorySessionStoreOptions {
295
+ /**
296
+ * How long a login nonce is valid before auto-expiry. Defaults to 5 min,
297
+ * matching typical SIWE recommendations.
298
+ */
299
+ nonceTtlMs?: number;
300
+ /** Clock override for tests. */
301
+ now?: () => number;
302
+ /**
303
+ * Bypass the production-safety guard. By default, instantiating
304
+ * `MemorySessionStore` when `process.env.NODE_ENV === "production"`
305
+ * THROWS — multi-pod K8s deploys do not share Map state, so sessions
306
+ * are not revocable across replicas, and tokens remain valid on pod
307
+ * B after `logout` on pod A. Only set this `true` for single-pod
308
+ * deployments where you've consciously accepted the trade-off.
309
+ */
310
+ dangerouslyAllowMemoryStoreInProduction?: boolean;
311
+ }
312
+ /**
313
+ * In-memory `ISessionStore` — Map-backed nonces and sessions with lazy
314
+ * expiry sweeps on every read/write.
315
+ *
316
+ * Not safe across processes or restarts; intended for dev/test only.
317
+ */
318
+ declare class MemorySessionStore implements ISessionStore {
319
+ private nonces;
320
+ private sessions;
321
+ private readonly nonceTtlMs;
322
+ private readonly now;
323
+ constructor(opts?: MemorySessionStoreOptions);
324
+ createNonce(): Promise<string>;
325
+ consumeNonce(nonce: string): Promise<boolean>;
326
+ createSession(session: Session): Promise<void>;
327
+ getSession(tokenId: string): Promise<Session | null>;
328
+ revokeSession(tokenId: string): Promise<void>;
329
+ revokeAllSessions(userAddress: Address): Promise<void>;
330
+ private purgeExpiredNonces;
331
+ private purgeExpiredSessions;
332
+ }
333
+
334
+ /**
335
+ * Thin wrapper around an `ISessionStore` exposing nonce generation and
336
+ * single-use consumption as its own named surface. AuthService uses it
337
+ * internally, and issuers can also use it directly for other flows that
338
+ * need replay protection (e.g. a CSRF-style challenge).
339
+ */
340
+ declare class NonceManager {
341
+ private readonly store;
342
+ constructor(store: ISessionStore);
343
+ /** Generate a fresh login nonce. The store is responsible for TTL. */
344
+ generate(): Promise<string>;
345
+ /**
346
+ * Atomically validate + consume a nonce. Returns `true` iff the nonce
347
+ * was known and unexpired (and is now removed from the store).
348
+ */
349
+ consume(nonce: string): Promise<boolean>;
350
+ }
351
+
352
+ interface AuthServiceConfig {
353
+ sessionStore: ISessionStore;
354
+ /** HMAC secret for signing JWTs (HS256). At least 32 bytes recommended. */
355
+ jwtSecret: string;
356
+ /**
357
+ * JWT lifetime passed to `jose`. Accepts any of `jose`'s time strings
358
+ * (`"24h"`, `"7d"`, `"45m"`, …). Defaults to `"24h"`.
359
+ */
360
+ jwtExpiresIn?: string;
361
+ /**
362
+ * Expected domain in the login message, e.g. `"app.example.com"`. The
363
+ * SIWE spec requires the frontend to build the message with this exact
364
+ * domain; the backend rejects any mismatch.
365
+ */
366
+ domain: string;
367
+ /** Expected chain id. */
368
+ chainId: number;
369
+ /**
370
+ * Optional `iss` (issuer) claim baked into JWTs and validated on
371
+ * verify. Recommended for multi-issuer deployments to prevent
372
+ * cross-validation if a JWT secret is accidentally shared. Examples:
373
+ * `"gg56-issuer"`, `"issuer.gg56.com"`. When unset, no `iss` is
374
+ * written or required (back-compat).
375
+ */
376
+ issuer?: string;
377
+ /**
378
+ * Optional `aud` (audience) claim baked into JWTs and validated on
379
+ * verify. Recommended to bind tokens to a specific consumer, e.g.
380
+ * `"gg56-mobile"` or `"gg56-web"`. When unset, no `aud` is written
381
+ * or required (back-compat).
382
+ */
383
+ audience?: string;
384
+ /** Clock override for tests. */
385
+ now?: () => Date;
386
+ }
387
+ interface LoginResult {
388
+ token: string;
389
+ userAddress: Address;
390
+ tokenId: string;
391
+ expiresAt: Date;
392
+ }
393
+ interface AuthContext {
394
+ userAddress: Address;
395
+ chainId: number;
396
+ tokenId: string;
397
+ }
398
+ /**
399
+ * Wallet-based authentication service implementing the EIP-4361 (Sign-In
400
+ * With Ethereum) login flow with server-issued JWTs.
401
+ *
402
+ * Flow:
403
+ * 1. `getNonce()` — client fetches a nonce, stores it locally
404
+ * 2. Client constructs a login message with the nonce + signs with wallet
405
+ * 3. `login(message, signature)` — server validates fields + signature,
406
+ * consumes the nonce, persists a session, returns a JWT
407
+ * 4. Protected endpoints call `verifyToken(token)` to resolve the user
408
+ * context; the session is re-checked on every call, so `logout()`
409
+ * revokes access immediately.
410
+ */
411
+ declare class AuthService {
412
+ private readonly sessionStore;
413
+ private readonly jwtSecret;
414
+ private readonly jwtExpiresIn;
415
+ private readonly domain;
416
+ private readonly chainId;
417
+ private readonly issuer?;
418
+ private readonly audience?;
419
+ private readonly nonceManager;
420
+ private readonly now;
421
+ constructor(config: AuthServiceConfig);
422
+ /** Generate a fresh login nonce. */
423
+ getNonce(): Promise<string>;
424
+ /**
425
+ * Verify a signed login message and issue a JWT on success.
426
+ * Throws an `AuthError` on any validation failure.
427
+ */
428
+ login(message: string, signature: Hex): Promise<LoginResult>;
429
+ /** Revoke the session backing the given JWT (logout). */
430
+ logout(token: string): Promise<void>;
431
+ private logSessionStoreError;
432
+ /**
433
+ * Verify a JWT and return the authenticated user context. Throws an
434
+ * `AuthError` if the token is missing, malformed, expired, revoked, or
435
+ * signed by a different key.
436
+ */
437
+ verifyToken(token: string): Promise<AuthContext>;
438
+ }
439
+
440
+ /**
441
+ * Extracts a Bearer token from an `Authorization` header value and verifies
442
+ * it via the supplied `AuthService`. Framework-agnostic — issuers wrap this
443
+ * in their framework's middleware pattern (Express, Fastify, Hono, …).
444
+ *
445
+ * Throws an `AuthError` if the header is missing, malformed, or the token
446
+ * fails verification. Callers should translate the `err.code` into an
447
+ * appropriate HTTP status (401 for auth failures, 500 for anything else).
448
+ */
449
+ declare function authenticateRequest(authHeader: string | undefined, authService: AuthService): Promise<AuthContext>;
450
+
451
+ /**
452
+ * Error codes surfaced by the auth subsystem. Issuers can pattern-match on
453
+ * `err.code` to translate into framework-specific HTTP responses.
454
+ */
455
+ type AuthErrorCode = "INVALID_MESSAGE" | "DOMAIN_MISMATCH" | "CHAIN_MISMATCH" | "NONCE_INVALID" | "MESSAGE_EXPIRED" | "MESSAGE_NOT_YET_VALID" | "SIGNATURE_INVALID" | "MISSING_TOKEN" | "MALFORMED_TOKEN" | "TOKEN_INVALID" | "TOKEN_EXPIRED" | "SESSION_REVOKED";
456
+ /**
457
+ * Extends `PafiSdkError` so issuer controllers can route auth failures
458
+ * through the same `createSdkErrorMapper` as everything else (no
459
+ * separate `instanceof AuthError` check needed).
460
+ */
461
+ declare class AuthError extends PafiSdkError {
462
+ readonly code: AuthErrorCode;
463
+ readonly httpStatus: SdkErrorHttpStatus;
464
+ constructor(code: AuthErrorCode, message: string);
465
+ }
466
+
467
+ /**
468
+ * Per-key rate limiting for unauthenticated public endpoints.
469
+ *
470
+ * `/auth/nonce` and `/auth/login` are unauthenticated and CPU-bound
471
+ * (SIWE message parsing + ECDSA `recover`), so without rate limiting
472
+ * an attacker can saturate the issuer at low cost. Worse, on
473
+ * `/auth/nonce` they can flood the session store with single-use
474
+ * nonces; with `MemorySessionStore` this is a pure DoS.
475
+ *
476
+ * Issuers wire a `IRateLimiter` impl in `IssuerApiHandlersConfig` —
477
+ * `MemoryRateLimiter` is the default reference impl (single-pod, dev).
478
+ * Production deployments use a Redis-backed impl that shares state
479
+ * across replicas (similar pattern to `ISessionStore`).
480
+ *
481
+ * The "key" is up to the caller — typically the client IP, but the
482
+ * issuer's HTTP layer chooses (could be userAddress on POST /login).
483
+ */
484
+ interface IRateLimiter {
485
+ /**
486
+ * Check + increment a counter under `key`. Returns `{ allowed: false,
487
+ * retryAfterMs }` when the caller has exceeded its quota for this
488
+ * window. Default config is action-specific (see
489
+ * `RateLimiterConfig`).
490
+ *
491
+ * Idempotent in the sense that successive calls with the same `key`
492
+ * within the same window count toward the limit; a separate window
493
+ * starts after `windowMs` elapses.
494
+ */
495
+ consume(key: string, action: RateLimitAction): Promise<{
496
+ allowed: boolean;
497
+ retryAfterMs?: number;
498
+ }>;
499
+ }
500
+ type RateLimitAction = "auth_nonce" | "auth_login";
501
+ interface RateLimiterConfig {
502
+ /**
503
+ * Per-key max requests per `windowMs`. Defaults:
504
+ * - auth_nonce: 1 per 2s (30/min)
505
+ * - auth_login: 5 per minute
506
+ *
507
+ * Issuers can tune per their threat model.
508
+ */
509
+ limits?: Partial<Record<RateLimitAction, {
510
+ max: number;
511
+ windowMs: number;
512
+ }>>;
513
+ /** Clock override for tests. */
514
+ now?: () => number;
515
+ }
516
+ /**
517
+ * In-memory `IRateLimiter` — Map of `{key, action}` → count + window
518
+ * start. Lazy expiry on read. Single-process: NOT safe for multi-pod
519
+ * deployments. Use a Redis impl in production.
520
+ */
521
+ declare class MemoryRateLimiter implements IRateLimiter {
522
+ private buckets;
523
+ private readonly limits;
524
+ private readonly now;
525
+ constructor(config?: RateLimiterConfig);
526
+ consume(key: string, action: RateLimitAction): Promise<{
527
+ allowed: boolean;
528
+ retryAfterMs?: number;
529
+ }>;
530
+ /**
531
+ * Test helper — clear all buckets. Not part of `IRateLimiter`; only
532
+ * exposed on the in-memory impl for unit tests.
533
+ */
534
+ reset(): void;
535
+ }
536
+ /**
537
+ * Convenience: a no-op `IRateLimiter` that lets every request through.
538
+ * Useful as a default in `IssuerApiHandlersConfig` when the issuer
539
+ * hasn't wired a rate limiter yet (back-compat) — emits a one-time
540
+ * warning at construction so consumers notice.
541
+ */
542
+ declare class NoopRateLimiter implements IRateLimiter {
543
+ private warned;
544
+ consume(): Promise<{
545
+ allowed: boolean;
546
+ }>;
547
+ }
548
+
549
+ type RelayErrorCode = "ENCODE_FAILED";
550
+ declare class RelayError extends PafiSdkError {
551
+ readonly httpStatus: "unprocessable";
552
+ readonly code: RelayErrorCode;
553
+ constructor(code: RelayErrorCode, message: string, cause?: unknown);
554
+ }
555
+
556
+ /**
557
+ * Optional config that lets `RelayService` auto-quote the operator fee
558
+ * + auto-resolve the recipient when callers don't pass `feeAmount` /
559
+ * `feeRecipient`. When both `provider` + `chainId` are set, callers can
560
+ * omit the fee params entirely; the service will:
561
+ *
562
+ * 1. Resolve `feeRecipient = getContractAddresses(chainId).pafiFeeRecipient`
563
+ * 2. If `feeAmount` is undefined → run `quoteOperatorFeePt(...)` against
564
+ * Chainlink + PAFI subgraph to compute the PT amount.
565
+ *
566
+ * When unset, the legacy "caller passes feeAmount + feeRecipient or no
567
+ * fee" behavior applies, so existing integrations keep working.
568
+ */
569
+ interface RelayServiceConfig {
570
+ provider?: PublicClient;
571
+ chainId?: number;
572
+ }
573
+ /**
574
+ * Builds unsigned `PartialUserOperation` payloads for the sponsored
575
+ * flow. The service is stateless and HTTP-client-free:
576
+ *
577
+ * - `prepareMint` signs a `MintRequest` EIP-712 with the caller-supplied
578
+ * issuer signer wallet, then encodes `PointToken.mint(to, amount,
579
+ * deadline, minterSig)` into a UserOp the frontend submits via
580
+ * EIP-7702 + Paymaster.
581
+ * - `prepareBurn` mirrors the above on the burn side using a
582
+ * pre-signed `BurnRequest` + `PointToken.burn(from, amount,
583
+ * deadline, burnerSig)`.
584
+ *
585
+ * There is no broadcasting, no operator wallet, no simulation — those
586
+ * concerns live in the Bundler + Paymaster.
587
+ */
588
+ declare class RelayService {
589
+ private readonly provider;
590
+ private readonly chainId;
591
+ constructor(config?: RelayServiceConfig);
592
+ /**
593
+ * Resolve the fee recipient + amount applied to the next UserOp:
594
+ *
595
+ * - If caller passed an explicit `feeRecipient`, use it (testing
596
+ * only — sponsor-relayer's L1 will reject any non-canonical
597
+ * recipient with `INSUFFICIENT_FEE`). Otherwise, default to
598
+ * `getContractAddresses(chainId).pafiFeeRecipient` when the
599
+ * service has a `chainId` configured.
600
+ * - If caller passed `feeAmount`, use it. Otherwise, when the
601
+ * service has both `provider` + `chainId`, auto-quote via
602
+ * `quoteOperatorFeePt`.
603
+ * - When the service is unconfigured AND caller passed nothing,
604
+ * return `{ feeAmount: 0n, feeRecipient: undefined }` — legacy
605
+ * "no fee" behavior, caller must opt in for the gas-reimbursement
606
+ * transfer to be added to the batch.
607
+ */
608
+ private resolveFee;
609
+ /**
610
+ * Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
611
+ * `PointToken.mint(to, amount, deadline, minterSig)`.
612
+ */
613
+ prepareMint(params: PrepareMintParams): Promise<PartialUserOperation>;
614
+ /**
615
+ * Build an unsigned UserOp for Scenario 2 (Burn/Redeem) — sig-gated
616
+ * `PointToken.burn(from, amount, deadline, burnerSig)`. Caller
617
+ * provides a pre-signed `BurnRequest` + sig bytes (typically from
618
+ * `PTRedeemHandler`).
619
+ *
620
+ * Direct burn (no sig) is not used — every burn goes through the
621
+ * issuer-signed `BurnRequest` path.
622
+ */
623
+ prepareBurn(params: PrepareBurnParams): Promise<PartialUserOperation>;
624
+ /**
625
+ * Build a dummy `PartialUserOperation` for the mint scenario, suitable
626
+ * for `feeManager.estimateGasFee({ partialUserOp, ... })`. NO signing —
627
+ * uses a 65-byte zero signature placeholder.
628
+ */
629
+ previewMintUserOp(params: PreviewMintParams): PartialUserOperation;
630
+ /** Burn-side mirror of `previewMintUserOp`. */
631
+ previewBurnUserOp(params: PreviewBurnParams): PartialUserOperation;
632
+ }
633
+ /**
634
+ * Inputs for `previewMintUserOp` — strict subset of `PrepareMintParams`
635
+ * that excludes the signing wallet, EIP-712 domain, and on-chain
636
+ * `mintRequestNonces` lookup since those don't affect calldata shape
637
+ * (the bundler estimate is shape-invariant under their values).
638
+ */
639
+ interface PreviewMintParams {
640
+ userAddress: Address;
641
+ aaNonce: bigint;
642
+ pointTokenAddress: Address;
643
+ amount: bigint;
644
+ deadline: bigint;
645
+ mintFeeWrapperAddress?: Address;
646
+ }
647
+ interface PreviewBurnParams {
648
+ userAddress: Address;
649
+ aaNonce: bigint;
650
+ pointTokenAddress: Address;
651
+ amount: bigint;
652
+ deadline: bigint;
653
+ }
654
+ /**
655
+ * Sig-gated `PointToken.mint(to, amount, deadline, minterSig)`.
656
+ *
657
+ * The issuer backend validates off-chain (balance, policy, KYC), signs
658
+ * a `MintRequest` EIP-712 with its minter signer, and packages the
659
+ * whole thing into a UserOp for the user to submit via EIP-7702 +
660
+ * Paymaster. On confirmation, PointIndexer watches `Transfer(0x0, user,
661
+ * amount)` and resolves the ledger lock.
662
+ */
663
+ interface PrepareMintParams {
664
+ /** User EOA that will send the UserOp (via EIP-7702 delegation). */
665
+ userAddress: Address;
666
+ /** ERC-4337 account nonce. Caller fetches from EntryPoint v0.7. */
667
+ aaNonce: bigint;
668
+ /** BatchExecutor delegation target (chain-specific). */
669
+ batchExecutorAddress: Address;
670
+ /** PointToken contract — the call target + EIP-712 verifying contract. */
671
+ pointTokenAddress: Address;
672
+ /** PT amount to mint to `userAddress`. */
673
+ amount: bigint;
674
+ /**
675
+ * Issuer minter signer wallet — signs the `MintRequest` EIP-712.
676
+ * Must be added to `PointToken.minters[]` via `addMinter(signerAddr)`
677
+ * at provisioning time. Typically HSM/KMS-backed in prod.
678
+ */
679
+ issuerSignerWallet: WalletClient;
680
+ /** EIP-712 domain for MintRequest. */
681
+ domain: PointTokenDomainConfig;
682
+ /** Current `mintRequestNonces[userAddress]` — caller reads from contract. */
683
+ mintRequestNonce: bigint;
684
+ /** Unix timestamp after which the signature expires. */
685
+ deadline: bigint;
686
+ /**
687
+ * Optional — when set, mint is routed through MintFeeWrapper:
688
+ * - sig.receiver = wrapper address (vs `userAddress` for direct path)
689
+ * - calldata target = wrapper.mintWithFee(pointToken, user, gross, ...)
690
+ * - the wrapper then mints `amount` to itself, splits per recipient list,
691
+ * and forwards net to the user.
692
+ *
693
+ * Leave undefined for direct-mint behavior (no fee skim). Wrapper
694
+ * must be registered on the PointToken via `IssuerRegistry.addIssuer`
695
+ * cascade (or owner-only `wrapper.registerToken`) before this path
696
+ * works on-chain.
697
+ */
698
+ mintFeeWrapperAddress?: Address;
699
+ /**
700
+ * Optional — application-level fee transfer appended after mint.
701
+ * Set both `feeAmount` and `feeRecipient` together.
702
+ */
703
+ feeAmount?: bigint;
704
+ feeRecipient?: Address;
705
+ /** Gas limits — defaults are conservative; caller can tighten. */
706
+ callGasLimit?: bigint;
707
+ verificationGasLimit?: bigint;
708
+ preVerificationGas?: bigint;
709
+ }
710
+ /**
711
+ * Sig-gated burn only — every burn goes through `BurnRequest` EIP-712
712
+ * signed by the issuer burner. The `mode: 'burnWithSig'` discriminant
713
+ * is preserved for backwards-compat with existing callers but is no
714
+ * longer branched on internally.
715
+ */
716
+ interface PrepareBurnParams {
717
+ userAddress: Address;
718
+ aaNonce: bigint;
719
+ pointTokenAddress: Address;
720
+ batchExecutorAddress: Address;
721
+ /** Discriminant kept for backwards compat. Always `'burnWithSig'`. */
722
+ mode?: "burnWithSig";
723
+ /** BurnRequest message the issuer burner signer signed. */
724
+ burnRequest: BurnRequest;
725
+ /** Serialized EIP-712 signature (bytes) over `burnRequest`. */
726
+ burnerSignature: Hex;
727
+ /**
728
+ * Optional — application-level PT fee transfer appended after burn.
729
+ * Used for gas reimbursement on the sponsored path. Set both
730
+ * `feeAmount` and `feeRecipient` together. User must hold
731
+ * `burnAmount + feeAmount` PT.
732
+ */
733
+ feeAmount?: bigint;
734
+ feeRecipient?: Address;
735
+ callGasLimit?: bigint;
736
+ verificationGasLimit?: bigint;
737
+ preVerificationGas?: bigint;
738
+ }
739
+
740
+ /**
741
+ * Caller-supplied fetch implementation, matching the WHATWG Fetch API
742
+ * shape. Defaults to `globalThis.fetch` when omitted; tests and
743
+ * non-browser/node environments can plug in their own.
744
+ */
745
+ type FetchImpl = (input: string, init?: RequestInit) => Promise<Response>;
746
+ /**
747
+ * Duck-typed estimator interface. `FeeManager` accepts any object
748
+ * satisfying this surface so the SDK stays decoupled from where the
749
+ * bundler estimate physically lives (PAFI sponsor-relayer in production,
750
+ * a mock in tests, a local Pimlico proxy in dev).
751
+ *
752
+ * The estimator's job: given the preview UserOp shape, return the gas
753
+ * units to multiply by `gasPrice` and feed into the caller's fee
754
+ * quoter. Premium and PM overhead are the estimator's responsibility,
755
+ * NOT the SDK's — preventing double-padding.
756
+ */
757
+ interface BundlerEstimatorClient {
758
+ /**
759
+ * Resolve gas units for a sponsored UserOp.
760
+ *
761
+ * @throws — implementations should throw on transport / auth /
762
+ * protocol errors so `FeeManager` can degrade to its hardcoded
763
+ * fallback. Network failures must NOT silently return a default,
764
+ * since that would mask a misconfiguration as a successful estimate.
765
+ */
766
+ getGasUnits(input: {
767
+ scenario: string;
768
+ contractAddress: Address;
769
+ paymasterAddress?: Address;
770
+ partialUserOp: {
771
+ sender: Address;
772
+ nonce: bigint;
773
+ callData: Hex;
774
+ signature?: Hex;
775
+ };
776
+ }): Promise<{
777
+ gasUnits: bigint;
778
+ source: "cache" | "bundler" | "fallback";
779
+ expiresAt: number;
780
+ }>;
781
+ }
782
+ interface PafiEstimatorClientConfig {
783
+ /**
784
+ * Base URL of the PAFI sponsor-relayer (no trailing slash). The
785
+ * adapter appends `/v1/estimate-gas-fee` — issuer infrastructure
786
+ * never talks to Pimlico directly.
787
+ */
788
+ baseUrl: string;
789
+ /** Issuer's PAFI API key — the same one used for `/paymaster/sponsor`. */
790
+ apiKey: string;
791
+ /** Issuer ID used in `X-Issuer-Id` header. */
792
+ issuerId: string;
793
+ /** Custom fetch (e.g. `undici` in tests). Defaults to `globalThis.fetch`. */
794
+ fetchImpl?: FetchImpl;
795
+ }
796
+ declare class PafiEstimatorHttpError extends Error {
797
+ readonly status: number;
798
+ readonly body: unknown;
799
+ constructor(status: number, body: unknown, message?: string);
800
+ }
801
+ /**
802
+ * HTTP adapter that hits PAFI sponsor-relayer's `/v1/estimate-gas-fee`
803
+ * endpoint. The SDK never imports Pimlico-specific code or holds the
804
+ * Pimlico API key — the bundler call happens server-side at PAFI.
805
+ *
806
+ * Authentication mirrors `/paymaster/sponsor`:
807
+ * - `Authorization: Bearer <apiKey>`
808
+ * - `X-Issuer-Id: <issuerId>`
809
+ *
810
+ * Issuers wire this once at boot via `createPafiEstimatorClient({
811
+ * baseUrl, apiKey, issuerId })` and pass it to `FeeManager` as
812
+ * `bundlerClient`. The same `baseUrl`/`apiKey`/`issuerId` already
813
+ * authenticate other PAFI calls — no new secrets required.
814
+ */
815
+ declare function createPafiEstimatorClient(config: PafiEstimatorClientConfig): BundlerEstimatorClient;
816
+
817
+ type GasFeeSource = "estimator" | "fallback" | "cached-fee";
818
+ /**
819
+ * Hooks for observability. Sync, best-effort — errors thrown by hooks
820
+ * are swallowed so they cannot break the user-facing fee flow.
821
+ */
822
+ interface FeeManagerMetrics {
823
+ onEstimate?: (info: {
824
+ source: GasFeeSource;
825
+ scenario?: string;
826
+ gasUnits: bigint;
827
+ latencyMs: number;
828
+ }) => void;
829
+ onEstimatorError?: (info: {
830
+ scenario?: string;
831
+ reason: string;
832
+ }) => void;
833
+ }
834
+ interface FeeManagerConfig {
835
+ /** Provider used for gas-price reads. */
836
+ provider: PublicClient;
837
+ /**
838
+ * Hardcoded fallback gas units, used when (a) no `bundlerClient` is
839
+ * configured, OR (b) the bundler call fails and `partialUserOp` was
840
+ * not supplied. Default: 500_000n. Sized as a safe over-estimate vs
841
+ * the historically observed mint/burn cost so a fallback never
842
+ * under-charges the sponsor.
843
+ */
844
+ gasUnits?: bigint;
845
+ /**
846
+ * SDK-side premium in basis points. Default: 10_000 (100% = no extra
847
+ * padding). Pimlico's bundler pads ~10-15% internally, and PAFI
848
+ * sponsor-relayer applies its own premium (110%) before returning
849
+ * gasUnits, so adding more here would compound the over-charge that
850
+ * the v0.20 refactor was designed to remove. Override only when the
851
+ * caller's `bundlerClient` returns raw bundler values without a
852
+ * premium applied.
853
+ */
854
+ gasPremiumBps?: number;
855
+ /**
856
+ * Quote function — given an amount of native wei, return the
857
+ * equivalent amount in the fee currency (PT raw units for mint/burn,
858
+ * USDT/USDC 6-decimal for swap / perp deposit). Chain- and
859
+ * token-agnostic by design; wire to `createNativePtQuoter` for PT or
860
+ * `quoteOperatorFeeUsdt` for stables.
861
+ */
862
+ quoteNativeToFee: (amountNative: bigint) => Promise<bigint>;
863
+ /**
864
+ * Optional bundler-driven gas-units estimator. When set,
865
+ * `estimateGasFee({ partialUserOp, scenario, contractAddress })` calls
866
+ * the estimator for per-UserOp accuracy. When unset, the legacy
867
+ * hardcoded path runs — backward-compatible with v0.16/0.19 callers.
868
+ */
869
+ bundlerClient?: BundlerEstimatorClient;
870
+ /** Optional observability hooks. Throws inside hooks are swallowed. */
871
+ metrics?: FeeManagerMetrics;
872
+ }
873
+ /**
874
+ * Parameters for a single bundler-driven `estimateGasFee` call. All
875
+ * fields optional for backwards compatibility — when `partialUserOp` is
876
+ * absent the SDK falls back to the hardcoded path.
877
+ */
878
+ interface EstimateGasFeeOptions {
879
+ partialUserOp?: {
880
+ sender: Address;
881
+ nonce: bigint;
882
+ callData: Hex;
883
+ signature?: Hex;
884
+ };
885
+ scenario?: string;
886
+ contractAddress?: Address;
887
+ paymasterAddress?: Address;
888
+ }
889
+ /**
890
+ * Computes the operator fee the issuer charges users for sponsored gas.
891
+ *
892
+ * Fee currency is whatever the injected `quoteNativeToFee` returns —
893
+ * PT for mint/burn, USDT/USDC for swap & perp deposit.
894
+ *
895
+ * Two execution paths:
896
+ *
897
+ * 1. **Estimator path** (recommended) — caller wires `bundlerClient`
898
+ * (typically `createPafiEstimatorClient`). Each call hits the PAFI
899
+ * sponsor-relayer's `/v1/estimate-gas-fee` which caches by
900
+ * `(scenario, contract codehash, paymaster)` and applies its own
901
+ * premium. Result is the most accurate fee available and matches
902
+ * what sponsor-relayer's verify path expects, eliminating
903
+ * `INSUFFICIENT_FEE` rejects from formula drift.
904
+ *
905
+ * 2. **Fallback path** — no bundler client OR estimator threw. Use
906
+ * hardcoded `gasUnits × premium`. Same shape as v0.16/0.19 so
907
+ * legacy integrations get identical behaviour.
908
+ *
909
+ * **No operator rebalancing**: the operator does NOT hold ETH; gas is
910
+ * paid by PAFI's Pimlico paymaster. The fee here is an
911
+ * application-level ERC-20 transfer in the same UserOp batch.
912
+ */
913
+ declare class FeeManager {
914
+ private readonly provider;
915
+ private readonly fallbackGasUnits;
916
+ private readonly gasPremiumBps;
917
+ private readonly quoteNativeToFee;
918
+ private readonly bundlerClient;
919
+ private readonly metrics;
920
+ private cachedFee;
921
+ private cacheExpiresAt;
922
+ private static readonly FEE_CACHE_TTL_MS;
923
+ constructor(config: FeeManagerConfig);
924
+ /**
925
+ * Estimate the operator fee for the next sponsored UserOp.
926
+ *
927
+ * Without `opts` → legacy path: `gasUnits × gasPrice × premium →
928
+ * quoteNativeToFee`. Cached for 10 s to absorb bursts.
929
+ *
930
+ * With `opts` AND `bundlerClient` → estimator path. Each call may
931
+ * hit a different bundler-cached result; the SDK does NOT add its
932
+ * own value cache because the estimator's cache TTL is the source
933
+ * of truth for "how long is this estimate good for".
934
+ */
935
+ estimateGasFee(opts?: EstimateGasFeeOptions): Promise<bigint>;
936
+ /** Manually purge the legacy 10s fee cache. */
937
+ invalidateCache(): void;
938
+ private resolveGasUnits;
939
+ private safeEmit;
940
+ }
941
+
942
+ /** Decoded Transfer(from=0x0 → to) event used to finalize a mint. */
943
+ interface MintEvent {
944
+ /** Destination address (the user who received the minted points) */
945
+ to: Address;
946
+ /** Amount minted (in wei / base units) */
947
+ amount: bigint;
948
+ /** Block number the mint was included in */
949
+ blockNumber: bigint;
950
+ /** Transaction hash that emitted the event */
951
+ txHash: Hex;
952
+ /** Log index within the tx, for deterministic ordering */
953
+ logIndex: number;
954
+ }
955
+ /** Decoded Transfer(from=user → 0x0) event used to finalize a burn-for-credit. */
956
+ interface BurnEvent {
957
+ /** The burner — user whose PT was burned. */
958
+ from: Address;
959
+ /** Amount burned. */
960
+ amount: bigint;
961
+ blockNumber: bigint;
962
+ txHash: Hex;
963
+ logIndex: number;
964
+ }
965
+ /**
966
+ * Cursor persistence interface — the indexer reports the next block
967
+ * number it is about to process so the caller can write it to Redis /
968
+ * Postgres / a file. The SDK does not own persistence because every
969
+ * issuer has their own storage stack.
970
+ *
971
+ * **Per-token keying.** When multiple PointTokens are wired through a
972
+ * single `createIssuerService` call, each `PointIndexer` MUST get its
973
+ * own cursor — otherwise token A advances the shared cursor past
974
+ * token B's events and token B's mints are never finalized (off-chain
975
+ * balance never deducts → on-chain PT supply for token B exceeds
976
+ * off-chain backing). Combined with the monotonic-save invariant this
977
+ * becomes a catastrophic-and-stable state where token B's cursor can
978
+ * never catch up. Implementations SHOULD expose `forKey(key)`
979
+ * returning a derived store keyed under a distinct namespace; the SDK
980
+ * factory calls it once per token.
981
+ *
982
+ * When `forKey` is absent, the SDK falls back to the bare store and
983
+ * emits a runtime warning if more than one token is configured. New
984
+ * implementations are strongly encouraged to add `forKey`.
985
+ */
986
+ interface IIndexerCursorStore {
987
+ /** Return the last persisted cursor (`undefined` on first run). */
988
+ load(): Promise<bigint | undefined>;
989
+ /** Persist a new cursor value. Called after each successful batch. */
990
+ save(blockNumber: bigint): Promise<void>;
991
+ /**
992
+ * Return a derived store keyed under `key`. The returned store is
993
+ * an independent `IIndexerCursorStore` so the same persistence
994
+ * backend can serve N indexers with N distinct cursors.
995
+ *
996
+ * Optional for backwards compatibility, but REQUIRED in practice
997
+ * whenever more than one PointToken is wired through the SDK
998
+ * factory (per-token keying section above).
999
+ */
1000
+ forKey?(key: string): IIndexerCursorStore;
1001
+ }
1002
+ /**
1003
+ * No-op cursor store. Useful when the caller wants to drive the cursor
1004
+ * entirely via `processBlockRange()` and doesn't need persistence.
1005
+ */
1006
+ declare class InMemoryCursorStore implements IIndexerCursorStore {
1007
+ private cursor;
1008
+ /**
1009
+ * Child stores keyed by `forKey()`. Each child has its own cursor,
1010
+ * so a single InMemoryCursorStore can back N PointIndexers in tests
1011
+ * / single-process callers.
1012
+ */
1013
+ private readonly children;
1014
+ load(): Promise<bigint | undefined>;
1015
+ save(blockNumber: bigint): Promise<void>;
1016
+ forKey(key: string): IIndexerCursorStore;
1017
+ }
1018
+ interface SingletonLockHandle {
1019
+ release(): Promise<void>;
1020
+ }
1021
+ interface ISingletonLock {
1022
+ /**
1023
+ * Attempt to acquire the lock for `key`. Returns a handle on success
1024
+ * (caller is the leader), or `null` if another holder owns it.
1025
+ *
1026
+ * Implementations MUST be non-blocking — if the lock is held, return
1027
+ * null immediately. The factory polls / retries at a higher layer.
1028
+ */
1029
+ acquire(key: string): Promise<SingletonLockHandle | null>;
1030
+ }
1031
+
1032
+ declare class PointIndexerFinalizeError extends Error {
1033
+ readonly context: {
1034
+ pointToken: Address;
1035
+ to: Address;
1036
+ amount: bigint;
1037
+ txHash: `0x${string}`;
1038
+ blockNumber: bigint;
1039
+ };
1040
+ readonly cause: unknown;
1041
+ constructor(message: string, context: {
1042
+ pointToken: Address;
1043
+ to: Address;
1044
+ amount: bigint;
1045
+ txHash: `0x${string}`;
1046
+ blockNumber: bigint;
1047
+ }, cause: unknown);
1048
+ }
1049
+ interface PointIndexerConfig {
1050
+ provider: PublicClient;
1051
+ pointTokenAddress: Address;
1052
+ ledger: IPointLedger;
1053
+ /**
1054
+ * When set, indexer listens to `MintFeeWrapper.MintWithFee`
1055
+ * (filtered by this `pointToken`) instead of
1056
+ * `PointToken.Transfer(0x0)`.
1057
+ *
1058
+ * Required for wrapper-mediated mints because the on-chain Transfer
1059
+ * destination is the wrapper itself (not the end user), so a naive
1060
+ * `Transfer(0x0 → user)` watch would leave PENDING locks unresolved.
1061
+ *
1062
+ * `MintWithFee(indexed pointToken, indexed to, grossAmount, netAmount,
1063
+ * feeAmount)` carries the actual recipient + the gross amount that
1064
+ * matches the off-chain lock.
1065
+ *
1066
+ * Pass `undefined` (or the dead-zero address) for direct-mint chains
1067
+ * without a wrapper — indexer falls back to legacy Transfer mode.
1068
+ */
1069
+ mintFeeWrapperAddress?: Address;
1070
+ /**
1071
+ * Block to start from on first run. Ignored if the cursor store already
1072
+ * has a value. Defaults to `0n`.
1073
+ */
1074
+ fromBlock?: bigint;
1075
+ /**
1076
+ * Persistent cursor store. Defaults to an `InMemoryCursorStore` which
1077
+ * only survives inside the current process.
1078
+ */
1079
+ cursorStore?: IIndexerCursorStore;
1080
+ /**
1081
+ * Reorg safety: only treat events as final after this many confirmations.
1082
+ * The indexer reads `latestBlock - confirmations` as its high-water mark.
1083
+ * Default: 3 (a conservative number for Base L2).
1084
+ */
1085
+ confirmations?: number;
1086
+ /**
1087
+ * How many blocks to scan per `getLogs` call. Keeps RPC pages bounded.
1088
+ * Default: 2000 (comfortably under most provider page limits).
1089
+ */
1090
+ batchSize?: number;
1091
+ /**
1092
+ * Polling interval (ms) used by `start()`. Default: 5000 (5s).
1093
+ */
1094
+ pollIntervalMs?: number;
1095
+ /** Clock override for tests. */
1096
+ now?: () => number;
1097
+ /**
1098
+ * Observability hook fired on EVERY tick error (RPC failure, ledger
1099
+ * write failure, etc). Issuers MUST wire this to their alert
1100
+ * pipeline (Sentry / Datadog / PagerDuty) — without it, the indexer
1101
+ * silently swallows errors via `console.error`, and indexer outage
1102
+ * means PENDING mint locks never resolve (user balances stuck).
1103
+ *
1104
+ * When omitted, falls back to `console.error` for back-compat.
1105
+ */
1106
+ onTickError?: (err: unknown) => void;
1107
+ }
1108
+ /**
1109
+ * Watches mint events on PointToken and finalizes the issuer ledger when
1110
+ * a confirmed mint matches a PENDING lock.
1111
+ *
1112
+ * **Two modes** — selected at construction by `mintFeeWrapperAddress`:
1113
+ *
1114
+ * 1. **Wrapper mode (recommended for mainnet)**
1115
+ * Listens to `MintFeeWrapper.MintWithFee(indexed pointToken, indexed to,
1116
+ * grossAmount, netAmount, feeAmount)` filtered by `pointToken`. The
1117
+ * `to` field is the actual end-user (not the wrapper), and `grossAmount`
1118
+ * matches the off-chain lock's amount.
1119
+ *
1120
+ * 2. **Direct mode (chains without wrapper)**
1121
+ * Listens to `PointToken.Transfer(from=0x0 → to)`. Used when the
1122
+ * wrapper address is undefined / zero / dead.
1123
+ *
1124
+ * Finalization strategy (per event):
1125
+ * 1. Find a PENDING locked mint request for `to` with matching amount.
1126
+ * 2. Call `ledger.deductBalance(to, amount, txHash)` — this is idempotent
1127
+ * in the default `MemoryPointLedger` because deducting also resolves
1128
+ * the matching lock.
1129
+ * 3. If no matching lock is found (e.g. a manual `PointToken.mint()` call
1130
+ * or a race where the lock already expired), log and skip — this
1131
+ * intentionally does NOT credit the ledger, because the gateway is
1132
+ * the only sanctioned way to spawn a mint.
1133
+ *
1134
+ * Reorg safety: events are only processed when they are at least
1135
+ * `confirmations` blocks deep. A new `getLogs` call on every poll picks
1136
+ * up finalized blocks from the persisted cursor.
1137
+ *
1138
+ * Pure-polling design (rather than `watchContractEvent`) keeps the SDK
1139
+ * compatible with HTTP-only providers and with RPC rotation.
1140
+ */
1141
+ declare class PointIndexer {
1142
+ private readonly provider;
1143
+ private readonly pointTokenAddress;
1144
+ private readonly mintFeeWrapperAddress;
1145
+ private readonly ledger;
1146
+ private readonly cursorStore;
1147
+ private readonly startBlock;
1148
+ private readonly confirmations;
1149
+ private readonly batchSize;
1150
+ private readonly pollIntervalMs;
1151
+ private readonly onTickError?;
1152
+ private running;
1153
+ private timer;
1154
+ constructor(config: PointIndexerConfig);
1155
+ /** Begin polling. Schedules `tick()` on a loop. */
1156
+ start(): void;
1157
+ /** Stop polling. Safe to call multiple times. */
1158
+ stop(): void;
1159
+ /**
1160
+ * Run one poll cycle: load cursor → scan [cursor, safeHead] in
1161
+ * `batchSize` chunks → persist new cursor. Swallows any error and
1162
+ * schedules the next tick. Visible for test harnesses via a public name.
1163
+ */
1164
+ tick(): Promise<void>;
1165
+ private handleTickError;
1166
+ private scheduleNext;
1167
+ /**
1168
+ * Scan `[from, to]` inclusive for mint events in `batchSize` chunks.
1169
+ * Callers can use this directly to backfill a specific range without
1170
+ * engaging `start()`. On completion, the cursor is advanced to `to + 1`.
1171
+ */
1172
+ processBlockRange(from: bigint, to: bigint): Promise<void>;
1173
+ /**
1174
+ * Wrapper mode: listen for `MintWithFee` on the wrapper,
1175
+ * filtered to events for THIS pointToken only. The event's `to` field
1176
+ * is the actual end user, and `grossAmount` matches the lock amount.
1177
+ */
1178
+ private fetchWrapperMintEvents;
1179
+ /**
1180
+ * Direct mode (legacy / chains without wrapper): listen for
1181
+ * `Transfer(from=0x0 → to)` on the PointToken itself.
1182
+ */
1183
+ private fetchTransferMintEvents;
1184
+ private decodeTransferMintEvents;
1185
+ /**
1186
+ * Finalize a single mint event: match it to a PENDING lock in the
1187
+ * ledger, then call `deductBalance` (which also resolves the lock in
1188
+ * the default `MemoryPointLedger`).
1189
+ *
1190
+ * No-matching-lock is a valid state: it means either the lock already
1191
+ * expired, or the mint was authorized out-of-band (e.g. a direct
1192
+ * `PointToken.mint()` from an EOA minter for testing). In that case we
1193
+ * do NOT touch the ledger — crediting here would silently allow the
1194
+ * issuer to mint without going through the gateway.
1195
+ */
1196
+ private finalize;
1197
+ }
1198
+
1199
+ /**
1200
+ * Mirror of `PointIndexerFinalizeError` — raised when
1201
+ * `ledger.resolveCreditByBurnTx` rejects. Silently swallowing this
1202
+ * error and advancing the cursor anyway would permanently drop
1203
+ * confirmed on-chain burns from the off-chain credit pipeline. The
1204
+ * error therefore propagates, the chunk's cursor save is skipped,
1205
+ * and the next tick retries.
1206
+ */
1207
+ declare class BurnIndexerFinalizeError extends Error {
1208
+ readonly context: {
1209
+ pointToken: Address;
1210
+ from: Address;
1211
+ amount: bigint;
1212
+ txHash: `0x${string}`;
1213
+ blockNumber: bigint;
1214
+ lockId: string;
1215
+ };
1216
+ readonly cause: unknown;
1217
+ constructor(message: string, context: {
1218
+ pointToken: Address;
1219
+ from: Address;
1220
+ amount: bigint;
1221
+ txHash: `0x${string}`;
1222
+ blockNumber: bigint;
1223
+ lockId: string;
1224
+ }, cause: unknown);
1225
+ }
1226
+ interface BurnIndexerConfig {
1227
+ provider: PublicClient;
1228
+ pointTokenAddress: Address;
1229
+ ledger: IPointLedger;
1230
+ /** Block to start from on first run. Ignored if cursor store has value. */
1231
+ fromBlock?: bigint;
1232
+ cursorStore?: IIndexerCursorStore;
1233
+ /**
1234
+ * Reorg safety — only treat events as final after this many
1235
+ * confirmations. Default: 3.
1236
+ */
1237
+ confirmations?: number;
1238
+ /** Blocks per getLogs call. Default: 2000. */
1239
+ batchSize?: number;
1240
+ /** Polling interval (ms). Default: 5000. */
1241
+ pollIntervalMs?: number;
1242
+ now?: () => number;
1243
+ /**
1244
+ * Map a burn event to the pending credit lockId that should be resolved.
1245
+ * Return `undefined` to skip this burn event (no credit granted).
1246
+ *
1247
+ * REQUIRED — there is no default implementation. Issuers with a Postgres
1248
+ * ledger typically JOIN on `(from, amount, status=PENDING)`. The in-memory
1249
+ * ledger uses a lookup by lockId supplied out-of-band from the claim flow.
1250
+ */
1251
+ matchLockId: (event: BurnEvent) => Promise<string | undefined>;
1252
+ /**
1253
+ * Observability hook — see PointIndexerConfig.onTickError for full
1254
+ * rationale. Critical for burn indexer too: silent failure means
1255
+ * pending credits never resolve, user reports "I burned my PT but
1256
+ * never got my off-chain credit".
1257
+ */
1258
+ onTickError?: (err: unknown) => void;
1259
+ }
1260
+ /**
1261
+ * Mirror of `PointIndexer` for the reverse direction — watches
1262
+ * `Transfer(user → 0x0)` events (ERC-20 burns) on the PointToken
1263
+ * contract and finalizes pending off-chain credits.
1264
+ *
1265
+ * Finalization flow:
1266
+ * 1. For each Burn event at `{from, amount, txHash}`:
1267
+ * 2. Call `ledger.resolveCreditByBurnTx(lockId, txHash)` where `lockId`
1268
+ * is resolved by the caller's `onMatchCredit` hook or a
1269
+ * ledger-specific lookup. The SDK does not prescribe the matching
1270
+ * strategy — issuers with a Postgres ledger can JOIN by
1271
+ * `(from, amount, status=PENDING)`; the in-memory ledger matches
1272
+ * by `lockId` supplied out-of-band.
1273
+ *
1274
+ * When no pending credit matches an observed Burn event, the indexer
1275
+ * logs + skips — **it never credits off-chain** from a Burn that was
1276
+ * not first reserved via `reservePendingCredit()`. This prevents
1277
+ * spurious credits from one-off admin burns or direct burn calls
1278
+ * outside the issuer SDK.
1279
+ */
1280
+ declare class BurnIndexer {
1281
+ private readonly provider;
1282
+ /**
1283
+ * The PointToken this indexer watches. Exposed so callers can key
1284
+ * leader-election locks / cursor stores by token
1285
+ */
1286
+ readonly pointTokenAddress: Address;
1287
+ private readonly ledger;
1288
+ private readonly cursorStore;
1289
+ private readonly startBlock;
1290
+ private readonly confirmations;
1291
+ private readonly batchSize;
1292
+ private readonly pollIntervalMs;
1293
+ private readonly onTickError?;
1294
+ matchLockId: (event: BurnEvent) => Promise<string | undefined>;
1295
+ private running;
1296
+ private timer;
1297
+ constructor(config: BurnIndexerConfig);
1298
+ start(): void;
1299
+ stop(): void;
1300
+ tick(): Promise<void>;
1301
+ private handleTickError;
1302
+ private scheduleNext;
1303
+ /**
1304
+ * Scan `[from, to]` inclusive for burn events. Callers can drive this
1305
+ * directly to backfill a specific range without `start()`. Cursor is
1306
+ * advanced to `to + 1` on completion.
1307
+ */
1308
+ processBlockRange(from: bigint, to: bigint): Promise<void>;
1309
+ private decodeBurnEvents;
1310
+ /**
1311
+ * Resolve a matching pending credit for this burn event and call
1312
+ * `ledger.resolveCreditByBurnTx(lockId, txHash)`. If no match found,
1313
+ * log + skip.
1314
+ */
1315
+ private finalize;
1316
+ }
1317
+
1318
+ /**
1319
+ * Minimal duck-typed adapter so we don't drag a TypeORM / pg dep into
1320
+ * the SDK. The caller passes anything that can execute a parameterised
1321
+ * query and yield `{ got: boolean }` rows — TypeORM `DataSource.query`,
1322
+ * a raw `pg` client, knex's `raw`, kysely, etc.
1323
+ *
1324
+ * The helper requires `int8` (signed 64-bit) lock IDs. We derive them
1325
+ * from the lock `key` via a deterministic hash so different replicas
1326
+ * agree on the int regardless of casing / whitespace drift.
1327
+ */
1328
+ interface PostgresQueryRunner {
1329
+ query(sql: string, params?: unknown[]): Promise<unknown>;
1330
+ }
1331
+ /**
1332
+ * Build an `ISingletonLock` backed by PostgreSQL session-level advisory
1333
+ * locks (`pg_try_advisory_lock`).
1334
+ *
1335
+ * Why advisory locks (vs. row-level locks or a leases table):
1336
+ * 1. **Non-blocking** — `pg_try_advisory_lock` returns immediately;
1337
+ * perfect for the "try then idle" pattern the SDK uses on boot.
1338
+ * 2. **Auto-release on connection drop** — if the leader pod crashes,
1339
+ * its PG connection closes, and the lock is freed automatically.
1340
+ * No timeout tuning, no lease renewal loops, no zombie locks.
1341
+ * 3. **Zero schema** — no extra table, no migration, no contention
1342
+ * with application reads.
1343
+ *
1344
+ * **Caveat — long-lived connection required.** Session-level locks are
1345
+ * held only for the lifetime of the PG connection that called
1346
+ * `pg_try_advisory_lock`. PgBouncer in `transaction` pooling mode will
1347
+ * release the lock between statements; you MUST run in `session`
1348
+ * pooling or skip the bouncer for the indexer process. The advisory
1349
+ * `transaction` variant exists but doesn't fit the "hold while polling"
1350
+ * pattern — for that you'd need a periodic re-acquire, which we don't
1351
+ * implement here.
1352
+ *
1353
+ * @example
1354
+ * import { DataSource } from "typeorm";
1355
+ * import { makePostgresSingletonLock } from "@pafi-dev/issuer";
1356
+ *
1357
+ * const dataSource = new DataSource({ ... });
1358
+ * const lock = makePostgresSingletonLock({
1359
+ * query: (sql, params) => dataSource.query(sql, params),
1360
+ * });
1361
+ *
1362
+ * createIssuerService({
1363
+ * ...,
1364
+ * indexer: {
1365
+ * autoStart: true,
1366
+ * singletonLock: lock,
1367
+ * },
1368
+ * });
1369
+ */
1370
+ declare function makePostgresSingletonLock(runner: PostgresQueryRunner): ISingletonLock;
1371
+
1372
+ interface ApiConfigResponse {
1373
+ chainId: number;
1374
+ contracts: {
1375
+ /**
1376
+ * Legacy single-token field — kept for backward compat with v0.1.x
1377
+ * frontends. Prefer `pointTokens` for multi-token issuers.
1378
+ */
1379
+ pointToken?: Address;
1380
+ /**
1381
+ * All supported PointToken addresses (v0.2.0+). Single-token issuers
1382
+ * will have one entry that matches `pointToken`. Multi-token
1383
+ * issuers expose the full list here so the frontend can render a
1384
+ * token picker.
1385
+ */
1386
+ pointTokens?: Address[];
1387
+ relay?: Address;
1388
+ issuerRegistry?: Address;
1389
+ pointTokenFactory?: Address;
1390
+ mintingOracle?: Address;
1391
+ poolManager?: Address;
1392
+ usdt?: Address;
1393
+ /**
1394
+ * EIP-7702 delegation target — the single contract every user's
1395
+ * EOA must delegate to before submitting sponsored UserOps. On
1396
+ * Base mainnet this is Pimlico's `Simple7702Account` at
1397
+ * `0xe6Cae83BdE06E4c305530e199D7217f42808555B` (swapped in
1398
+ * 2026-04-27 to replace the earlier Coinbase Smart Wallet v2
1399
+ * BatchExecutor, which had a SignatureWrapper incompatibility
1400
+ * surfacing as AA23 0x3c10b94e during validateUserOp).
1401
+ */
1402
+ batchExecutor?: Address;
1403
+ /**
1404
+ * Single global MintFeeWrapper that skims a fee on every
1405
+ * sig-gated mint and distributes across the configured recipients.
1406
+ */
1407
+ mintFeeWrapper?: Address;
1408
+ };
1409
+ /**
1410
+ * Per-PointToken mint fee (in basis points, 0–10000) read from
1411
+ * `MintFeeWrapper.totalFeeBps(pointToken)`. Frontend uses this to show
1412
+ * "you'll receive X PT (Y% mint fee)" before user signs.
1413
+ *
1414
+ * Map keyed by lower-cased PointToken address (JSON-friendly) so the
1415
+ * FE can look up the fee without iterating. Omitted (or with zero
1416
+ * values) when the chain has no wrapper deployed or recipients are
1417
+ * not configured.
1418
+ */
1419
+ mintFeeBpsByToken?: Record<string, number>;
1420
+ /**
1421
+ * Absolute URL that the Issuer App opens after a successful claim to
1422
+ * let the user swap PT → USDT or deposit into the perp DEX on PAFI
1423
+ * Web. Mobile opens this in an in-app browser
1424
+ * (SFSafariViewController / Chrome Custom Tabs). Desktop opens in a
1425
+ * popup. See [MOBILE_SDK_INTEGRATION.md] "PAFI Web Handoff" section.
1426
+ *
1427
+ * Optional — if omitted, the Issuer App should hide the "Open PAFI"
1428
+ * button.
1429
+ */
1430
+ pafiWebUrl?: string;
1431
+ }
1432
+ interface ApiNonceResponse {
1433
+ nonce: string;
1434
+ /**
1435
+ * Complete EIP-4361 message, ready to pass directly to `wallet.signMessage()`.
1436
+ * Built by the controller using `createLoginMessage()` — requires the caller's
1437
+ * `walletAddress`, so it cannot be constructed inside the handler itself.
1438
+ */
1439
+ message?: string;
1440
+ }
1441
+ interface ApiLoginRequest {
1442
+ message: string;
1443
+ signature: Hex;
1444
+ }
1445
+ interface ApiLoginResponse {
1446
+ token: string;
1447
+ userAddress: Address;
1448
+ /** Unix ms when the JWT (and backing session) expires. */
1449
+ expiresAt: number;
1450
+ }
1451
+ interface ApiGasFeeResponse {
1452
+ /** Gas fee quoted in USDT (6-decimal base units). */
1453
+ gasFeeUsdt: bigint;
1454
+ }
1455
+ interface ApiPoolsRequest {
1456
+ chainId: number;
1457
+ pointTokenAddress: Address;
1458
+ }
1459
+ interface ApiPoolsResponse {
1460
+ pools: PoolKey[];
1461
+ }
1462
+ interface ApiUserRequest {
1463
+ chainId: number;
1464
+ userAddress: Address;
1465
+ pointTokenAddress: Address;
1466
+ }
1467
+ interface ApiUserResponse {
1468
+ /**
1469
+ * Off-chain point balance from the issuer's ledger (excludes PENDING locks).
1470
+ * This is what the user can claim into on-chain PT via `/claim`.
1471
+ */
1472
+ offChainBalance: bigint;
1473
+ /**
1474
+ * On-chain ERC-20 balance from `PointToken.balanceOf(user)`.
1475
+ * Points the user has already claimed but hasn't swapped or redeemed.
1476
+ */
1477
+ onChainBalance: bigint;
1478
+ /**
1479
+ * Sum of off-chain + on-chain balance. FE renders this as a single
1480
+ * "your points" number, per the unified-balance UX spec.
1481
+ */
1482
+ totalBalance: bigint;
1483
+ /**
1484
+ * @deprecated Use `offChainBalance` instead. Retained as a wire-level
1485
+ * alias for older mobile clients that still read it.
1486
+ */
1487
+ balance: bigint;
1488
+ isMinter: boolean;
1489
+ }
1490
+ interface ApiRedemptionPreviewRequest {
1491
+ pointTokenAddress?: Address;
1492
+ }
1493
+ interface ApiRedemptionPreviewResponse {
1494
+ availableAmountPt: bigint;
1495
+ dailyRemainingPt: bigint;
1496
+ cooldownUntilUnixSec: number | null;
1497
+ nextBlackoutEndsAtUnixSec: number | null;
1498
+ perTxMinPt: bigint;
1499
+ perTxMaxPt: bigint;
1500
+ policyVersion: string;
1501
+ policySource: "settlement" | "cache" | "default";
1502
+ }
1503
+ interface ApiRedemptionEvaluateRequest {
1504
+ amountPt: bigint;
1505
+ pointTokenAddress?: Address;
1506
+ }
1507
+ interface ApiRedemptionEvaluateResponse {
1508
+ allowed: boolean;
1509
+ denial?: {
1510
+ code: "AMOUNT_BELOW_MIN" | "AMOUNT_ABOVE_MAX" | "DAILY_LIMIT_EXCEEDED" | "COOLDOWN_ACTIVE" | "BLACKOUT_WINDOW";
1511
+ message: string;
1512
+ };
1513
+ preview: ApiRedemptionPreviewResponse;
1514
+ }
1515
+ type PoolsProvider = (request: ApiPoolsRequest) => Promise<ApiPoolsResponse>;
1516
+
1517
+ /**
1518
+ * Per-user redemption history needed by the evaluator. The issuer can
1519
+ * implement this with their existing burn journal table (preferred) or
1520
+ * use the bundled MemoryRedemptionHistoryStore for tests.
1521
+ */
1522
+ interface IRedemptionHistoryStore {
1523
+ /**
1524
+ * Sum of PT redeemed by `user` from `sinceUnixSec` onwards. Used for
1525
+ * the rolling 24h daily-limit check. MUST count both confirmed burns
1526
+ * and currently-reserved-pending entries — the policy treats reserved
1527
+ * burns as "spent" so a flood of pending requests can't bypass the cap.
1528
+ */
1529
+ sumRedeemedSince(user: Address, sinceUnixSec: number, pointTokenAddress?: Address): Promise<bigint>;
1530
+ /**
1531
+ * Unix-second timestamp of the user's last redemption attempt
1532
+ * (confirmed or reserved). null if never. Used for cooldown.
1533
+ */
1534
+ getLastRedeemedAtUnixSec(user: Address, pointTokenAddress?: Address): Promise<number | null>;
1535
+ /**
1536
+ * Record a new redemption attempt. Called by handleRedemptionInitiate
1537
+ * AFTER policy.evaluate() returns ALLOW and the BurnRequest is signed.
1538
+ * Implementations may persist this row in a redemption_history table or
1539
+ * derive from existing pending-credit reservations.
1540
+ */
1541
+ recordRedemption(entry: {
1542
+ user: Address;
1543
+ amountPt: bigint;
1544
+ pointTokenAddress?: Address;
1545
+ unixSec: number;
1546
+ /** Optional pointer back to the reservation lock id. */
1547
+ reservationId?: string;
1548
+ }): Promise<void>;
1549
+ }
1550
+ interface SettlementClientConfig {
1551
+ /**
1552
+ * chainId — used to derive the issuer-api base URL via
1553
+ * `getPafiServiceUrls(chainId).issuerApi`. SDK ships with the URL
1554
+ */
1555
+ chainId: number;
1556
+ baseUrl?: string;
1557
+ /** PAFI-assigned issuer id used in `X-Issuer-Id` header. */
1558
+ issuerId: string;
1559
+ /** Raw API key sent as `Authorization: Bearer <apiKey>`. */
1560
+ apiKey: string;
1561
+ /** Per-request timeout in milliseconds. Default 1000. */
1562
+ fetchTimeoutMs?: number;
1563
+ /** Override fetch (testing). */
1564
+ fetchImpl?: typeof fetch;
1565
+ }
1566
+ interface PolicyProviderConfig extends SettlementClientConfig {
1567
+ /** Cache TTL in milliseconds. Default 5 * 60 * 1000 (5min). */
1568
+ cacheTtlMs?: number;
1569
+ onFetchFailure?: "fail-closed" | "permissive-default";
1570
+ onWarning?: (msg: string, ctx: Record<string, unknown>) => void;
1571
+ /**
1572
+ * Optional clock for testability. Returns unix milliseconds.
1573
+ * Defaults to () => Date.now().
1574
+ */
1575
+ now?: () => number;
1576
+ }
1577
+
1578
+ interface ResolvedPolicy {
1579
+ policy: RedemptionPolicy;
1580
+ source: RedemptionPolicySource;
1581
+ }
1582
+ /**
1583
+ * Wraps SettlementClient with a 5-minute TTL cache and a hardcoded
1584
+ * default fallback. Single-flight: concurrent getPolicy() calls during
1585
+ * a cache miss share the same in-flight request.
1586
+ *
1587
+ * Resolution order:
1588
+ * 1. fresh cache hit → return cached
1589
+ * 2. cache miss → fetch ok → cache + return (source=settlement)
1590
+ * 3. cache miss → fetch failed → DEFAULT_REDEMPTION_POLICY (source=default)
1591
+ *
1592
+ * Stale cache is NEVER returned — once the TTL expires we either fetch
1593
+ * fresh data or fall through to default. This keeps behavior predictable:
1594
+ * "default" means "we couldn't talk to settlement RIGHT NOW", not "we got
1595
+ * lucky with a stale entry".
1596
+ */
1597
+ declare class PolicyProvider {
1598
+ private readonly client;
1599
+ private readonly issuerId;
1600
+ private readonly cacheTtlMs;
1601
+ private readonly onFetchFailure;
1602
+ private readonly onWarning?;
1603
+ private readonly now;
1604
+ private cache;
1605
+ private inflight;
1606
+ constructor(config: PolicyProviderConfig);
1607
+ getPolicy(): Promise<ResolvedPolicy>;
1608
+ /** Drop cached policy. Next getPolicy() will refetch. */
1609
+ invalidate(): void;
1610
+ private readCache;
1611
+ private fetchAndStore;
1612
+ }
1613
+
1614
+ interface RedemptionServiceConfig {
1615
+ policyProvider: PolicyProvider | PolicyProviderConfig;
1616
+ historyStore: IRedemptionHistoryStore;
1617
+ /** Defaults to () => Math.floor(Date.now() / 1000). */
1618
+ nowUnixSec?: () => number;
1619
+ }
1620
+ /**
1621
+ * High-level facade used by HTTP handlers. Combines PolicyProvider +
1622
+ * IRedemptionHistoryStore into preview/evaluate operations.
1623
+ *
1624
+ * preview(user) → RedemptionPreview (no amount required)
1625
+ * evaluate(user, amount) → RedemptionDecision
1626
+ * recordSuccessfulInitiate(..) → call AFTER signing the BurnRequest
1627
+ *
1628
+ * Note: this service does NOT mutate anything during evaluate(). The
1629
+ * caller must call recordSuccessfulInitiate() after the BurnRequest is
1630
+ * signed and the pending credit is reserved on the ledger. Splitting
1631
+ * "decide" from "record" lets the handler atomically reserve + record
1632
+ * under one DB transaction if it wants.
1633
+ */
1634
+ declare class RedemptionService {
1635
+ private readonly policyProvider;
1636
+ private readonly historyStore;
1637
+ private readonly nowUnixSec;
1638
+ constructor(config: RedemptionServiceConfig);
1639
+ preview(user: Address, pointTokenAddress?: Address): Promise<RedemptionPreview>;
1640
+ evaluate(user: Address, amountPt: bigint, pointTokenAddress?: Address): Promise<RedemptionDecision>;
1641
+ recordSuccessfulInitiate(entry: {
1642
+ user: Address;
1643
+ amountPt: bigint;
1644
+ pointTokenAddress?: Address;
1645
+ reservationId?: string;
1646
+ }): Promise<void>;
1647
+ }
1648
+
1649
+ interface IssuerApiHandlersConfig {
1650
+ authService: AuthService;
1651
+ ledger: IPointLedger;
1652
+ /** Used by `handleUser` to read on-chain nonces and minter status. */
1653
+ provider: PublicClient;
1654
+ /**
1655
+ * Legacy single-token config. Prefer `pointTokenAddresses` for multi-token
1656
+ * issuers (0.2.0+).
1657
+ */
1658
+ pointTokenAddress?: Address;
1659
+ /**
1660
+ * All supported PointToken addresses. Handlers accept any address in this
1661
+ * list; others are rejected with "unsupported pointToken".
1662
+ */
1663
+ pointTokenAddresses?: Address[];
1664
+ chainId: number;
1665
+ contracts: ApiConfigResponse["contracts"];
1666
+ /**
1667
+ * Optional — URL that the Issuer App opens for PT→USDT swap or perp
1668
+ * deposit after a successful claim. Surfaced in `/config` response.
1669
+ * See [MOBILE_SDK_INTEGRATION.md] "PAFI Web Handoff".
1670
+ */
1671
+ pafiWebUrl?: string;
1672
+ /**
1673
+ * MintFeeWrapper address used to read per-PointToken fee basis
1674
+ * points for `/config`. When omitted, the response will not include
1675
+ * `mintFeeBpsByToken` and FE must assume zero fee. Wrapper is shared
1676
+ * across PointTokens; per-token recipients live inside it.
1677
+ */
1678
+ mintFeeWrapperAddress?: Address;
1679
+ /** Required by `handleGasFee`; omit to disable the endpoint. */
1680
+ feeManager?: FeeManager;
1681
+ /** Required by `handlePools`; omit to disable the endpoint. */
1682
+ poolsProvider?: PoolsProvider;
1683
+ /**
1684
+ * Required by `handleRedemptionPreview` / `handleRedemptionEvaluate`;
1685
+ * omit to disable both. Wired by createIssuerService when the top-level
1686
+ * `redemption` config is provided.
1687
+ */
1688
+ redemption?: RedemptionService;
1689
+ /**
1690
+ * Rate limiter for `/auth/nonce` and `/auth/login` (both unauthenticated +
1691
+ * CPU-bound). When omitted, defaults to a `NoopRateLimiter` that lets
1692
+ * every request through (with a one-time prod warning) — issuers MUST
1693
+ * wire a real impl in production.
1694
+ */
1695
+ rateLimiter?: IRateLimiter;
1696
+ }
1697
+ /**
1698
+ * Framework-agnostic HTTP handlers that match the endpoints a `PafiSDK`
1699
+ * frontend expects to call. Issuers wrap these in Express / Fastify /
1700
+ * Hono / whatever — the handlers take plain inputs and return plain
1701
+ * outputs, with `AuthError` surfaced as typed exceptions.
1702
+ *
1703
+ * Every protected handler takes a pre-verified `userAddress` as its first
1704
+ * argument. The issuer's middleware wraps `authenticateRequest()` from
1705
+ * `@pafi-dev/issuer` to extract that address from the Bearer token before
1706
+ * routing.
1707
+ */
1708
+ declare class IssuerApiHandlers {
1709
+ private readonly authService;
1710
+ private readonly ledger;
1711
+ private readonly provider;
1712
+ /**
1713
+ * Set of supported PointToken addresses (checksum-normalized). Handlers
1714
+ * validate the request's `pointTokenAddress` against this set.
1715
+ */
1716
+ private readonly supportedTokens;
1717
+ private readonly chainId;
1718
+ private readonly contracts;
1719
+ private readonly pafiWebUrl?;
1720
+ private readonly feeManager?;
1721
+ private readonly poolsProvider?;
1722
+ private readonly redemption?;
1723
+ private readonly rateLimiter;
1724
+ private readonly mintFeeWrapperAddress?;
1725
+ /**
1726
+ * Per-token feeBps cache. Refreshed on /config when stale. feeBps
1727
+ * changes only when ops calls `wrapper.setRecipients`, so 5-min TTL
1728
+ * is more than safe for FE display purposes.
1729
+ */
1730
+ private readonly feeBpsCache;
1731
+ private static readonly FEE_BPS_TTL_MS;
1732
+ constructor(config: IssuerApiHandlersConfig);
1733
+ /**
1734
+ * `GET /auth/nonce`
1735
+ *
1736
+ * @param rateLimitKey Caller-side rate-limit key (typically client IP).
1737
+ * The HTTP layer (controller/middleware) extracts
1738
+ * this from the request and passes it through.
1739
+ * When omitted, no rate limit applies — production
1740
+ * callers SHOULD always pass a key.
1741
+ */
1742
+ handleGetNonce(rateLimitKey?: string): Promise<ApiNonceResponse>;
1743
+ /**
1744
+ * `POST /auth/login`
1745
+ *
1746
+ * @param body Login message + signature.
1747
+ * @param rateLimitKey Caller-side rate-limit key (typically client IP
1748
+ * or `body.userAddress` if known). See `handleGetNonce`.
1749
+ */
1750
+ handleLogin(body: ApiLoginRequest, rateLimitKey?: string): Promise<ApiLoginResponse>;
1751
+ /**
1752
+ * `GET /config?chainId=<id>`
1753
+ *
1754
+ * Returns the contract addresses and chain id that the frontend SDK
1755
+ * needs to build EIP-712 messages and interact with on-chain.
1756
+ */
1757
+ handleConfig(chainId: number): Promise<ApiConfigResponse>;
1758
+ /**
1759
+ * Read `totalFeeBps(pointToken)` for every supported PointToken from
1760
+ * the wrapper. Cached per-token for 5 minutes. Returns `undefined`
1761
+ * (caller drops the field) if every read fails — FE must treat
1762
+ * "field missing" as "fee unknown, do not display".
1763
+ */
1764
+ private resolveFeeBpsByToken;
1765
+ /** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
1766
+ handleGasFee(): Promise<ApiGasFeeResponse>;
1767
+ /** `POST /auth/logout` */
1768
+ handleLogout(token: string): Promise<void>;
1769
+ /**
1770
+ * `GET /pools?chainId=<id>&pointToken=<addr>`
1771
+ *
1772
+ * Delegates to the injected `PoolsProvider`. The handler itself does
1773
+ * not know where pools come from — that's an issuer decision.
1774
+ */
1775
+ handlePools(_userAddress: Address, request: ApiPoolsRequest): Promise<ApiPoolsResponse>;
1776
+ /**
1777
+ * `GET /user?chainId=<id>&user=<addr>&pointToken=<addr>`
1778
+ *
1779
+ * Returns per-user state the frontend needs to build a fresh mint:
1780
+ * on-chain nonces + minter status + off-chain balance.
1781
+ */
1782
+ handleUser(userAddress: Address, request: ApiUserRequest): Promise<ApiUserResponse>;
1783
+ /**
1784
+ * `GET /redemption/preview?pointToken=<addr>`
1785
+ *
1786
+ * Returns the headroom currently available to `userAddress` under the
1787
+ * configured RedemptionPolicy. Pure read — does not record anything.
1788
+ * Use this for UI to render "X PT redeemable now / next available at …".
1789
+ */
1790
+ handleRedemptionPreview(userAddress: Address, request: ApiRedemptionPreviewRequest): Promise<ApiRedemptionPreviewResponse>;
1791
+ /**
1792
+ * `POST /redemption/evaluate`
1793
+ *
1794
+ * Pre-flight check before the issuer signs a BurnRequest. Returns
1795
+ * { allowed, denial?, preview }. Caller (the burn-orchestrator) MUST
1796
+ * re-check on the actual initiate path — evaluate is read-only and a
1797
+ * caller could race two requests under the same headroom. The intended
1798
+ * write path is: evaluate → sign BurnRequest → reserve pending credit
1799
+ * → call `service.redemption.recordSuccessfulInitiate()`.
1800
+ */
1801
+ handleRedemptionEvaluate(userAddress: Address, request: ApiRedemptionEvaluateRequest): Promise<ApiRedemptionEvaluateResponse>;
1802
+ private requireSupportedToken;
1803
+ }
1804
+
1805
+ /**
1806
+ * Resolves the EIP-712 domain `name` for a PointToken. Used by handlers
1807
+ * (PTClaimHandler / PTRedeemHandler) that sign requests for multiple
1808
+ * PointTokens within the same issuer backend.
1809
+ *
1810
+ * Resolution order per address:
1811
+ * 1. Cache hit → return immediately (no RPC)
1812
+ * 2. Static override map (config.overrides) → cache + return
1813
+ * 3. On-chain `name()` lookup → cache + return
1814
+ *
1815
+ * Cache is per-instance — share one resolver across handlers so mint +
1816
+ * redeem paths amortise the same set of lookups.
1817
+ *
1818
+ * Why the override map: production issuers often know their PTs at
1819
+ * boot (env / DB) and want to avoid RPC quota churn + tolerate transient
1820
+ * RPC failures. The on-chain fallback handles ad-hoc PTs not in the map
1821
+ * (e.g. newly deployed during the process lifetime).
1822
+ */
1823
+ interface PointTokenDomainResolverConfig {
1824
+ provider: PublicClient;
1825
+ /**
1826
+ * Optional pre-configured map — checksummed (or lowercased) PointToken
1827
+ * address → ERC-20 `name()` value. When a PT's address is in this map,
1828
+ * the resolver short-circuits without an RPC call.
1829
+ */
1830
+ overrides?: Record<string, string>;
1831
+ }
1832
+ declare class PointTokenDomainResolver {
1833
+ private readonly provider;
1834
+ private readonly overrides;
1835
+ private readonly cache;
1836
+ constructor(config: PointTokenDomainResolverConfig);
1837
+ resolve(pointTokenAddress: Address): Promise<string>;
1838
+ /** Invalidate one address (after deploy / proxy upgrade) or all. */
1839
+ invalidate(pointTokenAddress?: Address): void;
1840
+ }
1841
+
1842
+ /**
1843
+ * Reverse flow — user-initiated PT redeem.
1844
+ *
1845
+ * User has on-chain PT, wants to convert back to off-chain points. The
1846
+ * issuer backend signs a `BurnRequest` with its burner signer, reserves
1847
+ * an off-chain pending credit, and returns an unsigned UserOp. The FE
1848
+ * submits the UserOp via EIP-7702 + PAFI sponsor-relayer. On confirmation,
1849
+ * `Transfer(user → 0x0)` is emitted; `BurnIndexer` resolves the pending
1850
+ * credit to a real off-chain credit.
1851
+ *
1852
+ * Contract path (mock ABI — matches deployed PointToken):
1853
+ *
1854
+ * burn(address from, uint256 amount, uint256 deadline, bytes burnerSig)
1855
+ *
1856
+ * On-chain checks:
1857
+ * - msg.sender == from (enforced via EIP-7702 delegation on user EOA)
1858
+ * - burnerSig signer ∈ burners[]
1859
+ * - nonce == burnRequestNonces[from]
1860
+ * - now <= deadline
1861
+ *
1862
+ * The user never signs an EIP-712 message in this flow. Their only
1863
+ * signature is the ERC-4337 UserOp signature, which the AA wallet
1864
+ * handles. Consent is implicit: by submitting the UserOp they authorize
1865
+ * the burn.
1866
+ */
1867
+ interface PTRedeemHandlerConfig {
1868
+ ledger: IPointLedger;
1869
+ relayService: RelayService;
1870
+ provider: PublicClient;
1871
+ /** BatchExecutor delegation target (chain-specific). */
1872
+ batchExecutorAddress: Address;
1873
+ /** Chain id — used for the BurnRequest EIP-712 domain. */
1874
+ chainId: number;
1875
+ /**
1876
+ * Resolver for the EIP-712 domain `name` (= PointToken.name() on-chain).
1877
+ * Required because mint/redeem now route the `pointTokenAddress` per
1878
+ * request — the handler can't bind a single domain at construction.
1879
+ * Pass a shared resolver instance so mint + redeem amortise the same
1880
+ * cache. The handler always uses `pointTokenAddress` as the EIP-712
1881
+ * `verifyingContract` — no override (per-PT domain separator is
1882
+ * isolated by contract address).
1883
+ */
1884
+ domainResolver: PointTokenDomainResolver;
1885
+ /**
1886
+ * Issuer burner signer wallet — signs the `BurnRequest` EIP-712.
1887
+ * Must be whitelisted via `PointToken.addBurner(signerAddr)` at
1888
+ * provisioning time. Typically HSM/KMS-backed in prod.
1889
+ */
1890
+ burnerSignerWallet: WalletClient;
1891
+ /**
1892
+ * Issuer's allow-listed PointToken contracts (checksummed EIP-55).
1893
+ * Every `handle()` call validates the request's `pointTokenAddress`
1894
+ * against this set BEFORE any chain read, signer call, or pending
1895
+ * credit reservation.
1896
+ * Pass the SAME set used to construct `IssuerApiHandlers` and
1897
+ * `PTClaimHandler` so the read, claim, and redeem paths agree on
1898
+ * what this issuer is willing to sign for.
1899
+ */
1900
+ supportedTokens: ReadonlySet<Address>;
1901
+ /**
1902
+ * Optional — when wired, used to estimate the PT gas-reimbursement
1903
+ * fee. The handler self-computes `feeAmount` + `feeRecipient` so the
1904
+ * caller doesn't repeat the boilerplate at each endpoint. Pass
1905
+ * `feeAmount` on `PTRedeemRequest` to override.
1906
+ */
1907
+ feeService?: FeeManager;
1908
+ /**
1909
+ * How long the pending credit stays reserved if the burn never lands.
1910
+ * Default: 15 min — long enough for a bundler submission + confirmation.
1911
+ */
1912
+ redeemLockDurationMs?: number;
1913
+ /**
1914
+ * How far ahead of `now` to set the BurnRequest deadline. Default:
1915
+ * 15 min. Must be long enough for Bundler + EntryPoint to execute;
1916
+ * short enough to prevent replay if the UserOp is abandoned.
1917
+ */
1918
+ signatureDeadlineSeconds?: number;
1919
+ /** Clock injection for tests; defaults to `Date.now`. */
1920
+ now?: () => number;
1921
+ /**
1922
+ * Optional — when wired, the handler enforces the per-issuer
1923
+ * RedemptionPolicy (daily limit / cooldown / blackout / per-tx range)
1924
+ * BEFORE signing the BurnRequest. On denial it throws
1925
+ * PTRedeemError("REDEMPTION_POLICY_DENIED", ...) with the policy
1926
+ * denial code in `policyDenialCode`. After a successful sign+reserve,
1927
+ * the handler records the redemption against the user's history so
1928
+ * the next preview reflects the spend.
1929
+ */
1930
+ redemptionService?: RedemptionService;
1931
+ }
1932
+ interface PTRedeemRequest {
1933
+ /** Address extracted from the verified JWT — must match `userAddress`. */
1934
+ authenticatedAddress: Address;
1935
+ userAddress: Address;
1936
+ amount: bigint;
1937
+ /** PointToken contract to redeem. The handler resolves the EIP-712
1938
+ * domain (name + verifyingContract) from this address via the
1939
+ * configured `domainResolver`. */
1940
+ pointTokenAddress: Address;
1941
+ /** ERC-4337 account nonce for the user's EOA. */
1942
+ aaNonce: bigint;
1943
+ /**
1944
+ * Optional override — explicit PT fee transfer appended after the
1945
+ * burn (sponsored path). When omitted, the handler auto-computes
1946
+ * via `feeService.estimateGasFee()` (if configured) and resolves
1947
+ * the recipient from `getContractAddresses(chainId).pafiFeeRecipient`.
1948
+ */
1949
+ feeAmount?: bigint;
1950
+ feeRecipient?: Address;
1951
+ /**
1952
+ * Optional — required only when the handler self-computes the
1953
+ * recipient. When the caller passes an explicit `feeRecipient`, this
1954
+ * is ignored.
1955
+ */
1956
+ chainId?: number;
1957
+ }
1958
+ interface PTRedeemResponse {
1959
+ /**
1960
+ * Sponsored path — UserOp with PT fee transfer. BurnRequest signed for
1961
+ * `request.amount - feeAmount`. User holds exactly `request.amount` PT
1962
+ * and the fee comes out of the redeem amount, NOT on top.
1963
+ */
1964
+ lockId: string;
1965
+ userOp: PartialUserOperation;
1966
+ /** = request.amount - feeAmount. What BurnIndexer credits off-chain on sponsored fire. */
1967
+ netCreditAmount: bigint;
1968
+ /**
1969
+ * Resolved fee — computed via `feeService.estimateGasFee()` when the
1970
+ * caller didn't override on the request, else echoed back from the
1971
+ * request override. Useful for the FE confirmation screen.
1972
+ */
1973
+ feeAmount: bigint;
1974
+ /**
1975
+ * Fallback path — UserOp with NO fee transfer. BurnRequest signed for
1976
+ * full `request.amount`. User pays gas in ETH directly. Present only
1977
+ * when `feeAmount > 0`.
1978
+ *
1979
+ * Same nonce as sponsored: only one of the two can fire on-chain;
1980
+ * the other lock auto-expires after `expiresInSeconds`. BurnIndexer
1981
+ * matches by burn amount and resolves the correct lock.
1982
+ */
1983
+ fallback?: {
1984
+ lockId: string;
1985
+ userOp: PartialUserOperation;
1986
+ /** = request.amount. Full credit when fallback fires. */
1987
+ netCreditAmount: bigint;
1988
+ };
1989
+ /** Seconds until the lock expires if the burn doesn't land. */
1990
+ expiresInSeconds: number;
1991
+ /** The BurnRequest deadline (unix seconds) — FE uses this to surface a countdown. */
1992
+ signatureDeadline: bigint;
1993
+ }
1994
+ type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED" | "REDEMPTION_POLICY_UNAVAILABLE" | "UNSUPPORTED_POINT_TOKEN";
1995
+ declare class PTRedeemError extends PafiSdkError {
1996
+ readonly httpStatus: "unprocessable";
1997
+ readonly code: PTRedeemErrorCode;
1998
+ readonly policyDenialCode?: RedemptionDenialCode;
1999
+ constructor(code: PTRedeemErrorCode, message: string, options?: {
2000
+ policyDenialCode?: RedemptionDenialCode;
2001
+ });
2002
+ }
2003
+ declare class PTRedeemHandler {
2004
+ private readonly ledger;
2005
+ private readonly relayService;
2006
+ private readonly provider;
2007
+ private readonly feeService?;
2008
+ private readonly batchExecutorAddress;
2009
+ private readonly chainId;
2010
+ private readonly domainResolver;
2011
+ private readonly burnerSignerWallet;
2012
+ private readonly supportedTokens;
2013
+ private readonly redeemLockDurationMs;
2014
+ private readonly signatureDeadlineSeconds;
2015
+ private readonly now;
2016
+ private readonly redemptionService?;
2017
+ /**
2018
+ * Per-user in-flight nonce guard (single-process only).
2019
+ *
2020
+ * Prevents two concurrent requests from reading the same on-chain
2021
+ * burnRequestNonce before either has completed, which would produce two
2022
+ * signed UserOps with the same nonce — only one succeeds on-chain; the
2023
+ * other leaves an orphaned pending credit and a wasted signer call.
2024
+ *
2025
+ * NOTE: This guard is effective only within a single Node.js process. For
2026
+ * multi-instance deployments (k8s, PM2 cluster), enforce mutual exclusion
2027
+ * via a distributed lock (Redis SETNX / Postgres advisory lock) keyed on
2028
+ * `(userAddress, pointTokenAddress)` BEFORE calling `handle()`.
2029
+ */
2030
+ private readonly inFlightNonces;
2031
+ constructor(config: PTRedeemHandlerConfig);
2032
+ handle(request: PTRedeemRequest): Promise<PTRedeemResponse>;
2033
+ private _handleAfterNonceLock;
2034
+ }
2035
+
2036
+ interface RetryConfig {
2037
+ maxAttempts?: number;
2038
+ initialDelayMs?: number;
2039
+ maxDelayMs?: number;
2040
+ maxRetryAfterMs?: number;
2041
+ }
2042
+ interface PafiBackendConfig {
2043
+ /**
2044
+ * chainId — used to derive the sponsor-relayer base URL via
2045
+ * `getPafiServiceUrls(chainId).sponsorRelayer`. SDK ships with the
2046
+ */
2047
+ chainId: number;
2048
+ baseUrl?: string;
2049
+ issuerId: string;
2050
+ apiKey: string;
2051
+ fetchImpl?: typeof fetch;
2052
+ timeoutMs?: number;
2053
+ retry?: RetryConfig;
2054
+ }
2055
+ type PafiBackendErrorCode = "MISSING_ISSUER_ID" | "MISSING_API_KEY" | "ISSUER_UNAUTHORIZED" | "USER_UNAUTHORIZED" | "INTENT_REJECTED" | "MINT_CAP_EXCEEDED" | "ISSUER_INACTIVE" | "BROKER_NOT_WHITELISTED" | "RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_EXCEEDED_DAILY" | "RATE_LIMIT_EXCEEDED_PER_USER" | "ISSUER_BUDGET_EXCEEDED" | "RATE_LIMITER_UNAVAILABLE" | "PAYMASTER_UNAVAILABLE" | "TARGET_NOT_ALLOWLISTED" | "BAD_REQUEST" | "INTERNAL_ERROR" | "TIMEOUT" | "NETWORK_ERROR" | (string & {});
2056
+ declare class PafiBackendError extends Error {
2057
+ code: PafiBackendErrorCode;
2058
+ httpStatus: number;
2059
+ details?: unknown | undefined;
2060
+ readonly retryAfter?: number;
2061
+ private readonly serverSafeToRetry?;
2062
+ constructor(code: PafiBackendErrorCode, message: string, httpStatus: number, details?: unknown | undefined, opts?: {
2063
+ retryAfter?: number;
2064
+ safeToRetry?: boolean;
2065
+ });
2066
+ get safeToRetry(): boolean;
2067
+ }
2068
+
2069
+ interface RelayUserOpRequest {
2070
+ userOp: Record<string, string | null>;
2071
+ entryPoint: string;
2072
+ eip7702Auth?: {
2073
+ chainId: string;
2074
+ address: string;
2075
+ nonce: string;
2076
+ r: string;
2077
+ s: string;
2078
+ yParity: string;
2079
+ };
2080
+ }
2081
+ interface RelayUserOpResponse {
2082
+ userOpHash: Hex;
2083
+ }
2084
+ interface SponsorshipUserOp {
2085
+ sender: Address;
2086
+ nonce: bigint;
2087
+ callData: Hex;
2088
+ callGasLimit: bigint;
2089
+ verificationGasLimit: bigint;
2090
+ preVerificationGas: bigint;
2091
+ maxFeePerGas: bigint;
2092
+ maxPriorityFeePerGas: bigint;
2093
+ }
2094
+ interface SponsorshipTarget {
2095
+ contract: Address;
2096
+ function: string;
2097
+ pointToken: Address;
2098
+ }
2099
+ interface SponsorshipRequest {
2100
+ chainId: number;
2101
+ scenario: string;
2102
+ userOp: SponsorshipUserOp;
2103
+ target: SponsorshipTarget;
2104
+ /**
2105
+ * EIP-7702 authorization tuple — REQUIRED for the `delegate`
2106
+ * scenario so Pimlico's `pm_sponsorUserOperation` can simulate the
2107
+ * UserOp with the EOA already delegated. Without it, simulator
2108
+ * reverts `AA20 account not deployed` (chicken-and-egg: the same
2109
+ * UserOp anchors the delegation).
2110
+ *
2111
+ * Optional for non-delegate scenarios where the sender already
2112
+ * has bytecode (i.e. delegated previously). v0.7.5 added.
2113
+ */
2114
+ eip7702Auth?: {
2115
+ chainId: string;
2116
+ address: string;
2117
+ nonce: string;
2118
+ r: string;
2119
+ s: string;
2120
+ yParity: string;
2121
+ };
2122
+ }
2123
+ interface SponsorshipResponse {
2124
+ paymaster: Address;
2125
+ paymasterData: Hex;
2126
+ paymasterVerificationGasLimit: bigint;
2127
+ paymasterPostOpGasLimit: bigint;
2128
+ /**
2129
+ * Pimlico's `pm_sponsorUserOperation` re-estimates these gas fields
2130
+ * and signs its paymaster signature over the new values. Callers
2131
+ * MUST overwrite the matching userOp fields with these BEFORE
2132
+ * computing the EIP-712 userOpHash and submitting to the bundler.
2133
+ * Otherwise both `AA34` (paymaster sig) and `AA24` (sender sig) will
2134
+ * fire — the EntryPoint hashes the actual on-chain field values, and
2135
+ * a mismatch invalidates every sig over the hash.
2136
+ */
2137
+ callGasLimit?: bigint;
2138
+ verificationGasLimit?: bigint;
2139
+ preVerificationGas?: bigint;
2140
+ /**
2141
+ * Bundler-required gas price (Pimlico's `pimlico_getUserOperationGasPrice`
2142
+ * fast tier). Pimlico's paymaster signs over these — caller MUST apply
2143
+ * to the userOp before computing the EIP-712 userOpHash. Base RPC's
2144
+ * `eth_feeHistory` underestimates the bundler floor by 10-15 %, so
2145
+ * relying on it produces "maxFeePerGas must be at least ..." rejections.
2146
+ */
2147
+ maxFeePerGas?: bigint;
2148
+ maxPriorityFeePerGas?: bigint;
2149
+ expiresAt: number;
2150
+ }
2151
+ declare class PafiBackendClient {
2152
+ private readonly config;
2153
+ private readonly baseUrl;
2154
+ constructor(config: PafiBackendConfig);
2155
+ requestSponsorship(request: SponsorshipRequest): Promise<SponsorshipResponse>;
2156
+ /**
2157
+ * Fetch ERC-4337 UserOp receipt via PAFI's authenticated bundler proxy.
2158
+ * Returns `null` when the bundler hasn't seen the userOp yet — caller
2159
+ * should keep polling. Used by status endpoints to short-circuit the
2160
+ * on-chain indexer when several PENDING locks share the same amount.
2161
+ */
2162
+ getUserOpReceipt(userOpHash: Hex): Promise<{
2163
+ success: boolean;
2164
+ txHash: Hex;
2165
+ blockNumber: string;
2166
+ } | null>;
2167
+ relayUserOperation(request: RelayUserOpRequest): Promise<RelayUserOpResponse>;
2168
+ private _doRequest;
2169
+ }
2170
+
2171
+ /**
2172
+ * Status snapshot returned to mobile clients polling a mint lock.
2173
+ * Matches the legacy gg56 shape so existing FE code keeps working.
2174
+ */
2175
+ interface MintStatusResponse {
2176
+ lockId: string;
2177
+ status: "PENDING" | "MINTED" | "EXPIRED" | "FAILED";
2178
+ txHash: Hex | null;
2179
+ amount: string;
2180
+ createdAt: string;
2181
+ expiresAt: string;
2182
+ }
2183
+ interface BurnStatusResponse {
2184
+ lockId: string;
2185
+ status: "PENDING" | "RESOLVED" | "EXPIRED";
2186
+ txHash: Hex | null;
2187
+ amount: string;
2188
+ createdAt: string;
2189
+ expiresAt: string;
2190
+ resolvedAt?: string | null;
2191
+ }
2192
+ interface MintStatusParams {
2193
+ lockId: string;
2194
+ /** User EOA from auth — handler returns 404 if the lock isn't theirs. */
2195
+ userAddress: Address;
2196
+ ledger: IPointLedger;
2197
+ /**
2198
+ * PAFI backend client for the bundler-receipt fallback. Optional —
2199
+ * when omitted, status only reflects what the on-chain `PointIndexer`
2200
+ * has finalized into the ledger. With it, the handler queries the
2201
+ * bundler receipt when:
2202
+ * - lock.status === "PENDING"
2203
+ * - lock.userOpHash is bound (set by `/claim/submit`)
2204
+ *
2205
+ * If the bundler reports the UserOp confirmed AND the receipt block
2206
+ * is past the configured `confirmations` depth, the handler updates
2207
+ * the ledger lock + returns `MINTED` immediately, bypassing
2208
+ * `PointIndexer`'s amount-based race (multiple PENDING locks with
2209
+ * the same amount can be matched to the wrong tx_hash).
2210
+ */
2211
+ pafiBackendClient?: PafiBackendClient | null;
2212
+ /**
2213
+ * Guard for the bundler-receipt fallback. The bundler returns success at zero
2214
+ * confs, but `PointIndexer` enforces a 3-block reorg window;
2215
+ * crediting / debiting off-chain at 0 confs while the indexer waits
2216
+ * for finality leaves an unbacked durable mutation if the tx is
2217
+ * reorged out. Pass the SAME PublicClient that the indexer uses so
2218
+ * both paths see consistent chain head. Optional only for legacy
2219
+ * callers that don't supply `pafiBackendClient`; required whenever
2220
+ * the receipt-fallback path can run.
2221
+ */
2222
+ provider?: PublicClient;
2223
+ /**
2224
+ * Confirmation depth required before the receipt
2225
+ * fallback applies the credit / debit. MUST match
2226
+ * `PointIndexer.confirmations` (default 3). Operators who reduce
2227
+ * the indexer depth must set this to the same value.
2228
+ */
2229
+ confirmations?: number;
2230
+ /** Optional logger for "ledger update failed" warnings. */
2231
+ onWarning?: (msg: string) => void;
2232
+ }
2233
+ interface BurnStatusParams {
2234
+ lockId: string;
2235
+ userAddress: Address;
2236
+ ledger: IPointLedger;
2237
+ pafiBackendClient?: PafiBackendClient | null;
2238
+ /**
2239
+ * BurnStatusParams.provider` for full
2240
+ * rationale. The receipt-fallback path applies a spendable
2241
+ * off-chain credit; without a confirmation-depth gate a reorg
2242
+ * before `BurnIndexer.confirmations` leaves durable unbacked
2243
+ * credit with no reversal mechanism.
2244
+ */
2245
+ provider?: PublicClient;
2246
+ /**
2247
+ * Confirmation depth required before the receipt
2248
+ * fallback applies the credit. MUST match
2249
+ * `BurnIndexer.confirmations` (default 3).
2250
+ */
2251
+ confirmations?: number;
2252
+ onWarning?: (msg: string) => void;
2253
+ }
2254
+ declare class LockNotFoundError extends PafiSdkError {
2255
+ readonly code = "LOCK_NOT_FOUND";
2256
+ readonly httpStatus: "not_found";
2257
+ constructor();
2258
+ }
2259
+
2260
+ /**
2261
+ * Handle GET /claim/status/:lockId.
2262
+ *
2263
+ * Returns the lock's current state from the ledger. When the ledger
2264
+ * supports `userOpHash` binding AND the lock is still PENDING, falls
2265
+ * back to the bundler receipt — the canonical truth that bypasses
2266
+ * `PointIndexer`'s amount-based matching race.
2267
+ *
2268
+ * Throws `LockNotFoundError` when the lock doesn't exist or doesn't
2269
+ * belong to `userAddress`. Caller maps to HTTP 404.
2270
+ */
2271
+ declare function handleClaimStatus(params: MintStatusParams): Promise<MintStatusResponse>;
2272
+ /**
2273
+ * Handle GET /redeem/status/:lockId. Symmetric to `handleClaimStatus`
2274
+ * for the burn → off-chain credit flow.
2275
+ *
2276
+ * Bundler-receipt fallback resolves the credit via
2277
+ * `ledger.resolveCreditByBurnTx` when the bundler reports the burn
2278
+ * UserOp succeeded.
2279
+ */
2280
+ declare function handleRedeemStatus(params: BurnStatusParams): Promise<BurnStatusResponse>;
2281
+
2282
+ /**
2283
+ * A pending UserOp serialized for persistent storage (Redis, Postgres, memory).
2284
+ *
2285
+ * All bigint fields are stored as decimal strings so the entry can be
2286
+ * JSON-serialized without precision loss. Convert back to bigint before
2287
+ * calling `computeUserOpHash` or `serializeUserOpToJsonRpc`.
2288
+ */
2289
+ interface PendingUserOpEntry {
2290
+ sender: Address;
2291
+ nonce: string;
2292
+ callData: Hex;
2293
+ callGasLimit: string;
2294
+ verificationGasLimit: string;
2295
+ preVerificationGas: string;
2296
+ maxFeePerGas: string;
2297
+ maxPriorityFeePerGas: string;
2298
+ paymaster?: Address;
2299
+ paymasterVerificationGasLimit?: string;
2300
+ paymasterPostOpGasLimit?: string;
2301
+ paymasterData?: Hex;
2302
+ chainId: number;
2303
+ /** Hex-encoded userOpHash — the value the user signed via personal_sign. */
2304
+ userOpHash: Hex;
2305
+ /**
2306
+ * Fee-stripped fallback variant. Set by `/claim/prepare` and
2307
+ * `/redeem/prepare` when a PT operator fee is configured AND the
2308
+ * paymaster sponsorship attached successfully — i.e. the user might
2309
+ * still want to submit without paymaster (paying ETH gas), and in
2310
+ * that case shouldn't be charged the PT fee. `/claim/submit` reads
2311
+ * this branch when its request body specifies
2312
+ * `variant: "fallback"`.
2313
+ *
2314
+ * Has a different `callData` (no PT.transfer prepended) and
2315
+ * therefore a different `userOpHash`. Paymaster fields are NOT
2316
+ * present — the fallback is by definition unsponsored.
2317
+ */
2318
+ fallback?: {
2319
+ callData: Hex;
2320
+ callGasLimit: string;
2321
+ verificationGasLimit: string;
2322
+ preVerificationGas: string;
2323
+ userOpHash: Hex;
2324
+ /**
2325
+ * Lock id (PendingCredit on redeem, LockedMint on claim) reserved
2326
+ * for the FALLBACK path — distinct from the outer entry's
2327
+ * sponsored lock because the on-chain amount the user burns/mints
2328
+ * differs between variants.
2329
+ *
2330
+ * If `handleMobileSubmit.bindUserOpHash` bound the submitted
2331
+ * userOpHash to the SPONSORED lockId even when the user submitted
2332
+ * the fallback variant, the bundler-receipt fallback in
2333
+ * `handleRedeemStatus` would resolve the sponsored credit
2334
+ * (`amount - fee`) against the on-chain burn of the FULL `amount`
2335
+ * — every fee-bearing fallback redeem would permanently
2336
+ * under-credit the user by exactly the fee. Surface the fallback
2337
+ * lockId here so submit can route the bind to the correct ledger
2338
+ * row.
2339
+ */
2340
+ lockId?: string;
2341
+ };
2342
+ /**
2343
+ * EIP-7702 authorization tuple — present only on the `delegate`
2344
+ * scenario where the UserOp anchors the EOA's one-time delegation
2345
+ * to a 7702 implementation. Embedded into the eth_sendUserOperation
2346
+ * payload at submit time so Pimlico bundler applies the bytecode
2347
+ * atomically with the UserOp execution. Pre-computed at prepare
2348
+ * time from the user's `signAuthorization` signature so submit
2349
+ * doesn't need to re-derive it.
2350
+ *
2351
+ * v0.7.7 — added per delegate-flow refactor (mobile prepare/submit).
2352
+ */
2353
+ eip7702Auth?: {
2354
+ chainId: string;
2355
+ address: string;
2356
+ nonce: string;
2357
+ r: string;
2358
+ s: string;
2359
+ yParity: string;
2360
+ };
2361
+ }
2362
+ /**
2363
+ * Storage backend for pending UserOps in the mobile prepare/submit pattern.
2364
+ *
2365
+ * Implement this interface and wire it into your issuer backend:
2366
+ * - `save()` — called by `POST /claim/prepare` and `POST /redeem/prepare`
2367
+ * - `get()` — called by `POST /claim/submit` and `POST /redeem/submit`
2368
+ * - `delete()` — called after successful submit or explicit cancellation
2369
+ *
2370
+ * The default implementation in the gg56 boilerplate uses Redis with
2371
+ * a short TTL matching the MintRequest / BurnRequest deadline.
2372
+ */
2373
+ interface IPendingUserOpStore {
2374
+ save(lockId: string, entry: PendingUserOpEntry, ttlSeconds: number): Promise<void>;
2375
+ get(lockId: string): Promise<PendingUserOpEntry | null>;
2376
+ delete(lockId: string): Promise<void>;
2377
+ }
2378
+
2379
+ /**
2380
+ * Convert a stored `PendingUserOpEntry` (decimal-string fields) plus a
2381
+ * signature into the JSON-RPC wire format for `eth_sendUserOperation`.
2382
+ *
2383
+ * Bridges the gap between the serialized storage format (decimal strings,
2384
+ * safe for JSON/Redis) and `serializeUserOpToJsonRpc` which expects bigints.
2385
+ */
2386
+ declare function serializeEntryToJsonRpc(entry: PendingUserOpEntry, signature: Hex, variant?: "sponsored" | "fallback"): Record<string, string | null>;
2387
+
2388
+ /**
2389
+ * In-memory `IPendingUserOpStore` for **single-instance dev / test
2390
+ * harnesses only**.
2391
+ *
2392
+ * Multi-instance deployments (k8s, PM2 cluster) MUST use a shared store
2393
+ * — typically Redis with a TTL key or Postgres with `expires_at`. If
2394
+ * `prepare` lands on instance A and `submit` on instance B, an
2395
+ * in-memory store on A loses the entry.
2396
+ *
2397
+ * Entries are evicted lazily on `get()` if expired. Periodic sweep is
2398
+ * not implemented — the in-memory map's footprint scales with
2399
+ * outstanding pending UserOps, which is bounded by the issuer's lock
2400
+ * duration (default 15 min).
2401
+ */
2402
+ declare class MemoryPendingUserOpStore implements IPendingUserOpStore {
2403
+ private readonly entries;
2404
+ private readonly now;
2405
+ constructor(now?: () => number);
2406
+ save(lockId: string, entry: PendingUserOpEntry, ttlSeconds: number): Promise<void>;
2407
+ get(lockId: string): Promise<PendingUserOpEntry | null>;
2408
+ delete(lockId: string): Promise<void>;
2409
+ }
2410
+
2411
+ /**
2412
+ * Re-shape `UserOpTypedData` so all `bigint` fields become hex strings —
2413
+ * required for JSON transport over HTTP. Mirrors the inverse of what
2414
+ * `walletClient.signTypedData` accepts on the client (it auto-coerces hex
2415
+ * strings back to bigints for `uint256` fields).
2416
+ */
2417
+ type SerializedUserOpTypedData = {
2418
+ domain: UserOpTypedData["domain"];
2419
+ types: UserOpTypedData["types"];
2420
+ primaryType: UserOpTypedData["primaryType"];
2421
+ message: {
2422
+ sender: Address;
2423
+ nonce: Hex;
2424
+ initCode: Hex;
2425
+ callData: Hex;
2426
+ accountGasLimits: Hex;
2427
+ preVerificationGas: Hex;
2428
+ gasFees: Hex;
2429
+ paymasterAndData: Hex;
2430
+ };
2431
+ };
2432
+ /**
2433
+ * Convert a `UserOpTypedData` payload into the JSON-safe wire form
2434
+ * (bigint → hex string). The mobile client passes this directly to
2435
+ * `eth_signTypedData_v4` / viem's `signTypedData`.
2436
+ */
2437
+ declare function serializeUserOpTypedData(td: UserOpTypedData): SerializedUserOpTypedData;
2438
+ /**
2439
+ * Merge Pimlico's paymaster-sponsorship response into a UserOp
2440
+ * skeleton, applying only fields that are actually defined.
2441
+ *
2442
+ * `pm_sponsorUserOperation` returns:
2443
+ * - `paymaster` / `paymasterData` — required for the sponsored sig
2444
+ * - `paymasterVerificationGasLimit` / `paymasterPostOpGasLimit`
2445
+ * - **re-estimated** `callGasLimit` / `verificationGasLimit` /
2446
+ * `preVerificationGas` — the paymaster signature is computed over
2447
+ * these new values
2448
+ * - **bundler-required** `maxFeePerGas` / `maxPriorityFeePerGas` —
2449
+ * Base RPC's `eth_feeHistory` underestimates, bundler rejects
2450
+ *
2451
+ * Callers MUST re-merge ALL of these into the userOp BEFORE computing
2452
+ * the EIP-712 userOpHash — otherwise both the paymaster signature
2453
+ * (AA34) and the user signature (AA24, recovered against a different
2454
+ * hash) fail at validation.
2455
+ *
2456
+ * Skips fields that are undefined so legacy paymaster responses
2457
+ * (without re-estimated gas) don't accidentally zero out the original
2458
+ * estimates.
2459
+ */
2460
+ /**
2461
+ * Subset of `SponsorshipResponse` consumed by the merge/hash helpers.
2462
+ * Inlined as a structural type (rather than importing
2463
+ * `SponsorshipResponse` from `pafi-backend/client`) to keep
2464
+ * `userop-store` independent of the HTTP-client module.
2465
+ */
2466
+ interface PaymasterGasEstimates {
2467
+ paymaster: Address;
2468
+ paymasterData: Hex;
2469
+ paymasterVerificationGasLimit: bigint;
2470
+ paymasterPostOpGasLimit: bigint;
2471
+ callGasLimit?: bigint;
2472
+ verificationGasLimit?: bigint;
2473
+ preVerificationGas?: bigint;
2474
+ maxFeePerGas?: bigint;
2475
+ maxPriorityFeePerGas?: bigint;
2476
+ }
2477
+ declare function mergePaymasterFields<T extends object>(userOp: T, paymasterFields: PaymasterGasEstimates | undefined): T;
2478
+ interface PreparedUserOp {
2479
+ /** The bundler-ready UserOp (with paymaster + Pimlico-quoted gas). */
2480
+ userOp: PartialUserOperation & {
2481
+ maxFeePerGas: bigint;
2482
+ maxPriorityFeePerGas: bigint;
2483
+ paymaster?: Address;
2484
+ paymasterData?: Hex;
2485
+ paymasterVerificationGasLimit?: bigint;
2486
+ paymasterPostOpGasLimit?: bigint;
2487
+ };
2488
+ /** Hex-encoded EIP-712 digest. Equals `EntryPoint.getUserOpHash`. */
2489
+ userOpHash: Hex;
2490
+ /** Typed-data payload — pass directly to `eth_signTypedData_v4`. */
2491
+ typedData: SerializedUserOpTypedData;
2492
+ }
2493
+ /**
2494
+ * Atomic "merge paymaster fields + compute hash + build typed data" —
2495
+ * the only place the SDK should be doing all three operations.
2496
+ *
2497
+ * Why bundled into one call:
2498
+ *
2499
+ * Pimlico's `pm_sponsorUserOperation` re-estimates
2500
+ * `callGasLimit` / `verificationGasLimit` / `preVerificationGas` and
2501
+ * the bundler fee fields (`maxFeePerGas` / `maxPriorityFeePerGas`),
2502
+ * then signs `paymasterData` over the new values. The EntryPoint
2503
+ * hashes the actual on-chain field values during validation, so
2504
+ * callers MUST overwrite the matching userOp fields BEFORE
2505
+ * computing the userOpHash. Forgetting any field triggers AA34
2506
+ * (paymaster signature mismatch) and/or AA24 (sender signature
2507
+ * mismatch) — opaque failures that take hours to debug.
2508
+ *
2509
+ * Splitting "merge" and "hash" into two free functions invites
2510
+ * exactly this bug: each call site has to reinvent the merge logic,
2511
+ * and any drift between sites silently produces stale hashes.
2512
+ * Returning all three pieces (`userOp`, `userOpHash`, `typedData`)
2513
+ * from a single call makes "hash before merge" structurally
2514
+ * impossible: there is no intermediate userOp to compute a stale
2515
+ * hash over.
2516
+ *
2517
+ * Behaviour:
2518
+ * - When `paymasterFields` is undefined (paymaster declined / Path
2519
+ * C self-pay), the result is just the hash + typed-data of the
2520
+ * caller's `partialUserOp` — no fields are touched.
2521
+ * - When provided, only defined keys in `paymasterFields` overwrite
2522
+ * the matching fields. Original gas/fee estimates survive when
2523
+ * the paymaster did not re-estimate.
2524
+ *
2525
+ * Replaces:
2526
+ * - manual spread merges (e.g. previous `delegateHandler.ts`)
2527
+ * - `mergePaymasterFields` + separate `computeUserOpHash` +
2528
+ * `buildUserOpTypedData` triplets in any new call site.
2529
+ *
2530
+ * Note: the original `mergePaymasterFields` and the raw
2531
+ * `computeUserOpHash` / `buildUserOpTypedData` exports remain
2532
+ * available — use this helper unless you have a specific reason to
2533
+ * stage the operations.
2534
+ */
2535
+ declare function applyPaymasterGasEstimates(partialUserOp: PartialUserOperation & {
2536
+ maxFeePerGas: bigint;
2537
+ maxPriorityFeePerGas: bigint;
2538
+ }, paymasterFields: PaymasterGasEstimates | undefined, chainId: number): PreparedUserOp;
2539
+ interface PrepareMobileUserOpParams {
2540
+ /** Lock id (issuer-generated) keying both store entry + ledger row. */
2541
+ lockId: string;
2542
+ /**
2543
+ * Sponsored variant — built with the PT operator-fee transfer
2544
+ * included. SDK or caller should set `partialUserOp.maxFeePerGas` /
2545
+ * `maxPriorityFeePerGas` from `provider.estimateFeesPerGas()` before
2546
+ * calling — they get overridden by Pimlico's quote anyway, but
2547
+ * they must be valid bigints for the merge.
2548
+ */
2549
+ partialUserOp: PartialUserOperation & {
2550
+ maxFeePerGas: bigint;
2551
+ maxPriorityFeePerGas: bigint;
2552
+ };
2553
+ /**
2554
+ * Optional fee-stripped fallback variant — built with `gasFeePt: 0n`
2555
+ * (no PT operator-fee transfer). Submitted when paymaster refuses
2556
+ * sponsorship and the user pays ETH gas directly.
2557
+ */
2558
+ partialUserOpFallback?: PartialUserOperation & {
2559
+ maxFeePerGas?: bigint;
2560
+ maxPriorityFeePerGas?: bigint;
2561
+ };
2562
+ /**
2563
+ * Optional separate lock id for the FALLBACK path — required when
2564
+ * the upstream handler reserves a DIFFERENT ledger row for the
2565
+ * fallback variant (PTRedeemHandler reserves
2566
+ * `amount` for fallback vs `amount - fee` for sponsored).
2567
+ *
2568
+ * When omitted, `handleMobileSubmit` binds the fallback userOpHash
2569
+ * to the outer sponsored `lockId`, which makes
2570
+ * `handleRedeemStatus`'s bundler-receipt fallback resolve the
2571
+ * SMALLER sponsored credit against the on-chain burn of the FULL
2572
+ * amount — every fee-bearing fallback redeem then permanently
2573
+ * under-credits the user by exactly the fee. Pass the fallback
2574
+ * lockId here so submit can route the bind to the correct row.
2575
+ */
2576
+ lockIdFallback?: string;
2577
+ /** Paymaster sponsorship response, or `undefined` if PAFI declined. */
2578
+ paymasterFields?: {
2579
+ paymaster: Address;
2580
+ paymasterData: Hex;
2581
+ paymasterVerificationGasLimit: bigint;
2582
+ paymasterPostOpGasLimit: bigint;
2583
+ callGasLimit?: bigint;
2584
+ verificationGasLimit?: bigint;
2585
+ preVerificationGas?: bigint;
2586
+ maxFeePerGas?: bigint;
2587
+ maxPriorityFeePerGas?: bigint;
2588
+ };
2589
+ chainId: number;
2590
+ /** Pending-userop store implementation (Redis/Postgres/Memory). */
2591
+ store: IPendingUserOpStore;
2592
+ /** TTL the store entry should outlive — typically the MintRequest deadline. */
2593
+ ttlSeconds: number;
2594
+ /**
2595
+ * Optional EIP-7702 authorization tuple. Persisted in the store entry
2596
+ * so `handleMobileSubmit` can replay it to the bundler at submit time.
2597
+ * NOT folded into the UserOp hash — Pimlico applies SetCode atomically
2598
+ * but the authorization itself is a top-level field of
2599
+ * `eth_sendUserOperation`, not part of `PackedUserOperation`. Hash
2600
+ * stays stable so the user's typed-data signature remains valid.
2601
+ */
2602
+ eip7702Auth?: {
2603
+ chainId: string;
2604
+ address: string;
2605
+ nonce: string;
2606
+ r: string;
2607
+ s: string;
2608
+ yParity: string;
2609
+ };
2610
+ }
2611
+ interface PrepareMobileUserOpResult {
2612
+ sponsored: PreparedUserOp;
2613
+ /**
2614
+ * Set when `partialUserOpFallback` was supplied AND the PT fee was
2615
+ * non-zero (i.e. sponsored ≠ fallback). Mobile client picks which
2616
+ * variant to sign + submit; SDK's `serializeEntryToJsonRpc` reads
2617
+ * the `variant` flag to dispatch.
2618
+ */
2619
+ fallback?: PreparedUserOp;
2620
+ /** What got persisted into the pending-userop store. */
2621
+ entry: PendingUserOpEntry;
2622
+ }
2623
+ /**
2624
+ * Build the sponsored UserOp + (optional) fee-stripped fallback for the
2625
+ * mobile prepare/submit flow.
2626
+ *
2627
+ * What this does, end-to-end:
2628
+ * 1. Merge Pimlico's paymaster sponsorship + re-quoted gas into the
2629
+ * caller's `partialUserOp` skeleton.
2630
+ * 2. Compute the EIP-712 userOpHash + the typed-data payload (in
2631
+ * JSON-safe form for HTTP transport).
2632
+ * 3. (Optional) Repeat for the `partialUserOpFallback` skeleton with
2633
+ * no paymaster fields — produces a separate hash + typed-data so
2634
+ * the client can re-sign if it falls back.
2635
+ * 4. Persist a single store entry containing BOTH callData variants
2636
+ * keyed by lockId. `serializeEntryToJsonRpc` reads the `variant`
2637
+ * param at submit time.
2638
+ *
2639
+ * Replaces ~100 LoC of glue per scenario in issuer controllers.
2640
+ */
2641
+ declare function prepareMobileUserOp(params: PrepareMobileUserOpParams): Promise<PrepareMobileUserOpResult>;
2642
+
2643
+ /**
2644
+ * Mobile prepare/submit orchestrators — abstract the duplicate glue
2645
+ * between `/claim/prepare`+`/claim/submit` and `/redeem/prepare`+
2646
+ * `/redeem/submit`. Both share the same shape:
2647
+ *
2648
+ * prepare: fees + delegation check → paymaster → prepareMobileUserOp
2649
+ * submit: fetch entry → serialize+sign → relay → bind hash → delete
2650
+ *
2651
+ * Issuer controllers shrink to ~30 LoC each — wire the handler that
2652
+ * produces `partialUserOp[+ fallback]`, hand off to these.
2653
+ */
2654
+ declare class PendingUserOpNotFoundError extends PafiSdkError {
2655
+ readonly code = "PENDING_USEROP_NOT_FOUND";
2656
+ readonly httpStatus: "not_found";
2657
+ constructor(lockId: string);
2658
+ }
2659
+ /**
2660
+ * Thrown when the authenticated user attempts to submit a pending
2661
+ * UserOp that belongs to a different sender. Map to 403.
2662
+ *
2663
+ * The pending-userop store is keyed by `lockId` (a UUID), but lockIds
2664
+ * are predictable by anyone who logs them — without this check, user A
2665
+ * could submit user B's UserOp once they leak/guess the lockId. The
2666
+ * UserOp signature itself recovers to user B (so on-chain it would
2667
+ * fail), but the issuer would still bind the bundler hash to user B's
2668
+ * lock and consume relay quota, enabling DoS / state pollution.
2669
+ */
2670
+ declare class PendingUserOpForbiddenError extends PafiSdkError {
2671
+ readonly code = "PENDING_USEROP_FORBIDDEN";
2672
+ readonly httpStatus: "forbidden";
2673
+ constructor(lockId: string);
2674
+ }
2675
+ interface HandleMobilePrepareParams {
2676
+ /** User EOA — used for the delegation check. */
2677
+ userAddress: Address;
2678
+ chainId: number;
2679
+ /** Lock id (issuer-generated) keying both store entry + ledger row. */
2680
+ lockId: string;
2681
+ /**
2682
+ * Partial UserOp from the upstream handler (PTClaimHandler /
2683
+ * PTRedeemHandler). The orchestrator will top up `maxFeePerGas` /
2684
+ * `maxPriorityFeePerGas` from `provider.estimateFeesPerGas()` if the
2685
+ * fields aren't already set, then request paymaster sponsorship.
2686
+ */
2687
+ partialUserOp: PartialUserOperation;
2688
+ /** Optional fee-stripped fallback variant. */
2689
+ partialUserOpFallback?: PartialUserOperation;
2690
+ /**
2691
+ * Optional separate lock id for the fallback path. Required when the
2692
+ * upstream handler (e.g. PTRedeemHandler) reserves a DIFFERENT
2693
+ * ledger row for the fallback variant. See `prepareMobileUserOp`
2694
+ * for the full rationale.
2695
+ */
2696
+ lockIdFallback?: string;
2697
+ /**
2698
+ * Scenario tag — passed to `requestSponsorship` so the relayer can
2699
+ * apply per-scenario L1 enforcement (`mint`, `burn`, etc.).
2700
+ */
2701
+ scenario: string;
2702
+ /** Target contract for the paymaster intent. */
2703
+ pointTokenAddress: Address;
2704
+ /** TTL the store entry should outlive. Typically the lock duration in seconds. */
2705
+ ttlSeconds: number;
2706
+ store: IPendingUserOpStore;
2707
+ provider: PublicClient;
2708
+ /** Optional — when omitted, paymaster is skipped and `sponsored: false` is returned. */
2709
+ pafiBackendClient?: PafiBackendClient | null;
2710
+ onWarning?: (msg: string) => void;
2711
+ /**
2712
+ * Optional EIP-7702 authorization tuple supplied by an FE that wants
2713
+ * atomic activation — the user signs the auth alongside the UserOp
2714
+ * typed-data, and the bundler applies SetCode + handleOps in one
2715
+ * bundler tx. When provided:
2716
+ * - `needsDelegation` is forced false (the bundler will activate
2717
+ * delegation regardless of current on-chain code).
2718
+ * - The auth is persisted in the pending-userop entry and replayed
2719
+ * to the bundler at submit time.
2720
+ *
2721
+ * Format matches Pimlico's `eth_sendUserOperation` `eip7702Auth` field.
2722
+ * The FE typically derives it from `useSign7702Authorization()` and
2723
+ * normalises with `attachDelegationIfNeeded()` in `@pafi-dev/core`.
2724
+ */
2725
+ eip7702Auth?: {
2726
+ chainId: string;
2727
+ address: string;
2728
+ nonce: string;
2729
+ r: string;
2730
+ s: string;
2731
+ yParity: string;
2732
+ };
2733
+ }
2734
+ interface HandleMobilePrepareResult extends PrepareMobileUserOpResult {
2735
+ /**
2736
+ * True when paymaster sponsorship was applied to the sponsored variant.
2737
+ * (Renamed from `sponsored` to avoid clashing with
2738
+ * `PrepareMobileUserOpResult.sponsored` which is the PreparedUserOp object.)
2739
+ */
2740
+ isSponsored: boolean;
2741
+ /**
2742
+ * True when the user's EOA has no EIP-7702 delegation set on-chain.
2743
+ * The mobile client must run the `/delegate/*` flow first.
2744
+ */
2745
+ needsDelegation: boolean;
2746
+ }
2747
+ /**
2748
+ * Build the mobile prepare response. End-to-end:
2749
+ *
2750
+ * 1. Pull `estimateFeesPerGas` + `getCode(user)` in parallel.
2751
+ * 2. Top up sponsored UserOp fees if the upstream handler left them
2752
+ * unset.
2753
+ * 3. `requestPaymaster` — non-fatal: if PAFI declines, the sponsored
2754
+ * variant still works, the FE just falls back to the unsponsored
2755
+ * response.
2756
+ * 4. `prepareMobileUserOp` — merge + hash + persist.
2757
+ */
2758
+ declare function handleMobilePrepare(params: HandleMobilePrepareParams): Promise<HandleMobilePrepareResult>;
2759
+ interface HandleMobileSubmitParams {
2760
+ lockId: string;
2761
+ /**
2762
+ * Authenticated user EOA — extracted from the JWT / session by the
2763
+ * caller. Enforces ownership: the helper rejects with
2764
+ * `PendingUserOpForbiddenError` when `entry.sender !==
2765
+ * authenticatedAddress`.
2766
+ */
2767
+ authenticatedAddress: Address;
2768
+ /** User signature over `userOpHash` (or `userOpHashFallback`). */
2769
+ signature: Hex;
2770
+ /** Which variant the user actually signed. Defaults to `'sponsored'`. */
2771
+ variant?: "sponsored" | "fallback";
2772
+ store: IPendingUserOpStore;
2773
+ /**
2774
+ * Bind the bundler-returned hash to the lock so `claim/redeem status`
2775
+ * can fall back to the bundler receipt when the indexer's
2776
+ * amount-based match races and resolves a sibling. Different ledgers
2777
+ * use different methods (`bindMintUserOpHash` vs `bindCreditUserOpHash`),
2778
+ * so the caller passes the correct one.
2779
+ */
2780
+ bindUserOpHash: (lockId: string, userOpHash: Hex) => Promise<void>;
2781
+ pafiBackendClient?: PafiBackendClient | null;
2782
+ /** Defaults to `ENTRY_POINT_V08`. */
2783
+ entryPoint?: string;
2784
+ }
2785
+ /**
2786
+ * Submit a previously-prepared mobile UserOp to the bundler.
2787
+ *
2788
+ * Throws:
2789
+ * - `PendingUserOpNotFoundError` — entry expired or already submitted.
2790
+ * Map to 404.
2791
+ * - `PendingUserOpForbiddenError` — `entry.sender` doesn't match the
2792
+ * authenticated user. Map to 403.
2793
+ * - `BundlerNotConfiguredError` — `pafiBackendClient` missing. Map to 503.
2794
+ * - `BundlerRejectedError` — bundler rejected the UserOp. Map to 422.
2795
+ */
2796
+ declare function handleMobileSubmit(params: HandleMobileSubmitParams): Promise<{
2797
+ userOpHash: Hex;
2798
+ }>;
2799
+
2800
+ type DecodedCall$1 = ReturnType<typeof decodeBatchExecuteCalls>[number];
2801
+
2802
+ /**
2803
+ * Structural shape — accepts both the raw `IssuerStateValidator` from
2804
+ * `@pafi-dev/issuer/issuer-state` and any host-framework wrapper (e.g.
2805
+ * a NestJS Injectable that delegates to it). Only `preValidateMint`
2806
+ * is needed on this surface.
2807
+ */
2808
+ interface IssuerStateValidatorLike {
2809
+ preValidateMint(pointToken: Address, amount: bigint): Promise<unknown>;
2810
+ }
2811
+ /**
2812
+ * Sig-gated mint handler — mirrors `PTRedeemHandler` on the mint side.
2813
+ *
2814
+ * Pre-validates against IssuerRegistry + on-chain totalSupply, locks the
2815
+ * off-chain balance, builds the sponsored UserOp (mint + PT fee
2816
+ * transfer) plus an optional fallback variant (mint only — for
2817
+ * paymaster-refused fallback).
2818
+ *
2819
+ * Caller fetches AA + mintRequest nonces (so issuers can plug in their
2820
+ * own composer — gg56 uses a timestamp-key 2D nonce). Caller layers
2821
+ * paymaster sponsorship + sponsorAuth on top of the returned UserOps.
2822
+ */
2823
+ type PTClaimErrorCode = "INVALID_AMOUNT" | "VALIDATION_FAILED" | "BUILD_FAILED" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "UNSUPPORTED_POINT_TOKEN";
2824
+ declare class PTClaimError extends PafiSdkError {
2825
+ readonly httpStatus: "unprocessable";
2826
+ readonly code: PTClaimErrorCode;
2827
+ readonly details?: Record<string, unknown>;
2828
+ constructor(code: PTClaimErrorCode, message: string, details?: Record<string, unknown>);
2829
+ }
2830
+ interface PTClaimHandlerConfig {
2831
+ ledger: IPointLedger;
2832
+ relayService: RelayService;
2833
+ provider: PublicClient;
2834
+ /** Issuer minter signer wallet — passed through to RelayService.prepareMint. */
2835
+ issuerSignerWallet: WalletClient;
2836
+ /**
2837
+ * Resolver for the EIP-712 domain `name` (= PointToken.name() on-chain).
2838
+ * Required because mint routes the `pointTokenAddress` per request —
2839
+ * the handler can't bind a single domain at construction. Pass a
2840
+ * shared resolver instance so mint + redeem amortise the same cache.
2841
+ * `chainId` + `verifyingContract` are derived from the request's
2842
+ * `chainId` + `pointTokenAddress` respectively.
2843
+ */
2844
+ domainResolver: PointTokenDomainResolver;
2845
+ /**
2846
+ * Issuer's allow-listed PointToken contracts (checksummed EIP-55).
2847
+ * Every `handle()` call validates the request's `pointTokenAddress`
2848
+ * against this set BEFORE any chain read or signer call.
2849
+ *
2850
+ * write surface (claim/redeem) previously skipped it. The asymmetry
2851
+ * meant an issuer signer whitelisted as minter on PointTokens
2852
+ * outside the configured indexer set could be coerced into signing
2853
+ * a valid `MintForRequest` for an off-set token — the on-chain
2854
+ * mint would succeed, but no PointIndexer was watching to debit the
2855
+ * off-chain ledger. Silent supply-invariant violation.
2856
+ *
2857
+ * Pass the SAME set used to construct `IssuerApiHandlers` so the
2858
+ * read and write paths agree on what this issuer is willing to
2859
+ * sign for.
2860
+ */
2861
+ supportedTokens: ReadonlySet<Address>;
2862
+ /** Optional — when wired, used to estimate the PT gas-reimbursement fee. */
2863
+ feeService?: FeeManager;
2864
+ /** Optional — pre-validates issuer status + cap before locking balance. */
2865
+ issuerStateValidator?: IssuerStateValidatorLike;
2866
+ /** How long the off-chain balance lock survives if the mint never lands. Default 15 min. */
2867
+ lockDurationMs?: number;
2868
+ /** How far ahead of `now` to set the MintRequest deadline. Default 15 min. */
2869
+ signatureDeadlineSeconds?: number;
2870
+ now?: () => number;
2871
+ /**
2872
+ * Optional override for the MintFeeWrapper address. By default the
2873
+ * handler auto-resolves the wrapper from the request's `chainId` via
2874
+ * `getContractAddresses(chainId).mintFeeWrapper`. Set this only when:
2875
+ * - testing against a non-canonical wrapper deploy, or
2876
+ * - the SDK's hardcoded address is stale and you can't bump the SDK yet
2877
+ *
2878
+ * Pass the dead-zero address to opt OUT of the wrapper path (force direct
2879
+ * mint with no fee). Pass `undefined` (default) to use SDK's lookup.
2880
+ *
2881
+ * Caller responsibility either way:
2882
+ * - DAO must have called `IssuerRegistry.setMintFeeWrapper(...)` so
2883
+ * `addIssuer` cascades the recipient list.
2884
+ * - The PointToken must have been registered with the wrapper (via
2885
+ * `addIssuer` cascade or owner-only `wrapper.registerToken`).
2886
+ */
2887
+ mintFeeWrapperAddress?: Address;
2888
+ }
2889
+ interface PTClaimRequest {
2890
+ authenticatedAddress: Address;
2891
+ userAddress: Address;
2892
+ amount: bigint;
2893
+ pointTokenAddress: Address;
2894
+ chainId: number;
2895
+ /** ERC-4337 account nonce for the user's EOA. */
2896
+ aaNonce: bigint;
2897
+ }
2898
+ interface PTClaimResponse {
2899
+ /** Sponsored UserOp — mint + PT fee transfer (when feeAmount > 0). */
2900
+ userOp: PartialUserOperation;
2901
+ /**
2902
+ * Fallback UserOp — mint only, no PT fee transfer. Present only when
2903
+ * `feeAmount > 0`. User pays gas in ETH directly.
2904
+ */
2905
+ fallback?: PartialUserOperation;
2906
+ lockId: string;
2907
+ feeAmount: bigint;
2908
+ signatureDeadline: bigint;
2909
+ expiresInSeconds: number;
2910
+ /** Decoded calls for the sponsored UserOp (convenience for FE-submit path). */
2911
+ calls: DecodedCall$1[];
2912
+ /** Decoded calls for the fallback UserOp (when present). */
2913
+ callsFallback?: DecodedCall$1[];
2914
+ }
2915
+ declare class PTClaimHandler {
2916
+ private readonly cfg;
2917
+ private readonly inFlightNonces;
2918
+ constructor(config: PTClaimHandlerConfig);
2919
+ handle(request: PTClaimRequest): Promise<PTClaimResponse>;
2920
+ }
2921
+
2922
+ type DecodedCall = ReturnType<typeof decodeBatchExecuteCalls>[number];
2923
+
2924
+ /**
2925
+ * Orderly perp-deposit handler — builds the sponsored + fallback
2926
+ * UserOps for the PAFI Relay path.
2927
+ *
2928
+ * Resolves USDC + verifies the broker is whitelisted on the Vault,
2929
+ * quotes the Relay's USDC fee (covers LayerZero msg.value out of the
2930
+ * Relay's ETH reserve), then builds two UserOps:
2931
+ *
2932
+ * - **sponsored** — USDC.transfer(fee, PAFI) + USDC.approve(relay,
2933
+ * total) + Relay.deposit(req). User reimburses gas in USDC
2934
+ * (input-token rule — user holds USDC at start of batch).
2935
+ * - **fallback** — USDC.approve + Relay.deposit only. User pays
2936
+ * ERC-4337 gas in ETH. Built only when `feeAmount > 0`.
2937
+ *
2938
+ * `maxFee` is set to `quote × 1.5` — slippage cap on the Relay's
2939
+ * USD-pricing during the inclusion window. If the actual fee at
2940
+ * execution exceeds maxFee the Relay reverts (`FeeExceedsMax`).
2941
+ *
2942
+ * v0.7 — Switched gas fee from PT (output-side, mixed token UX) to
2943
+ * USDC (input-side, single token UX). User now only needs USDC to
2944
+ * complete the deposit, no separate PT balance required.
2945
+ */
2946
+ type PerpDepositErrorCode = "PERP_DEPOSIT_UNAVAILABLE" | "BROKER_NOT_WHITELISTED" | "RELAY_FEE_EXCEEDS_AMOUNT" | "FEE_EXCEEDS_AMOUNT" | "INVALID_AMOUNT";
2947
+ declare class PerpDepositError extends PafiSdkError {
2948
+ readonly httpStatus: "unprocessable";
2949
+ readonly code: PerpDepositErrorCode;
2950
+ readonly safeToRetry: boolean;
2951
+ constructor(code: PerpDepositErrorCode, message: string);
2952
+ }
2953
+ interface PerpDepositHandlerConfig {
2954
+ provider: PublicClient;
2955
+ /**
2956
+ * Slippage premium applied on top of the Relay quote when computing
2957
+ * `maxFee`. Default 50% (factor 150). The Relay reverts if actual fee
2958
+ * exceeds `maxFee` so a generous cap reduces re-quote churn.
2959
+ */
2960
+ maxFeePremiumBps?: number;
2961
+ /**
2962
+ * Gas units used by `quoteOperatorFeeUsdt` to size the USDC gas
2963
+ * reimbursement. Default 500_000 (covers approve + Relay.deposit
2964
+ * + LayerZero call). Pass a tighter Pimlico estimate if available.
2965
+ */
2966
+ gasUnits?: bigint;
2967
+ /** Premium bps for `quoteOperatorFeeUsdt`. Default 12_000 (1.2×). */
2968
+ gasPremiumBps?: number;
2969
+ }
2970
+ interface PerpDepositRequest {
2971
+ userAddress: Address;
2972
+ chainId: number;
2973
+ amount: bigint;
2974
+ brokerId: keyof typeof BROKER_HASHES;
2975
+ /** ERC-4337 account nonce. */
2976
+ aaNonce: bigint;
2977
+ }
2978
+ interface PerpDepositResponse {
2979
+ userOp: PartialUserOperation;
2980
+ fallback?: PartialUserOperation;
2981
+ feeAmount: bigint;
2982
+ relayTokenFee: bigint;
2983
+ maxFee: bigint;
2984
+ netDeposit: bigint;
2985
+ accountId: `0x${string}`;
2986
+ brokerHash: `0x${string}`;
2987
+ usdcAddress: Address;
2988
+ relayAddress: Address;
2989
+ calls: DecodedCall[];
2990
+ callsFallback?: DecodedCall[];
2991
+ }
2992
+ declare class PerpDepositHandler {
2993
+ private readonly cfg;
2994
+ constructor(config: PerpDepositHandlerConfig);
2995
+ handle(request: PerpDepositRequest): Promise<PerpDepositResponse>;
2996
+ }
2997
+
2998
+ interface HandleDelegateSubmitParams {
2999
+ lockId: string;
3000
+ /** Authenticated user EOA — must match the entry's `sender`. */
3001
+ authenticatedAddress: Address;
3002
+ /** User signature over the persisted userOpHash. */
3003
+ userOpSig: Hex;
3004
+ store: IPendingUserOpStore;
3005
+ pafiBackendClient?: PafiBackendClient | null;
3006
+ /** Defaults to `ENTRY_POINT_V08`. */
3007
+ entryPoint?: string;
3008
+ }
3009
+ interface HandleDelegateSubmitResult {
3010
+ userOpHash: Hex;
3011
+ }
3012
+ /**
3013
+ * Retrieve the persisted delegate UserOp, embed the user's signature,
3014
+ * and submit to the bundler with the cached `eip7702Auth` field.
3015
+ *
3016
+ * Throws:
3017
+ * - `PendingUserOpNotFoundError` — entry expired or already submitted (404)
3018
+ * - `PendingUserOpForbiddenError` — sender mismatch (403)
3019
+ * - `BundlerNotConfiguredError` — `pafiBackendClient` missing (503)
3020
+ * - `BundlerRejectedError` — bundler rejected the UserOp (422)
3021
+ */
3022
+ declare function handleDelegateSubmit(params: HandleDelegateSubmitParams): Promise<HandleDelegateSubmitResult>;
3023
+
3024
+ /**
3025
+ * Normalized HTTP status the issuer controller should surface for a
3026
+ * given SDK error. Mirrors `SdkErrorHttpStatus` on `PafiSdkError`.
3027
+ */
3028
+ type SdkErrorStatus = SdkErrorHttpStatus;
3029
+ /**
3030
+ * Structured body the issuer controller passes to its
3031
+ * framework-specific exception class. Stripe-style envelope:
3032
+ *
3033
+ * ```json
3034
+ * {
3035
+ * "type": "business_logic_error",
3036
+ * "code": "REDEMPTION_POLICY_DENIED",
3037
+ * "message": "...",
3038
+ * "param": null,
3039
+ * "metadata": { "policyDenialCode": "PER_TX_MIN" },
3040
+ * "safeToRetry": false
3041
+ * }
3042
+ * ```
3043
+ *
3044
+ * Carries enough fields for the global HTTP filter to emit the final
3045
+ * envelope without losing any SDK-side context.
3046
+ */
3047
+ interface SdkErrorBody {
3048
+ /** Stripe-style taxonomy slot — drives UI branching. */
3049
+ type: PafiErrorType;
3050
+ /** Machine-readable code, e.g. `"REDEMPTION_POLICY_DENIED"`. */
3051
+ code: string;
3052
+ /** Human-readable message. */
3053
+ message: string;
3054
+ /** Field name that triggered the error, when applicable. */
3055
+ param?: string;
3056
+ /** UI-facing structured context. */
3057
+ metadata?: Record<string, unknown>;
3058
+ /** Raw debug context. */
3059
+ details?: unknown;
3060
+ /** True when retry is safe (no side effects yet). */
3061
+ safeToRetry: boolean;
3062
+ }
3063
+ /**
3064
+ * Per-status exception factories. The issuer's controller wires one
3065
+ * factory per status to its preferred framework's exception class
3066
+ * (NestJS `UnprocessableEntityException`, Fastify `httpErrors.badData`,
3067
+ * etc). The factory must return an Error — `createSdkErrorMapper`
3068
+ * uses `throw factory(body)`.
3069
+ */
3070
+ interface SdkErrorMapperFactories {
3071
+ notFound: (body: SdkErrorBody) => Error;
3072
+ forbidden: (body: SdkErrorBody) => Error;
3073
+ unprocessable: (body: SdkErrorBody) => Error;
3074
+ serviceUnavailable: (body: SdkErrorBody) => Error;
3075
+ }
3076
+ /**
3077
+ * Build the Stripe-style body from any `PafiSdkError`. Exposed for
3078
+ * frameworks that don't fit the four-factory shape (e.g. a Hono error
3079
+ * handler that builds its own response object).
3080
+ */
3081
+ declare function buildSdkErrorBody(err: PafiSdkError): SdkErrorBody;
3082
+ /**
3083
+ * Build a single error-mapping function that converts any `PafiSdkError`
3084
+ * into the issuer's framework-specific HTTP exception. Status, code,
3085
+ * `safeToRetry`, and `details` come straight off the error class —
3086
+ * the mapper is a dumb funnel, no per-error business logic.
3087
+ *
3088
+ * Any non-`PafiSdkError` is re-thrown unchanged so unexpected runtime
3089
+ * errors propagate to the framework's default 500 handler.
3090
+ *
3091
+ * Usage (NestJS):
3092
+ *
3093
+ * ```ts
3094
+ * const mapSdkError = createSdkErrorMapper({
3095
+ * notFound: (body) => new NotFoundException(body),
3096
+ * forbidden: (body) => new ForbiddenException(body),
3097
+ * unprocessable: (body) => new UnprocessableEntityException(body),
3098
+ * serviceUnavailable: (body) => new ServiceUnavailableException(body),
3099
+ * });
3100
+ *
3101
+ * try { ... } catch (err) { mapSdkError(err); }
3102
+ * ```
3103
+ *
3104
+ * Returns `never` so call sites in `try/catch` propagate the throw
3105
+ * without a redundant `throw` keyword.
3106
+ */
3107
+ declare function createSdkErrorMapper(factories: SdkErrorMapperFactories): (err: unknown) => never;
3108
+
3109
+ /**
3110
+ * Top-level configuration for `createIssuerService`.
3111
+ *
3112
+ * The SDK is HTTP-client-free: it signs EIP-712 messages, reads
3113
+ * on-chain state, builds unsigned UserOperations, and maintains the
3114
+ * off-chain ledger. It never broadcasts transactions — that's the
3115
+ * frontend's responsibility via Bundler + Paymaster.
3116
+ *
3117
+ * **Multi-token:** Pass `pointTokenAddresses: Address[]` to
3118
+ * support multiple PointTokens under a single issuer backend. Legacy
3119
+ * `pointTokenAddress: Address` still works for single-token deployments.
3120
+ * When both are provided, `pointTokenAddresses` takes precedence.
3121
+ */
3122
+ interface IssuerServiceConfig {
3123
+ chainId: number;
3124
+ /** Legacy single-token shortcut; prefer `pointTokenAddresses`. */
3125
+ pointTokenAddress?: Address;
3126
+ /** All PointToken addresses this issuer supports. */
3127
+ pointTokenAddresses?: Address[];
3128
+ /**
3129
+ * Issuer-specific contract addresses merged into the `/config` response.
3130
+ * PAFI-owned addresses (batchExecutor, usdt, issuerRegistry, mintingOracle,
3131
+ * mintFeeWrapper, …) are auto-resolved from `getContractAddresses(chainId)`
3132
+ * and can be omitted. Only `pointToken` / `pointTokens` must be provided.
3133
+ */
3134
+ contracts?: Pick<ApiConfigResponse["contracts"], "pointToken" | "pointTokens" | "relay">;
3135
+ /**
3136
+ * Shared `PublicClient` used for on-chain reads, simulation, indexer
3137
+ * polling, and gas-price lookups. Must be pointed at the target chain.
3138
+ */
3139
+ provider: PublicClient;
3140
+ auth: {
3141
+ jwtSecret: string;
3142
+ /** SIWE-style login-message domain, e.g. `"app.example.com"`. */
3143
+ domain: string;
3144
+ /** Passed straight to `jose` (`"24h"`, `"7d"`, …). Default `"24h"`. */
3145
+ jwtExpiresIn?: string;
3146
+ };
3147
+ /**
3148
+ * Off-chain point ledger — the source of truth for user balances and
3149
+ * in-flight minting locks. Every issuer provides their own database-backed
3150
+ * implementation (Postgres, Redis, etc.) that implements `IPointLedger`.
3151
+ * The SDK does not ship a production ledger; each issuer's data model and
3152
+ * infrastructure are different.
3153
+ */
3154
+ ledger: IPointLedger;
3155
+ /**
3156
+ * Policy engine — optional, defaults to `DefaultPolicyEngine` which checks
3157
+ * off-chain balance. Extend or replace to add KYC, volume caps, etc.
3158
+ */
3159
+ policy?: IPolicyEngine;
3160
+ /** Session store — optional, defaults to `MemorySessionStore` (dev/test only). */
3161
+ sessionStore?: ISessionStore;
3162
+ /**
3163
+ * Fee management config. If omitted the `handleGasFee` endpoint will
3164
+ * throw "not configured" at request time.
3165
+ */
3166
+ fee?: Omit<FeeManagerConfig, "provider">;
3167
+ /**
3168
+ * Pool discovery function for `handlePools`. If omitted the endpoint
3169
+ * throws "not configured" at request time.
3170
+ */
3171
+ poolsProvider?: PoolsProvider;
3172
+ indexer?: {
3173
+ fromBlock?: bigint;
3174
+ cursorStore?: IIndexerCursorStore;
3175
+ confirmations?: number;
3176
+ batchSize?: number;
3177
+ pollIntervalMs?: number;
3178
+ /**
3179
+ * If `true`, the factory calls `indexer.start()` before returning.
3180
+ * Default: `false` — the caller decides when to begin polling.
3181
+ *
3182
+ * **SAFETY:** in a multi-replica deployment, ALWAYS pair
3183
+ * `autoStart: true` with `singletonLock` below. Without leader
3184
+ * election, every replica starts its own indexer fleet and races
3185
+ * against the others — see `ISingletonLock` docs for the
3186
+ * consequences.
3187
+ */
3188
+ autoStart?: boolean;
3189
+ /**
3190
+ * Leader-election primitive. When provided, the factory wraps
3191
+ * `indexer.start()` with a `singletonLock.acquire(key)` call and
3192
+ * only starts the indexer if it wins the lock. Non-leaders stay
3193
+ * idle and take over on the next acquire attempt (when the leader
3194
+ * pod's connection drops, the lock auto-releases).
3195
+ *
3196
+ * The lock key includes the indexer kind + the PointToken address,
3197
+ * so different tokens can be sharded across replicas if desired.
3198
+ *
3199
+ * Recommended: `makePostgresSingletonLock(dataSource)`.
3200
+ */
3201
+ singletonLock?: ISingletonLock;
3202
+ /**
3203
+ * Override the MintFeeWrapper address used by the indexer. When
3204
+ * omitted, the factory auto-resolves from
3205
+ * `getContractAddresses(chainId).mintFeeWrapper`. Pass the
3206
+ * dead-zero address (`0x...dEaD`) to force direct-Transfer mode
3207
+ * (useful for local fork tests).
3208
+ */
3209
+ mintFeeWrapperAddress?: Address;
3210
+ };
3211
+ /**
3212
+ * Redemption restriction config. When provided, the SDK fetches the
3213
+ * per-issuer policy from PAFI issuer-api (with 5min cache + default
3214
+ * fallback) and exposes preview/evaluate via `service.redemption`.
3215
+ * The handler endpoints `handleRedemptionPreview` / `handleRedemptionEvaluate`
3216
+ * are wired only when this is configured.
3217
+ *
3218
+ * `chainId` is taken from the top-level config; the issuer-api URL
3219
+ * is looked up via `getPafiServiceUrls(chainId)`. Only credentials
3220
+ * + the history store are required here.
3221
+ */
3222
+ redemption?: {
3223
+ issuerId: string;
3224
+ apiKey: string;
3225
+ historyStore: IRedemptionHistoryStore;
3226
+ /**
3227
+ * Optional override for the PAFI issuer-api base URL. Audit
3228
+ * env var (e.g. `PAFI_ISSUER_API_URL`) so policy fetch hits the
3229
+ * canonical environment for the deploy. Undefined → SDK
3230
+ * ship-default per chainId.
3231
+ */
3232
+ baseUrl?: string;
3233
+ /**
3234
+ * Behavior khi settlement-api fetch fail. Default `'fail-closed'`
3235
+ * Opt in to `'permissive-default'` ONLY when paired with an alert
3236
+ * on the `policy_provider_fallback` event surfaced via
3237
+ * `onPolicyWarning`.
3238
+ */
3239
+ onFetchFailure?: "fail-closed" | "permissive-default";
3240
+ /**
3241
+ * Observability hook for `policy_provider_fallback` events. Wire
3242
+ * to your logger / Sentry / Datadog so the on-call dashboard sees
3243
+ * settlement-api degradation.
3244
+ */
3245
+ onPolicyWarning?: (msg: string, ctx: Record<string, unknown>) => void;
3246
+ /** Override fetch (testing). */
3247
+ fetchImpl?: typeof fetch;
3248
+ /** Per-fetch timeout in ms. Default 1000. */
3249
+ fetchTimeoutMs?: number;
3250
+ /** Cache TTL in ms. Default 5 * 60 * 1000. */
3251
+ cacheTtlMs?: number;
3252
+ };
3253
+ }
3254
+ interface IssuerService {
3255
+ /** AuthService — login, logout, nonce management. */
3256
+ auth: AuthService;
3257
+ /** Session store — nonce + JWT session persistence. */
3258
+ session: ISessionStore;
3259
+ ledger: IPointLedger;
3260
+ policy: IPolicyEngine;
3261
+ /** RelayService — prepareMint / prepareBurn UserOp builders. */
3262
+ relay: RelayService;
3263
+ /** FeeManager — gas fee estimation. Undefined if not configured. */
3264
+ fee: FeeManager | undefined;
3265
+ /** All indexers keyed by PointToken address. */
3266
+ indexers: Map<Address, PointIndexer>;
3267
+ /**
3268
+ * Lock handles for the indexers this replica was elected leader for.
3269
+ * Empty when `autoStart` is false, or when no `singletonLock` was
3270
+ * provided. Call `release()` on each during graceful shutdown so
3271
+ * peers can take over without waiting for the connection to die.
3272
+ */
3273
+ indexerLeaderLocks: SingletonLockHandle[];
3274
+ /** Framework-agnostic HTTP handlers — wire into Express / Fastify / Hono. */
3275
+ api: IssuerApiHandlers;
3276
+ /**
3277
+ * Redemption restriction service. Undefined when `redemption` is not
3278
+ * configured — the corresponding handlers throw "not configured" at
3279
+ * request time.
3280
+ */
3281
+ redemption: RedemptionService | undefined;
3282
+ }
3283
+ /**
3284
+ * Wire a fully-functional issuer service from a single config object.
3285
+ *
3286
+ * Defaults:
3287
+ * - `sessionStore` → `MemorySessionStore` (dev/test only — replace in prod)
3288
+ * - `policy` → `DefaultPolicyEngine({ ledger })`
3289
+ * - `feeManager` → undefined (handleGasFee throws until configured)
3290
+ * - `poolsProvider` → undefined (handlePools throws until configured)
3291
+ * - `indexer.autoStart` → false
3292
+ *
3293
+ * Throws synchronously if any required field is missing.
3294
+ */
3295
+ declare function createIssuerService(config: IssuerServiceConfig): Promise<IssuerService>;
3296
+
3297
+ /**
3298
+ * Adapter that absorbs every "framework-agnostic" endpoint body into a
3299
+ * single class so issuer controllers stay thin (one line per endpoint).
3300
+ *
3301
+ * What this absorbs:
3302
+ * - Reading + reshaping IssuerApiHandlers responses into wire DTOs
3303
+ * (bigint → string, etc.)
3304
+ * - Composing handler.handle() output with sponsorAuth + decoded calls
3305
+ * - Wiring handleMobilePrepare / handleMobileSubmit / handleClaimStatus
3306
+ * - Building the EIP-7702 delegate UserOp + relay
3307
+ * - Quoting PT → USDT for the cashout preview
3308
+ *
3309
+ * What stays in the issuer controller:
3310
+ * - HTTP routing decorators (`@Get`, `@Post`, `@UseGuards`)
3311
+ * - Auth context extraction (`@User() user: AuthContext`)
3312
+ * - Body DTO classes (with framework-specific decorators)
3313
+ * - Nonce composer (issuer-specific; e.g. gg56 uses 2D timestamp keys)
3314
+ *
3315
+ * Every method that can throw a typed SDK error throws `PafiSdkError` —
3316
+ * the controller wraps every call with `try/catch + mapSdkError`, or
3317
+ * the adapter pre-binds an `errorMapper` and auto-translates.
3318
+ */
3319
+ interface IssuerApiAdapterConfig {
3320
+ issuerService: IssuerService;
3321
+ ledger: IPointLedger;
3322
+ provider: PublicClient;
3323
+ /** Issuer signer wallet — used for `buildSponsorAuth` (EIP-712 sign). */
3324
+ issuerSignerWallet: WalletClient;
3325
+ /** Optional issuer id — when omitted, sponsorAuth is skipped (returns `undefined`). */
3326
+ pafiIssuerId?: string;
3327
+ /** Sig-gated mint handler. Required for `claim` / `claimPrepare`. */
3328
+ ptClaimHandler?: PTClaimHandler | null;
3329
+ /** Reverse-flow handler. Required for `redeem` / `redeemPrepare`. */
3330
+ ptRedeemHandler?: PTRedeemHandler | null;
3331
+ /** Orderly perp-deposit handler. Required for `perpDeposit`. */
3332
+ perpHandler?: PerpDepositHandler | null;
3333
+ /** Pending UserOp store — required for mobile prepare/submit. */
3334
+ pendingUserOpStore: IPendingUserOpStore;
3335
+ /** PAFI backend client — required for mobile submit + delegate submit + status fallback. */
3336
+ pafiBackendClient?: PafiBackendClient | null;
3337
+ /** Optional logger surface for non-fatal warnings. */
3338
+ onWarning?: (msg: string) => void;
3339
+ }
3340
+ interface ConfigDto {
3341
+ chainId: number;
3342
+ contracts: Record<string, string | undefined>;
3343
+ }
3344
+ interface GasFeeDto {
3345
+ gasFeeUsdt: string;
3346
+ }
3347
+ interface PoolsDto {
3348
+ pools: unknown[];
3349
+ }
3350
+ interface UserDto {
3351
+ offChainBalance: string;
3352
+ onChainBalance: string;
3353
+ totalBalance: string;
3354
+ /** @deprecated alias for `offChainBalance` */
3355
+ balance: string;
3356
+ isMinter: boolean;
3357
+ }
3358
+ interface DecodedCallDto {
3359
+ to: string;
3360
+ data: string;
3361
+ value: string;
3362
+ }
3363
+ interface ClaimDto {
3364
+ calls: DecodedCallDto[];
3365
+ callsFallback?: DecodedCallDto[];
3366
+ feeAmount: string;
3367
+ lockId: string;
3368
+ signatureDeadline: string;
3369
+ sponsorAuth?: BuiltSponsorAuth;
3370
+ }
3371
+ interface RedeemDto {
3372
+ calls: DecodedCallDto[];
3373
+ callsFallback?: DecodedCallDto[];
3374
+ feeAmount: string;
3375
+ lockId: string;
3376
+ lockIdFallback?: string;
3377
+ netCreditAmount: string;
3378
+ netCreditAmountFallback?: string;
3379
+ expiresInSeconds: number;
3380
+ signatureDeadline: string;
3381
+ sponsorAuth?: BuiltSponsorAuth;
3382
+ }
3383
+ interface PerpDepositDto {
3384
+ calls: DecodedCallDto[];
3385
+ callsFallback?: DecodedCallDto[];
3386
+ relayTokenFee: string;
3387
+ maxFee: string;
3388
+ netDeposit: string;
3389
+ ptGasFee: string;
3390
+ accountId: Hex;
3391
+ brokerHash: Hex;
3392
+ usdcAddress: Address;
3393
+ relayAddress: Address;
3394
+ sponsorAuth?: BuiltSponsorAuth;
3395
+ }
3396
+ interface MobilePrepareDto {
3397
+ lockId: string;
3398
+ userOpHash: Hex;
3399
+ typedData: SerializedUserOpTypedData;
3400
+ userOpHashFallback?: Hex;
3401
+ typedDataFallback?: SerializedUserOpTypedData;
3402
+ feeAmount: string;
3403
+ signatureDeadline: string;
3404
+ expiresInSeconds: number;
3405
+ sponsored: boolean;
3406
+ needsDelegation: boolean;
3407
+ }
3408
+ interface RedeemPrepareDto extends MobilePrepareDto {
3409
+ netCreditAmount: string;
3410
+ netCreditAmountFallback?: string;
3411
+ /**
3412
+ * Lock id reserved for the FALLBACK redeem path (= full `amount`).
3413
+ * Mobile FE polls `/redeem/status/:lockIdFallback` when it submits
3414
+ * the fallback variant (`variant: 'fallback'` on `/redeem/submit`).
3415
+ */
3416
+ lockIdFallback?: string;
3417
+ }
3418
+ interface MobileSubmitDto {
3419
+ userOpHash: Hex;
3420
+ }
3421
+ interface DelegateStatusDto {
3422
+ isDelegated: boolean;
3423
+ batchExecutorAddress: Address;
3424
+ /**
3425
+ * EOA tx count fetched on-chain via `getTransactionCount(blockTag: 'pending')`.
3426
+ * Mobile passes this VERBATIM into Privy `signAuthorization({ ..., nonce })` —
3427
+ * MUST NOT hardcode `0` or fetch independently. For a fresh embedded wallet
3428
+ * it will be `"0"`, but re-delegation or post-activity wallets will be > 0.
3429
+ * Returned as decimal string (preserves bigint precision over JSON).
3430
+ */
3431
+ delegationNonce: string;
3432
+ chainId: number;
3433
+ }
3434
+ interface DelegatePrepareDto {
3435
+ /**
3436
+ * v0.7.7 — refactored to mobile prepare/submit pattern. Mobile signs
3437
+ * the EIP-7702 authorization LOCALLY (Privy `signAuthorization`),
3438
+ * then signs `userOpHash` (or `typedData`) BEFORE submit. See
3439
+ * `handleDelegatePrepare` for rationale.
3440
+ *
3441
+ * Pre-v0.7.7 callers expected `{ authorizationHash, delegationNonce,
3442
+ * batchExecutorAddress, chainId }` — `delegationNonce` +
3443
+ * `batchExecutorAddress` retained for back-compat so mobile can
3444
+ * compute the authorization hash if needed; `authorizationHash`
3445
+ * removed (mobile's Privy hook computes it locally).
3446
+ */
3447
+ lockId: string;
3448
+ userOpHash: Hex;
3449
+ typedData: SerializedUserOpTypedData;
3450
+ expiresInSeconds: number;
3451
+ isSponsored: boolean;
3452
+ /** Echoed for mobile to recompute the authorization hash if desired. */
3453
+ delegationNonce: string;
3454
+ batchExecutorAddress: Address;
3455
+ chainId: number;
3456
+ }
3457
+ declare class AdapterMisconfiguredError extends Error {
3458
+ readonly code: "ADAPTER_MISCONFIGURED";
3459
+ constructor(message: string);
3460
+ }
3461
+ declare class IssuerApiAdapter {
3462
+ private readonly cfg;
3463
+ constructor(config: IssuerApiAdapterConfig);
3464
+ config(chainId: number): Promise<ConfigDto>;
3465
+ gasFee(): Promise<GasFeeDto>;
3466
+ pools(authenticatedAddress: Address, chainId: number, pointTokenAddress: Address): Promise<PoolsDto>;
3467
+ user(authenticatedAddress: Address, chainId: number, userAddress: Address, pointTokenAddress: Address): Promise<UserDto>;
3468
+ claim(input: {
3469
+ authenticatedAddress: Address;
3470
+ chainId: number;
3471
+ pointTokenAddress: Address;
3472
+ amount: bigint;
3473
+ aaNonce: bigint;
3474
+ }): Promise<ClaimDto>;
3475
+ redeem(input: {
3476
+ authenticatedAddress: Address;
3477
+ chainId: number;
3478
+ pointTokenAddress: Address;
3479
+ amount: bigint;
3480
+ aaNonce: bigint;
3481
+ }): Promise<RedeemDto>;
3482
+ perpDeposit(input: {
3483
+ authenticatedAddress: Address;
3484
+ chainId: number;
3485
+ amount: bigint;
3486
+ brokerId: Parameters<PerpDepositHandler["handle"]>[0]["brokerId"];
3487
+ aaNonce: bigint;
3488
+ }): Promise<PerpDepositDto>;
3489
+ claimPrepare(input: {
3490
+ authenticatedAddress: Address;
3491
+ chainId: number;
3492
+ pointTokenAddress: Address;
3493
+ amount: bigint;
3494
+ aaNonce: bigint;
3495
+ /**
3496
+ * Optional EIP-7702 authorization tuple for atomic activation. When
3497
+ * present, the mobile flow merges SetCode + UserOp into one bundler
3498
+ * tx — no separate /delegate call required. See
3499
+ * `attachDelegationIfNeeded` in `@pafi-dev/core`.
3500
+ */
3501
+ eip7702Auth?: {
3502
+ chainId: string;
3503
+ address: string;
3504
+ nonce: string;
3505
+ r: string;
3506
+ s: string;
3507
+ yParity: string;
3508
+ };
3509
+ }): Promise<MobilePrepareDto>;
3510
+ claimSubmit(input: {
3511
+ authenticatedAddress: Address;
3512
+ lockId: string;
3513
+ signature: Hex;
3514
+ variant?: "sponsored" | "fallback";
3515
+ }): Promise<MobileSubmitDto>;
3516
+ redeemPrepare(input: {
3517
+ authenticatedAddress: Address;
3518
+ chainId: number;
3519
+ pointTokenAddress: Address;
3520
+ amount: bigint;
3521
+ aaNonce: bigint;
3522
+ /**
3523
+ * Optional EIP-7702 authorization for atomic activation — see
3524
+ * `claimPrepare` for the full rationale + Privy hook reference.
3525
+ */
3526
+ eip7702Auth?: {
3527
+ chainId: string;
3528
+ address: string;
3529
+ nonce: string;
3530
+ r: string;
3531
+ s: string;
3532
+ yParity: string;
3533
+ };
3534
+ }): Promise<RedeemPrepareDto>;
3535
+ redeemSubmit(input: {
3536
+ authenticatedAddress: Address;
3537
+ lockId: string;
3538
+ signature: Hex;
3539
+ variant?: "sponsored" | "fallback";
3540
+ }): Promise<MobileSubmitDto>;
3541
+ claimStatus(authenticatedAddress: Address, lockId: string): Promise<MintStatusResponse>;
3542
+ redeemStatus(authenticatedAddress: Address, lockId: string): Promise<BurnStatusResponse>;
3543
+ delegateStatus(authenticatedAddress: Address, chainId: number): Promise<DelegateStatusDto>;
3544
+ /**
3545
+ * Build the delegation-anchor UserOp + obtain paymaster sponsorship
3546
+ * + persist as a pending entry. Mobile must:
3547
+ *
3548
+ * 1. Sign EIP-7702 authorization LOCALLY (Privy `signAuthorization`
3549
+ * with `{contractAddress: batchExecutorAddress, chainId,
3550
+ * nonce: delegationNonce}`) → 65-byte authSig hex.
3551
+ * 2. POST `/delegate/prepare` with `{ chainId, delegationNonce,
3552
+ * authSig }` → this method.
3553
+ * 3. Sign returned `userOpHash` LOCALLY (`signTypedData(typedData)`).
3554
+ * 4. POST `/delegate/submit` with `{ lockId, userOpSig }`.
3555
+ *
3556
+ * v0.7.7 — replaces single-shot delegateSubmit that tried to relay
3557
+ * a UserOp with empty `signature: "0x"` (Simple7702Account's
3558
+ * validateUserOp reverts `ECDSAInvalidSignatureLength` 0xfce698f7).
3559
+ */
3560
+ delegatePrepare(authenticatedAddress: Address, input: {
3561
+ chainId: number;
3562
+ delegationNonce: bigint;
3563
+ authSig: Hex | string;
3564
+ aaNonce: bigint;
3565
+ }): Promise<DelegatePrepareDto>;
3566
+ delegateSubmit(input: {
3567
+ authenticatedAddress: Address;
3568
+ lockId: string;
3569
+ userOpSig: Hex;
3570
+ }): Promise<MobileSubmitDto>;
3571
+ /**
3572
+ * Build + sign a SponsorAuth payload. Returns `undefined` when no
3573
+ * issuer id is configured, so the controller can skip the field.
3574
+ */
3575
+ private buildSponsorAuth;
3576
+ private runMobilePrepare;
3577
+ private assertRedeemHandler;
3578
+ /**
3579
+ * Narrow an optional handler to non-null and throw a clear error when
3580
+ * the issuer wired the adapter without it. Lets issuers opt out of
3581
+ * flows they don't expose (gg56 ships only mobile claim/redeem, so
3582
+ * `swapHandler` + `perpHandler` aren't constructed).
3583
+ */
3584
+ private assertHandler;
3585
+ }
3586
+
3587
+ /**
3588
+ * Config for `createSubgraphPoolsProvider`.
3589
+ */
3590
+ interface SubgraphPoolsProviderConfig {
3591
+ /**
3592
+ * Fully qualified subgraph GraphQL endpoint.
3593
+ * Defaults to the PAFI-hosted subgraph (`PAFI_SUBGRAPH_URL`).
3594
+ * Override only when pointing at a staging or custom deployment.
3595
+ */
3596
+ subgraphUrl?: string;
3597
+ /**
3598
+ * Cache TTL in milliseconds. Pool discovery is near-static — a 30s
3599
+ * cache removes subgraph load without meaningfully delaying UX.
3600
+ * Set to 0 to disable caching. Default: 30_000.
3601
+ */
3602
+ cacheTtlMs?: number;
3603
+ /**
3604
+ * Optional fetch override for test harnesses. Defaults to global `fetch`.
3605
+ */
3606
+ fetchImpl?: typeof fetch;
3607
+ /**
3608
+ * Optional clock override for tests.
3609
+ */
3610
+ now?: () => number;
3611
+ /**
3612
+ * Optional error callback. Invoked on every recoverable error (subgraph
3613
+ * unreachable, non-200 response, GraphQL errors, invalid pool payload).
3614
+ * The provider continues to return `{ pools: [] }` on error and also
3615
+ * logs via `console.warn`/`console.error`; this hook lets host apps
3616
+ * forward errors to their own observability stack without parsing logs.
3617
+ * Throwing inside this callback is swallowed so the provider remains
3618
+ * total. (Addresses CODE_REVIEW issuer MEDIUM-5.)
3619
+ */
3620
+ onError?: (error: Error) => void;
3621
+ }
3622
+ /**
3623
+ * Create a `PoolsProvider` backed by the PAFI subgraph.
3624
+ *
3625
+ * Queries the `pafiTokens` entity for the given `pointTokenAddress`,
3626
+ * reads its linked V3 `Pool`, and returns a single-element `PoolKey[]`.
3627
+ * Multiple pools per token would require a subgraph schema change.
3628
+ *
3629
+ * The result is cached in-process with a short TTL (default 30s). Pool
3630
+ * discovery is near-static so this avoids hammering the subgraph without
3631
+ * blocking config changes for long.
3632
+ *
3633
+ * Returns `{ pools: [] }` if:
3634
+ * - the token is not registered on PAFI yet (no PafiToken entity)
3635
+ * - the token is registered but its pool has not been initialised
3636
+ * - the subgraph is unreachable or returns an error (logs to console,
3637
+ * invokes `onError` if provided; does not throw — callers should
3638
+ * handle empty pool list gracefully)
3639
+ *
3640
+ * Assumes the PAFI subgraph schema (`pafiToken { pool { id feeTier
3641
+ * token0 { id } token1 { id } } }`). Issuers with a custom subgraph must
3642
+ * implement `PoolsProvider` themselves instead of using this helper.
3643
+ *
3644
+ * @example
3645
+ * ```ts
3646
+ * import { createSubgraphPoolsProvider, createIssuerService } from "@pafi-dev/issuer";
3647
+ *
3648
+ * const service = createIssuerService({
3649
+ * // ...other config
3650
+ * poolsProvider: createSubgraphPoolsProvider({
3651
+ * subgraphUrl: "https://graph.pacificfinance.org/subgraphs/name/pafi",
3652
+ * onError: (err) => metrics.increment("issuer.pools.error", { reason: err.message }),
3653
+ * }),
3654
+ * });
3655
+ * ```
3656
+ */
3657
+ declare function createSubgraphPoolsProvider(config?: SubgraphPoolsProviderConfig): PoolsProvider;
3658
+
3659
+ /**
3660
+ * Config for `createSubgraphNativeUsdtQuoter`.
3661
+ */
3662
+ interface SubgraphNativeUsdtQuoterConfig {
3663
+ /**
3664
+ * Fully qualified subgraph GraphQL endpoint.
3665
+ * Defaults to the PAFI-hosted subgraph (`PAFI_SUBGRAPH_URL`).
3666
+ * Override only when pointing at a staging or custom deployment.
3667
+ */
3668
+ subgraphUrl?: string;
3669
+ /**
3670
+ * Decimals of the USDT token. Defaults to 6 (standard USDT/USDC on
3671
+ * Base, Ethereum, Polygon). Override for chains where USDT uses a
3672
+ * different decimals value.
3673
+ */
3674
+ usdtDecimals?: number;
3675
+ /**
3676
+ * Decimals of the native token (ETH on Base/mainnet/Arbitrum/Optimism,
3677
+ * MATIC on Polygon). Default: 18.
3678
+ */
3679
+ nativeDecimals?: number;
3680
+ /**
3681
+ * Cache TTL in milliseconds. ETH price drifts slowly relative to gas
3682
+ * estimation needs — a 30s cache keeps fees stable across bursts of
3683
+ * requests without letting them go stale during volatile markets.
3684
+ * Set to 0 to disable caching. Default: 30_000.
3685
+ */
3686
+ cacheTtlMs?: number;
3687
+ /**
3688
+ * Fallback price (USDT per native token, human-readable float) used
3689
+ * when the subgraph is unreachable. This keeps the backend operational
3690
+ * during subgraph outages rather than bricking cashouts. The fee will
3691
+ * be slightly off but the operator still gets paid. Default: 3000.
3692
+ */
3693
+ fallbackEthPriceUsd?: number;
3694
+ /** Optional fetch override for test harnesses. */
3695
+ fetchImpl?: typeof fetch;
3696
+ /** Optional clock override for tests. */
3697
+ now?: () => number;
3698
+ }
3699
+ /**
3700
+ * Create a native→USDT quoter backed by the PAFI subgraph's
3701
+ * `Bundle.ethPriceUSD`. The returned function has the shape
3702
+ * `(amountNative: bigint) => Promise<bigint>` and can be passed as
3703
+ * `quoteNativeToFee` to `FeeManager` — the fee currency is configured
3704
+ * at the integration layer, not hardcoded here.
3705
+ *
3706
+ * Used by `FeeManager.estimateGasFee()` to convert the gas cost into
3707
+ * an ERC-20 amount charged as part of the sponsored UserOp batch.
3708
+ * Price precision is not critical — a 1-2% drift is acceptable since
3709
+ * the fee manager applies a `gasPremiumBps` buffer.
3710
+ *
3711
+ * The result is cached in-process with a short TTL (default 30s). If
3712
+ * the subgraph is unreachable, falls back to `fallbackEthPriceUsd` so
3713
+ * gas estimation doesn't block user flow during a subgraph outage.
3714
+ *
3715
+ * @example
3716
+ * ```ts
3717
+ * import { createSubgraphNativeUsdtQuoter, createIssuerService } from "@pafi-dev/issuer";
3718
+ *
3719
+ * const service = createIssuerService({
3720
+ * // ...other config
3721
+ * fee: {
3722
+ * quoteNativeToFee: createSubgraphNativeUsdtQuoter({
3723
+ * subgraphUrl: "https://graph.pacificfinance.org/subgraphs/name/pafi",
3724
+ * }),
3725
+ * },
3726
+ * });
3727
+ * ```
3728
+ */
3729
+ declare function createSubgraphNativeUsdtQuoter(config?: SubgraphNativeUsdtQuoterConfig): (amountNative: bigint) => Promise<bigint>;
3730
+
3731
+ interface NativePtQuoterConfig {
3732
+ /** Viem PublicClient — used to call Chainlink on-chain. */
3733
+ provider: PublicClient;
3734
+ /** Address of the PointToken being traded. */
3735
+ pointTokenAddress: Address;
3736
+ /** Chainlink ETH/USD feed address. Defaults to Base mainnet feed. */
3737
+ chainlinkFeedAddress?: Address;
3738
+ /** PAFI subgraph GraphQL endpoint. */
3739
+ subgraphUrl?: string;
3740
+ /** Cache TTL in ms. Default: 30_000. */
3741
+ cacheTtlMs?: number;
3742
+ /** Fallback ETH price (USD) when Chainlink is unreachable. Default: 3000. */
3743
+ fallbackEthPriceUsd?: number;
3744
+ /** Fallback PT price (USDT per 1 PT) when subgraph is unreachable. Default: 0.1. */
3745
+ fallbackPtPriceUsdt?: number;
3746
+ /**
3747
+ * When true, the quoter throws on subgraph/Chainlink miss instead
3748
+ * of returning the fallback price. Security-sensitive callers
3749
+ * (e.g. sponsor-relayer's fee gate) MUST opt in so an attacker-
3750
+ * supplied token with no indexed pool cannot ride the default
3751
+ * fallback to a near-zero required fee. Issuer mint/burn paths
3752
+ * default to the legacy fallback behavior for resilience against
3753
+ * transient oracle hiccups.
3754
+ */
3755
+ failClosed?: boolean;
3756
+ fetchImpl?: typeof fetch;
3757
+ now?: () => number;
3758
+ }
3759
+ /**
3760
+ * Create a native→PT quoter for use as `FeeManager.quoteNativeToFee`.
3761
+ *
3762
+ * Converts ETH gas cost → USDT (via Chainlink ETH/USD) → PT (via subgraph
3763
+ * pool price), returning the fee amount in PT raw units (18 decimals).
3764
+ *
3765
+ * Formula:
3766
+ * feeInPT = amountNative × ethPrice_8dec × ptPerUsdt_18dec / 10^26
3767
+ *
3768
+ * Both prices are cached in-process (default 30s TTL).
3769
+ *
3770
+ * @example
3771
+ * ```ts
3772
+ * fee: {
3773
+ * quoteNativeToFee: createNativePtQuoter({
3774
+ * provider,
3775
+ * pointTokenAddress: "0x...",
3776
+ * chainlinkFeedAddress: getContractAddresses(chainId).chainlinkEthUsd,
3777
+ * }),
3778
+ * }
3779
+ * ```
3780
+ */
3781
+ declare function createNativePtQuoter(config: NativePtQuoterConfig): (amountNative: bigint) => Promise<bigint>;
3782
+
3783
+ /**
3784
+ * Typed errors thrown by the helpers below — issuer controllers map
3785
+ * these to the appropriate HTTP status. We don't depend on @nestjs/common
3786
+ * here because the SDK is framework-agnostic; the consuming controller
3787
+ * wraps each one in its preferred exception class.
3788
+ */
3789
+ declare class BundlerNotConfiguredError extends PafiSdkError {
3790
+ readonly code = "BUNDLER_NOT_CONFIGURED";
3791
+ readonly httpStatus: "service_unavailable";
3792
+ constructor();
3793
+ }
3794
+ declare class BundlerRejectedError extends PafiSdkError {
3795
+ readonly code = "BUNDLER_REJECTED";
3796
+ readonly httpStatus: "unprocessable";
3797
+ readonly cause: unknown;
3798
+ constructor(message: string, cause: unknown);
3799
+ }
3800
+ interface RequestPaymasterParams {
3801
+ /** PAFI backend client. When `null` / `undefined` → returns `undefined`. */
3802
+ client: PafiBackendClient | null | undefined;
3803
+ chainId: number;
3804
+ scenario: string;
3805
+ /** UserOp skeleton — must have all gas + fee fields set. */
3806
+ userOp: {
3807
+ sender: Address;
3808
+ nonce: bigint;
3809
+ callData: Hex;
3810
+ callGasLimit: bigint;
3811
+ verificationGasLimit: bigint;
3812
+ preVerificationGas: bigint;
3813
+ maxFeePerGas: bigint;
3814
+ maxPriorityFeePerGas: bigint;
3815
+ };
3816
+ /** Target contract (typically the PointToken). */
3817
+ pointTokenAddress: Address;
3818
+ /**
3819
+ * Function name to surface in logs / paymaster context. Defaults to
3820
+ * a per-scenario sensible value (`mint` / `burn` / `swap` / generic
3821
+ * scenario name).
3822
+ */
3823
+ functionName?: string;
3824
+ /**
3825
+ * EIP-7702 authorization tuple — REQUIRED for the `delegate`
3826
+ * scenario. Forwarded to sponsor-relayer's `/paymaster/sponsor`
3827
+ * which embeds it into the UserOp before `pm_sponsorUserOperation`,
3828
+ * letting Pimlico simulate the delegation atomically with the
3829
+ * sponsored UserOp. Without this, simulator throws
3830
+ * `AA20 account not deployed`. v0.7.5 added.
3831
+ */
3832
+ eip7702Auth?: {
3833
+ chainId: string;
3834
+ address: string;
3835
+ nonce: string;
3836
+ r: string;
3837
+ s: string;
3838
+ yParity: string;
3839
+ };
3840
+ /** Optional logger for the "sponsorship declined" warning. */
3841
+ onWarning?: (msg: string) => void;
3842
+ }
3843
+ /**
3844
+ * Thin wrapper around `PafiBackendClient.requestSponsorship` with the
3845
+ * "non-fatal on failure" semantics every issuer wants:
3846
+ *
3847
+ * - When the client is missing → returns `undefined` (the caller falls
3848
+ * back to a self-funded UserOp).
3849
+ * - When the network call throws OR PAFI declines (rate limit, intent
3850
+ * rejection, paymaster outage) → returns `undefined` after logging,
3851
+ * so the controller doesn't hard-fail. The caller's
3852
+ * `prepareMobileUserOp` / `mergePaymasterFields` will gracefully
3853
+ * produce the unsponsored variant.
3854
+ *
3855
+ * Replaces ~30 LoC of try/catch + scenario-to-function mapping every
3856
+ * issuer would copy.
3857
+ */
3858
+ declare function requestPaymaster(params: RequestPaymasterParams): Promise<Awaited<ReturnType<PafiBackendClient["requestSponsorship"]>> | undefined>;
3859
+ interface RelayUserOpParams {
3860
+ client: PafiBackendClient | null | undefined;
3861
+ /** EntryPoint address — typically `ENTRY_POINT_V08` from core. */
3862
+ entryPoint: typeof ENTRY_POINT_V08 | string;
3863
+ userOp: Record<string, string | null>;
3864
+ /** EIP-7702 authorization (delegation UserOps only). */
3865
+ eip7702Auth?: {
3866
+ chainId: string;
3867
+ address: string;
3868
+ nonce: string;
3869
+ r: string;
3870
+ s: string;
3871
+ yParity: string;
3872
+ };
3873
+ }
3874
+ /**
3875
+ * Submit a serialized UserOp to the Pimlico bundler via PAFI's
3876
+ * sponsor-relayer. Handles the "client missing" / "bundler rejected"
3877
+ * branches as typed errors so the controller can map to HTTP cleanly.
3878
+ *
3879
+ * Every issuer mobile flow has this exact wrapper — moved into SDK
3880
+ * to drop ~30 LoC of try/catch + error-shape boilerplate per
3881
+ * controller.
3882
+ *
3883
+ * Throws:
3884
+ * - `BundlerNotConfiguredError` — caller didn't configure
3885
+ * `PafiBackendClient`. Map to 503.
3886
+ * - `BundlerRejectedError` — bundler returned an error. Map to 422
3887
+ * (the FE can show the reason — usually `AA21` / `AA34` / etc.).
3888
+ */
3889
+ declare function relayUserOp(params: RelayUserOpParams): Promise<{
3890
+ userOpHash: Hex;
3891
+ }>;
3892
+
3893
+ /**
3894
+ * Registry record returned by `IssuerRegistry.getIssuer()` in V2
3895
+ * dual-bucket. Schema reshape from V1:
3896
+ *
3897
+ * V1: { issuerAddress, signerAddress, name, symbol, active, pointToken, mintingOracle }
3898
+ * V2: { signerAddress, name, active, capitalBase, basisPoints }
3899
+ *
3900
+ * `issuerAddress` is the lookup key the caller passed; we don't
3901
+ * re-emit it here because the caller already has it. `capitalBase` +
3902
+ * `basisPoints` drive the EQUITY-bucket cap (`capitalBase *
3903
+ * basisPoints / 10000`).
3904
+ */
3905
+ interface IssuerRegistryRecord {
3906
+ signerAddress: Address;
3907
+ name: string;
3908
+ active: boolean;
3909
+ capitalBase: bigint;
3910
+ basisPoints: number;
3911
+ }
3912
+ /**
3913
+ * Equity-bucket cap snapshot computed from the `IssuerRegistryRecord`.
3914
+ *
3915
+ * V1 had a separate `TokenCapRecord` sourced from
3916
+ * `MintingOracle.tokenCaps`; in V2 the oracle is stateless and the
3917
+ * EQUITY cap derives from the registry's `capitalBase` + `basisPoints`.
3918
+ * Kept as a distinct shape (rather than inlining into
3919
+ * `PreValidateMintResult`) so admin tooling can read it without
3920
+ * re-deriving the multiplication.
3921
+ */
3922
+ interface EquityCapRecord {
3923
+ capitalBase: bigint;
3924
+ basisPoints: number;
3925
+ hardCap: bigint;
3926
+ }
3927
+ interface PreValidateMintResult {
3928
+ /** Registry record read at pre-validation time. */
3929
+ issuer: IssuerRegistryRecord;
3930
+ /** Equity-bucket cap derived from issuer.capitalBase × basisPoints. */
3931
+ equityCap: EquityCapRecord;
3932
+ /** Current on-chain `PointToken.equitySupply()`. */
3933
+ equitySupply: bigint;
3934
+ /** equityCap.hardCap − equitySupply (clamped to 0). */
3935
+ remaining: bigint;
3936
+ }
3937
+ /**
3938
+ * Thrown by `IssuerStateValidator.preValidateMint()`.
3939
+ * `code` maps 1:1 to the HTTP error the issuer API surfaces to clients.
3940
+ *
3941
+ * `MINT_CAP_EXCEEDED` is `safeToRetry: true` because the cap may free
3942
+ * up between requests as other mints expire or settle. The other two
3943
+ * codes are configuration-time states — the FE can't recover by retry.
3944
+ */
3945
+ type IssuerStateErrorCode = "ISSUER_NOT_REGISTERED" | "ISSUER_INACTIVE" | "MINT_CAP_EXCEEDED";
3946
+ declare class IssuerStateError extends PafiSdkError {
3947
+ readonly httpStatus: "unprocessable";
3948
+ readonly code: IssuerStateErrorCode;
3949
+ readonly details?: Record<string, unknown>;
3950
+ readonly safeToRetry: boolean;
3951
+ constructor(code: IssuerStateErrorCode, message: string, details?: Record<string, unknown>);
3952
+ }
3953
+
3954
+ /**
3955
+ * Pure (framework-agnostic) validator for issuer state.
3956
+ *
3957
+ * Reads IssuerRegistry + PointToken on-chain state and pre-validates
3958
+ * mint requests before the user submits a UserOp. Catching these
3959
+ * off-chain lets issuers fail fast with a clear error rather than
3960
+ * wasting gas on a revert.
3961
+ *
3962
+ * Caching:
3963
+ * - `PointToken.issuer()` — memoized for the process lifetime (immutable)
3964
+ * - Full state (registry + totalSupply) — 10s TTL per PointToken
3965
+ * (see `ISSUER_RECORD_TTL_MS` comment)
3966
+ * - Burst calls while a fetch is in-flight share the same Promise
3967
+ * (thundering-herd protection)
3968
+ * - Operators can call `invalidate()` after admin txs land to bust
3969
+ * the cache immediately instead of waiting up to TTL.
3970
+ *
3971
+ * Usage in NestJS: wrap this in an `@Injectable()` service; pass
3972
+ * `PublicClient` and `registryAddress` from your DI container.
3973
+ */
3974
+ declare class IssuerStateValidator {
3975
+ private readonly provider;
3976
+ private readonly registryAddress;
3977
+ private readonly pointTokenIssuerCache;
3978
+ private readonly stateCache;
3979
+ private readonly inflight;
3980
+ constructor(provider: PublicClient, registryAddress: Address);
3981
+ /**
3982
+ * Convenience factory — reads `registryAddress` from the SDK
3983
+ * `CONTRACT_ADDRESSES` map for the given chain.
3984
+ */
3985
+ static forChain(provider: PublicClient, chainId: number): IssuerStateValidator;
3986
+ /**
3987
+ * Invalidate cached state for one PointToken, or everything if omitted.
3988
+ * Call after admin txs that change registry or cap settings — closes
3989
+ * the split-brain window described
3990
+ * passive TTL. Idempotent: safe to call when no entry exists.
3991
+ */
3992
+ invalidate(pointToken?: Address): void;
3993
+ /**
3994
+ * Resolve `PointToken.issuer()` once per token and memoize.
3995
+ * The issuer field is set at `initialize()` and never changes.
3996
+ */
3997
+ getIssuerAddressForPointToken(pointToken: Address): Promise<Address>;
3998
+ /**
3999
+ * Read registry record + totalSupply, with 30s cache and in-flight
4000
+ * deduplication. Does NOT throw on inactive/missing — returns raw state.
4001
+ */
4002
+ getIssuerState(pointToken: Address): Promise<PreValidateMintResult>;
4003
+ /**
4004
+ * Validate that `amount` PT can be minted on `pointToken` right now.
4005
+ *
4006
+ * Throws `IssuerStateError` with:
4007
+ * - `ISSUER_NOT_REGISTERED` — registry has no record for this issuer
4008
+ * - `ISSUER_INACTIVE` — issuer.active is false
4009
+ * - `MINT_CAP_EXCEEDED` — totalSupply + amount would exceed hardCap
4010
+ *
4011
+ * Returns the fetched state on success so callers can log without a
4012
+ * second RPC round-trip.
4013
+ */
4014
+ preValidateMint(pointToken: Address, amount: bigint): Promise<PreValidateMintResult>;
4015
+ private fetchIssuerState;
4016
+ }
4017
+
4018
+ /**
4019
+ * SDK-side fallback used when settlement-api is unreachable, returns
4020
+ * 404, or returns 5xx. Keep it permissive enough that an outage doesn't
4021
+ * lock all users out, but tight enough that it's not an abuse vector.
4022
+ */
4023
+ declare const DEFAULT_REDEMPTION_POLICY: RedemptionPolicy;
4024
+ declare function defaultPolicyFor(issuerId: string): RedemptionPolicy;
4025
+
4026
+ /**
4027
+ * Either a successful policy fetch or a structured failure. We never
4028
+ * throw from `fetchPolicy()` — callers fall back to cache/default on
4029
+ * any failure mode, so a thrown error would just force every caller
4030
+ * to wrap in try/catch.
4031
+ */
4032
+ type FetchResult = {
4033
+ ok: true;
4034
+ policy: RedemptionPolicy;
4035
+ } | {
4036
+ ok: false;
4037
+ reason: FetchFailureReason;
4038
+ status?: number;
4039
+ };
4040
+ type FetchFailureReason = "TIMEOUT" | "NETWORK" | "NOT_FOUND" | "UNAUTHORIZED" | "SERVER_ERROR" | "INVALID_RESPONSE";
4041
+ declare class SettlementClient {
4042
+ private readonly config;
4043
+ constructor(config: SettlementClientConfig);
4044
+ fetchPolicy(): Promise<FetchResult>;
4045
+ }
4046
+
4047
+ interface UserHistory {
4048
+ /** Total PT redeemed by user in the rolling 24h window. */
4049
+ redeemedLast24hPt: bigint;
4050
+ /** Last redemption timestamp (unix seconds), or null if never. */
4051
+ lastRedeemedAtUnixSec: number | null;
4052
+ }
4053
+ interface EvaluateInput {
4054
+ policy: RedemptionPolicy;
4055
+ policySource: RedemptionPolicySource;
4056
+ history: UserHistory;
4057
+ /** Amount being requested. Pass 0n for a pure preview. */
4058
+ amountPt: bigint;
4059
+ /** Current unix time in seconds (caller-controlled for testability). */
4060
+ nowUnixSec: number;
4061
+ }
4062
+ /**
4063
+ * Pure evaluator. Given a policy + user history snapshot + requested
4064
+ * amount, returns either ALLOW + a preview of the user's remaining
4065
+ * headroom, or DENY + the first failing rule.
4066
+ *
4067
+ * Preview is always populated, even on denial — UI uses it to render
4068
+ * "X PT redeemable now" / "next available at HH:MM" regardless.
4069
+ */
4070
+ declare function evaluateRedemption(input: EvaluateInput): RedemptionDecision;
4071
+ declare const REDEMPTION_HISTORY_WINDOW_SEC: number;
4072
+
4073
+ /**
4074
+ * In-memory IRedemptionHistoryStore for tests + the bundled NestJS
4075
+ * example. Production issuers should implement this against their
4076
+ * existing burn/journal table — sumRedeemedSince is hot path on every
4077
+ * redemption preview.
4078
+ */
4079
+ declare class MemoryRedemptionHistoryStore implements IRedemptionHistoryStore {
4080
+ private readonly entries;
4081
+ sumRedeemedSince(user: Address, sinceUnixSec: number, pointTokenAddress?: Address): Promise<bigint>;
4082
+ getLastRedeemedAtUnixSec(user: Address, pointTokenAddress?: Address): Promise<number | null>;
4083
+ recordRedemption(entry: {
4084
+ user: Address;
4085
+ amountPt: bigint;
4086
+ pointTokenAddress?: Address;
4087
+ unixSec: number;
4088
+ }): Promise<void>;
4089
+ }
4090
+
4091
+ declare const PAFI_ISSUER_SDK_VERSION: string;
4092
+
4093
+ export { AdapterMisconfiguredError, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiRedemptionEvaluateRequest, type ApiRedemptionEvaluateResponse, type ApiRedemptionPreviewRequest, type ApiRedemptionPreviewResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, type BundlerEstimatorClient, BundlerNotConfiguredError, BundlerRejectedError, type BurnEvent, BurnIndexer, type BurnIndexerConfig, BurnIndexerFinalizeError, type BurnStatusParams, type BurnStatusResponse, type ClaimDto, type ConfigDto, ConfigurationError, DEFAULT_REDEMPTION_POLICY, type DecodedCallDto, DefaultPolicyEngine, type DefaultPolicyEngineOptions, type DelegatePrepareDto, type DelegateStatusDto, type EstimateGasFeeOptions, type EvaluateInput, FeeManager, type FeeManagerConfig, type FeeManagerMetrics, type FetchFailureReason, type FetchImpl, type FetchResult, type GasFeeDto, type GasFeeSource, type HandleDelegateSubmitParams, type HandleDelegateSubmitResult, type HandleMobilePrepareParams, type HandleMobilePrepareResult, type HandleMobileSubmitParams, type IIndexerCursorStore, type IPendingUserOpStore, type IPointLedger, type IPolicyEngine, type IRateLimiter, type IRedemptionHistoryStore, type ISessionStore, type ISingletonLock, InMemoryCursorStore, IssuerApiAdapter, type IssuerApiAdapterConfig, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerRegistryRecord, type IssuerService, type IssuerServiceConfig, IssuerStateError, IssuerStateValidator, LockNotFoundError, type LockedMintRequest, type LoginResult, MemoryPendingUserOpStore, MemoryRateLimiter, MemoryRedemptionHistoryStore, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintStatusParams, type MintStatusResponse, type MintingStatus, type MobilePrepareDto, type MobileSubmitDto, type NativePtQuoterConfig, NonceManager, NoopRateLimiter, PAFI_ISSUER_SDK_VERSION, PTClaimError, PTClaimHandler, type PTClaimHandlerConfig, type PTClaimRequest, type PTClaimResponse, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, PafiBackendClient, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, type PafiEstimatorClientConfig, PafiEstimatorHttpError, type PaymasterGasEstimates, type PendingCredit, type PendingUserOpEntry, PendingUserOpForbiddenError, PendingUserOpNotFoundError, type PerpDepositDto, PerpDepositError, PerpDepositHandler, type PerpDepositHandlerConfig, type PerpDepositRequest, type PerpDepositResponse, PointIndexer, type PointIndexerConfig, PointIndexerFinalizeError, PointTokenDomainResolver, type PointTokenDomainResolverConfig, type PolicyDecision, type PolicyEvalRequest, PolicyProvider, type PolicyProviderConfig, type PoolsDto, type PoolsProvider, type PostgresQueryRunner, type PreValidateMintResult, type PrepareBurnParams, type PrepareMintParams, type PrepareMobileUserOpParams, type PrepareMobileUserOpResult, type PreparedUserOp, type PreviewBurnParams, type PreviewMintParams, REDEMPTION_HISTORY_WINDOW_SEC, type RateLimitAction, type RateLimiterConfig, type RedeemDto, type RedeemPrepareDto, RedemptionService, type RedemptionServiceConfig, RelayError, type RelayErrorCode, RelayService, type RelayUserOpParams, type RelayUserOpRequest, type RelayUserOpResponse, type RequestPaymasterParams, type ResolvedPolicy, type RetryConfig, type SdkErrorBody, type SdkErrorMapperFactories, type SdkErrorStatus, type SerializedUserOpTypedData, type Session, SettlementClient, type SettlementClientConfig, type SingletonLockHandle, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipTarget, type SponsorshipUserOp, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type UserDto, type UserHistory, applyPaymasterGasEstimates, authenticateRequest, buildSdkErrorBody, createIssuerService, createNativePtQuoter, createPafiEstimatorClient, createSdkErrorMapper, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider, defaultPolicyFor, evaluateRedemption, handleClaimStatus, handleDelegateSubmit, handleMobilePrepare, handleMobileSubmit, handleRedeemStatus, makePostgresSingletonLock, mergePaymasterFields, prepareMobileUserOp, relayUserOp, requestPaymaster, serializeEntryToJsonRpc, serializeUserOpTypedData };