@pafi-dev/issuer 0.33.0 → 0.35.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.
- package/dist/index.cjs +28 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +28 -22
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/http/index.d.cts +0 -112
- package/dist/http/index.d.ts +0 -112
- package/dist/index.d.cts +0 -4078
- package/dist/index.d.ts +0 -4078
- package/dist/nestjs/index.d.cts +0 -142
- package/dist/nestjs/index.d.ts +0 -142
- package/dist/types-CxVXRHLy.d.cts +0 -64
- package/dist/types-CxVXRHLy.d.ts +0 -64
- package/dist/wallet-auth/index.d.cts +0 -29
- package/dist/wallet-auth/index.d.ts +0 -29
package/dist/index.d.ts
DELETED
|
@@ -1,4078 +0,0 @@
|
|
|
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.js';
|
|
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 (audit finding H-05).** When multiple PointTokens
|
|
972
|
-
* are wired through a single `createIssuerService` call, each
|
|
973
|
-
* `PointIndexer` MUST get its own cursor — otherwise token A advances
|
|
974
|
-
* the shared cursor past token B's events and token B's mints are
|
|
975
|
-
* never finalized (off-chain balance never deducts → on-chain PT
|
|
976
|
-
* supply for token B exceeds off-chain backing). Combined with the
|
|
977
|
-
* H-04 monotonic-save fix this becomes a catastrophic-and-stable
|
|
978
|
-
* state where token B's cursor can never catch up. Implementations
|
|
979
|
-
* SHOULD expose `forKey(key)` returning a derived store keyed under
|
|
980
|
-
* a distinct namespace; the SDK 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 (see H-05).
|
|
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
|
-
* (the H-05 fix), so a single InMemoryCursorStore can back N
|
|
1011
|
-
* PointIndexers in tests / 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. See audit M-12: pre-fix this
|
|
1202
|
-
* was silently swallowed and the cursor advanced regardless,
|
|
1203
|
-
* permanently dropping confirmed on-chain burns from the off-chain
|
|
1204
|
-
* credit pipeline. Post-fix the error propagates, the chunk's cursor
|
|
1205
|
-
* save is skipped, 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 audit 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
|
-
* Audit PACI5-13 — required for the confirmation-depth check on the
|
|
2214
|
-
* bundler-receipt fallback. The bundler returns success at zero
|
|
2215
|
-
* confs, but `PointIndexer` enforces a 3-block reorg window;
|
|
2216
|
-
* crediting / debiting off-chain at 0 confs while the indexer waits
|
|
2217
|
-
* for finality leaves an unbacked durable mutation if the tx is
|
|
2218
|
-
* reorged out. Pass the SAME PublicClient that the indexer uses so
|
|
2219
|
-
* both paths see consistent chain head. Optional only for legacy
|
|
2220
|
-
* callers that don't supply `pafiBackendClient`; required whenever
|
|
2221
|
-
* the receipt-fallback path can run.
|
|
2222
|
-
*/
|
|
2223
|
-
provider?: PublicClient;
|
|
2224
|
-
/**
|
|
2225
|
-
* Audit PACI5-13 — confirmation depth required before the receipt
|
|
2226
|
-
* fallback applies the credit / debit. MUST match
|
|
2227
|
-
* `PointIndexer.confirmations` (default 3). Operators who reduce
|
|
2228
|
-
* the indexer depth must set this to the same value.
|
|
2229
|
-
*/
|
|
2230
|
-
confirmations?: number;
|
|
2231
|
-
/** Optional logger for "ledger update failed" warnings. */
|
|
2232
|
-
onWarning?: (msg: string) => void;
|
|
2233
|
-
}
|
|
2234
|
-
interface BurnStatusParams {
|
|
2235
|
-
lockId: string;
|
|
2236
|
-
userAddress: Address;
|
|
2237
|
-
ledger: IPointLedger;
|
|
2238
|
-
pafiBackendClient?: PafiBackendClient | null;
|
|
2239
|
-
/**
|
|
2240
|
-
* Audit PACI5-13 — see `MintStatusParams.provider` for full
|
|
2241
|
-
* rationale. The receipt-fallback path applies a spendable
|
|
2242
|
-
* off-chain credit; without a confirmation-depth gate a reorg
|
|
2243
|
-
* before `BurnIndexer.confirmations` leaves durable unbacked
|
|
2244
|
-
* credit with no reversal mechanism.
|
|
2245
|
-
*/
|
|
2246
|
-
provider?: PublicClient;
|
|
2247
|
-
/**
|
|
2248
|
-
* Audit PACI5-13 — confirmation depth required before the receipt
|
|
2249
|
-
* fallback applies the credit. MUST match
|
|
2250
|
-
* `BurnIndexer.confirmations` (default 3).
|
|
2251
|
-
*/
|
|
2252
|
-
confirmations?: number;
|
|
2253
|
-
onWarning?: (msg: string) => void;
|
|
2254
|
-
}
|
|
2255
|
-
declare class LockNotFoundError extends PafiSdkError {
|
|
2256
|
-
readonly code = "LOCK_NOT_FOUND";
|
|
2257
|
-
readonly httpStatus: "not_found";
|
|
2258
|
-
constructor();
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
/**
|
|
2262
|
-
* Handle GET /claim/status/:lockId.
|
|
2263
|
-
*
|
|
2264
|
-
* Returns the lock's current state from the ledger. When the ledger
|
|
2265
|
-
* supports `userOpHash` binding AND the lock is still PENDING, falls
|
|
2266
|
-
* back to the bundler receipt — the canonical truth that bypasses
|
|
2267
|
-
* `PointIndexer`'s amount-based matching race.
|
|
2268
|
-
*
|
|
2269
|
-
* Throws `LockNotFoundError` when the lock doesn't exist or doesn't
|
|
2270
|
-
* belong to `userAddress`. Caller maps to HTTP 404.
|
|
2271
|
-
*/
|
|
2272
|
-
declare function handleClaimStatus(params: MintStatusParams): Promise<MintStatusResponse>;
|
|
2273
|
-
/**
|
|
2274
|
-
* Handle GET /redeem/status/:lockId. Symmetric to `handleClaimStatus`
|
|
2275
|
-
* for the burn → off-chain credit flow.
|
|
2276
|
-
*
|
|
2277
|
-
* Bundler-receipt fallback resolves the credit via
|
|
2278
|
-
* `ledger.resolveCreditByBurnTx` when the bundler reports the burn
|
|
2279
|
-
* UserOp succeeded.
|
|
2280
|
-
*/
|
|
2281
|
-
declare function handleRedeemStatus(params: BurnStatusParams): Promise<BurnStatusResponse>;
|
|
2282
|
-
|
|
2283
|
-
/**
|
|
2284
|
-
* A pending UserOp serialized for persistent storage (Redis, Postgres, memory).
|
|
2285
|
-
*
|
|
2286
|
-
* All bigint fields are stored as decimal strings so the entry can be
|
|
2287
|
-
* JSON-serialized without precision loss. Convert back to bigint before
|
|
2288
|
-
* calling `computeUserOpHash` or `serializeUserOpToJsonRpc`.
|
|
2289
|
-
*/
|
|
2290
|
-
interface PendingUserOpEntry {
|
|
2291
|
-
sender: Address;
|
|
2292
|
-
nonce: string;
|
|
2293
|
-
callData: Hex;
|
|
2294
|
-
callGasLimit: string;
|
|
2295
|
-
verificationGasLimit: string;
|
|
2296
|
-
preVerificationGas: string;
|
|
2297
|
-
maxFeePerGas: string;
|
|
2298
|
-
maxPriorityFeePerGas: string;
|
|
2299
|
-
paymaster?: Address;
|
|
2300
|
-
paymasterVerificationGasLimit?: string;
|
|
2301
|
-
paymasterPostOpGasLimit?: string;
|
|
2302
|
-
paymasterData?: Hex;
|
|
2303
|
-
chainId: number;
|
|
2304
|
-
/** Hex-encoded userOpHash — the value the user signed via personal_sign. */
|
|
2305
|
-
userOpHash: Hex;
|
|
2306
|
-
/**
|
|
2307
|
-
* Fee-stripped fallback variant. Set by `/claim/prepare` and
|
|
2308
|
-
* `/redeem/prepare` when a PT operator fee is configured AND the
|
|
2309
|
-
* paymaster sponsorship attached successfully — i.e. the user might
|
|
2310
|
-
* still want to submit without paymaster (paying ETH gas), and in
|
|
2311
|
-
* that case shouldn't be charged the PT fee. `/claim/submit` reads
|
|
2312
|
-
* this branch when its request body specifies
|
|
2313
|
-
* `variant: "fallback"`.
|
|
2314
|
-
*
|
|
2315
|
-
* Has a different `callData` (no PT.transfer prepended) and
|
|
2316
|
-
* therefore a different `userOpHash`. Paymaster fields are NOT
|
|
2317
|
-
* present — the fallback is by definition unsponsored.
|
|
2318
|
-
*/
|
|
2319
|
-
fallback?: {
|
|
2320
|
-
callData: Hex;
|
|
2321
|
-
callGasLimit: string;
|
|
2322
|
-
verificationGasLimit: string;
|
|
2323
|
-
preVerificationGas: string;
|
|
2324
|
-
userOpHash: Hex;
|
|
2325
|
-
/**
|
|
2326
|
-
* Lock id (PendingCredit on redeem, LockedMint on claim) reserved
|
|
2327
|
-
* for the FALLBACK path — distinct from the outer entry's
|
|
2328
|
-
* sponsored lock because the on-chain amount the user burns/mints
|
|
2329
|
-
* differs between variants.
|
|
2330
|
-
*
|
|
2331
|
-
* Audit PACI5-21 — pre-fix `handleMobileSubmit.bindUserOpHash`
|
|
2332
|
-
* always bound the submitted userOpHash to the SPONSORED lockId,
|
|
2333
|
-
* even when the user submitted the fallback variant. The
|
|
2334
|
-
* bundler-receipt fallback in `handleRedeemStatus` then resolved
|
|
2335
|
-
* the sponsored credit (`amount - fee`) against the on-chain
|
|
2336
|
-
* burn of the FULL `amount` — every fee-bearing fallback redeem
|
|
2337
|
-
* permanently under-credited the user by exactly the fee. Surface
|
|
2338
|
-
* the fallback lockId here so submit can route the bind to the
|
|
2339
|
-
* correct ledger row.
|
|
2340
|
-
*/
|
|
2341
|
-
lockId?: string;
|
|
2342
|
-
};
|
|
2343
|
-
/**
|
|
2344
|
-
* EIP-7702 authorization tuple — present only on the `delegate`
|
|
2345
|
-
* scenario where the UserOp anchors the EOA's one-time delegation
|
|
2346
|
-
* to a 7702 implementation. Embedded into the eth_sendUserOperation
|
|
2347
|
-
* payload at submit time so Pimlico bundler applies the bytecode
|
|
2348
|
-
* atomically with the UserOp execution. Pre-computed at prepare
|
|
2349
|
-
* time from the user's `signAuthorization` signature so submit
|
|
2350
|
-
* doesn't need to re-derive it.
|
|
2351
|
-
*
|
|
2352
|
-
* v0.7.7 — added per delegate-flow refactor (mobile prepare/submit).
|
|
2353
|
-
*/
|
|
2354
|
-
eip7702Auth?: {
|
|
2355
|
-
chainId: string;
|
|
2356
|
-
address: string;
|
|
2357
|
-
nonce: string;
|
|
2358
|
-
r: string;
|
|
2359
|
-
s: string;
|
|
2360
|
-
yParity: string;
|
|
2361
|
-
};
|
|
2362
|
-
}
|
|
2363
|
-
/**
|
|
2364
|
-
* Storage backend for pending UserOps in the mobile prepare/submit pattern.
|
|
2365
|
-
*
|
|
2366
|
-
* Implement this interface and wire it into your issuer backend:
|
|
2367
|
-
* - `save()` — called by `POST /claim/prepare` and `POST /redeem/prepare`
|
|
2368
|
-
* - `get()` — called by `POST /claim/submit` and `POST /redeem/submit`
|
|
2369
|
-
* - `delete()` — called after successful submit or explicit cancellation
|
|
2370
|
-
*
|
|
2371
|
-
* The default implementation in the gg56 boilerplate uses Redis with
|
|
2372
|
-
* a short TTL matching the MintRequest / BurnRequest deadline.
|
|
2373
|
-
*/
|
|
2374
|
-
interface IPendingUserOpStore {
|
|
2375
|
-
save(lockId: string, entry: PendingUserOpEntry, ttlSeconds: number): Promise<void>;
|
|
2376
|
-
get(lockId: string): Promise<PendingUserOpEntry | null>;
|
|
2377
|
-
delete(lockId: string): Promise<void>;
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
/**
|
|
2381
|
-
* Convert a stored `PendingUserOpEntry` (decimal-string fields) plus a
|
|
2382
|
-
* signature into the JSON-RPC wire format for `eth_sendUserOperation`.
|
|
2383
|
-
*
|
|
2384
|
-
* Bridges the gap between the serialized storage format (decimal strings,
|
|
2385
|
-
* safe for JSON/Redis) and `serializeUserOpToJsonRpc` which expects bigints.
|
|
2386
|
-
*/
|
|
2387
|
-
declare function serializeEntryToJsonRpc(entry: PendingUserOpEntry, signature: Hex, variant?: "sponsored" | "fallback"): Record<string, string | null>;
|
|
2388
|
-
|
|
2389
|
-
/**
|
|
2390
|
-
* In-memory `IPendingUserOpStore` for **single-instance dev / test
|
|
2391
|
-
* harnesses only**.
|
|
2392
|
-
*
|
|
2393
|
-
* Multi-instance deployments (k8s, PM2 cluster) MUST use a shared store
|
|
2394
|
-
* — typically Redis with a TTL key or Postgres with `expires_at`. If
|
|
2395
|
-
* `prepare` lands on instance A and `submit` on instance B, an
|
|
2396
|
-
* in-memory store on A loses the entry.
|
|
2397
|
-
*
|
|
2398
|
-
* Entries are evicted lazily on `get()` if expired. Periodic sweep is
|
|
2399
|
-
* not implemented — the in-memory map's footprint scales with
|
|
2400
|
-
* outstanding pending UserOps, which is bounded by the issuer's lock
|
|
2401
|
-
* duration (default 15 min).
|
|
2402
|
-
*/
|
|
2403
|
-
declare class MemoryPendingUserOpStore implements IPendingUserOpStore {
|
|
2404
|
-
private readonly entries;
|
|
2405
|
-
private readonly now;
|
|
2406
|
-
constructor(now?: () => number);
|
|
2407
|
-
save(lockId: string, entry: PendingUserOpEntry, ttlSeconds: number): Promise<void>;
|
|
2408
|
-
get(lockId: string): Promise<PendingUserOpEntry | null>;
|
|
2409
|
-
delete(lockId: string): Promise<void>;
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
/**
|
|
2413
|
-
* Re-shape `UserOpTypedData` so all `bigint` fields become hex strings —
|
|
2414
|
-
* required for JSON transport over HTTP. Mirrors the inverse of what
|
|
2415
|
-
* `walletClient.signTypedData` accepts on the client (it auto-coerces hex
|
|
2416
|
-
* strings back to bigints for `uint256` fields).
|
|
2417
|
-
*/
|
|
2418
|
-
type SerializedUserOpTypedData = {
|
|
2419
|
-
domain: UserOpTypedData["domain"];
|
|
2420
|
-
types: UserOpTypedData["types"];
|
|
2421
|
-
primaryType: UserOpTypedData["primaryType"];
|
|
2422
|
-
message: {
|
|
2423
|
-
sender: Address;
|
|
2424
|
-
nonce: Hex;
|
|
2425
|
-
initCode: Hex;
|
|
2426
|
-
callData: Hex;
|
|
2427
|
-
accountGasLimits: Hex;
|
|
2428
|
-
preVerificationGas: Hex;
|
|
2429
|
-
gasFees: Hex;
|
|
2430
|
-
paymasterAndData: Hex;
|
|
2431
|
-
};
|
|
2432
|
-
};
|
|
2433
|
-
/**
|
|
2434
|
-
* Convert a `UserOpTypedData` payload into the JSON-safe wire form
|
|
2435
|
-
* (bigint → hex string). The mobile client passes this directly to
|
|
2436
|
-
* `eth_signTypedData_v4` / viem's `signTypedData`.
|
|
2437
|
-
*/
|
|
2438
|
-
declare function serializeUserOpTypedData(td: UserOpTypedData): SerializedUserOpTypedData;
|
|
2439
|
-
/**
|
|
2440
|
-
* Merge Pimlico's paymaster-sponsorship response into a UserOp
|
|
2441
|
-
* skeleton, applying only fields that are actually defined.
|
|
2442
|
-
*
|
|
2443
|
-
* `pm_sponsorUserOperation` returns:
|
|
2444
|
-
* - `paymaster` / `paymasterData` — required for the sponsored sig
|
|
2445
|
-
* - `paymasterVerificationGasLimit` / `paymasterPostOpGasLimit`
|
|
2446
|
-
* - **re-estimated** `callGasLimit` / `verificationGasLimit` /
|
|
2447
|
-
* `preVerificationGas` — the paymaster signature is computed over
|
|
2448
|
-
* these new values
|
|
2449
|
-
* - **bundler-required** `maxFeePerGas` / `maxPriorityFeePerGas` —
|
|
2450
|
-
* Base RPC's `eth_feeHistory` underestimates, bundler rejects
|
|
2451
|
-
*
|
|
2452
|
-
* Callers MUST re-merge ALL of these into the userOp BEFORE computing
|
|
2453
|
-
* the EIP-712 userOpHash — otherwise both the paymaster signature
|
|
2454
|
-
* (AA34) and the user signature (AA24, recovered against a different
|
|
2455
|
-
* hash) fail at validation.
|
|
2456
|
-
*
|
|
2457
|
-
* Skips fields that are undefined so legacy paymaster responses
|
|
2458
|
-
* (without re-estimated gas) don't accidentally zero out the original
|
|
2459
|
-
* estimates.
|
|
2460
|
-
*/
|
|
2461
|
-
/**
|
|
2462
|
-
* Subset of `SponsorshipResponse` consumed by the merge/hash helpers.
|
|
2463
|
-
* Inlined as a structural type (rather than importing
|
|
2464
|
-
* `SponsorshipResponse` from `pafi-backend/client`) to keep
|
|
2465
|
-
* `userop-store` independent of the HTTP-client module.
|
|
2466
|
-
*/
|
|
2467
|
-
interface PaymasterGasEstimates {
|
|
2468
|
-
paymaster: Address;
|
|
2469
|
-
paymasterData: Hex;
|
|
2470
|
-
paymasterVerificationGasLimit: bigint;
|
|
2471
|
-
paymasterPostOpGasLimit: bigint;
|
|
2472
|
-
callGasLimit?: bigint;
|
|
2473
|
-
verificationGasLimit?: bigint;
|
|
2474
|
-
preVerificationGas?: bigint;
|
|
2475
|
-
maxFeePerGas?: bigint;
|
|
2476
|
-
maxPriorityFeePerGas?: bigint;
|
|
2477
|
-
}
|
|
2478
|
-
declare function mergePaymasterFields<T extends object>(userOp: T, paymasterFields: PaymasterGasEstimates | undefined): T;
|
|
2479
|
-
interface PreparedUserOp {
|
|
2480
|
-
/** The bundler-ready UserOp (with paymaster + Pimlico-quoted gas). */
|
|
2481
|
-
userOp: PartialUserOperation & {
|
|
2482
|
-
maxFeePerGas: bigint;
|
|
2483
|
-
maxPriorityFeePerGas: bigint;
|
|
2484
|
-
paymaster?: Address;
|
|
2485
|
-
paymasterData?: Hex;
|
|
2486
|
-
paymasterVerificationGasLimit?: bigint;
|
|
2487
|
-
paymasterPostOpGasLimit?: bigint;
|
|
2488
|
-
};
|
|
2489
|
-
/** Hex-encoded EIP-712 digest. Equals `EntryPoint.getUserOpHash`. */
|
|
2490
|
-
userOpHash: Hex;
|
|
2491
|
-
/** Typed-data payload — pass directly to `eth_signTypedData_v4`. */
|
|
2492
|
-
typedData: SerializedUserOpTypedData;
|
|
2493
|
-
}
|
|
2494
|
-
/**
|
|
2495
|
-
* Atomic "merge paymaster fields + compute hash + build typed data" —
|
|
2496
|
-
* the only place the SDK should be doing all three operations.
|
|
2497
|
-
*
|
|
2498
|
-
* Why bundled into one call:
|
|
2499
|
-
*
|
|
2500
|
-
* Pimlico's `pm_sponsorUserOperation` re-estimates
|
|
2501
|
-
* `callGasLimit` / `verificationGasLimit` / `preVerificationGas` and
|
|
2502
|
-
* the bundler fee fields (`maxFeePerGas` / `maxPriorityFeePerGas`),
|
|
2503
|
-
* then signs `paymasterData` over the new values. The EntryPoint
|
|
2504
|
-
* hashes the actual on-chain field values during validation, so
|
|
2505
|
-
* callers MUST overwrite the matching userOp fields BEFORE
|
|
2506
|
-
* computing the userOpHash. Forgetting any field triggers AA34
|
|
2507
|
-
* (paymaster signature mismatch) and/or AA24 (sender signature
|
|
2508
|
-
* mismatch) — opaque failures that take hours to debug.
|
|
2509
|
-
*
|
|
2510
|
-
* Splitting "merge" and "hash" into two free functions invited
|
|
2511
|
-
* exactly this bug — each call site reinvented the merge logic,
|
|
2512
|
-
* and one was already drifting (see audit M-02). Returning all
|
|
2513
|
-
* three pieces (`userOp`, `userOpHash`, `typedData`) from a single
|
|
2514
|
-
* call makes "hash before merge" structurally impossible: there is
|
|
2515
|
-
* no intermediate userOp to compute a stale 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
|
-
* Audit PACI5-21 — when omitted, `handleMobileSubmit` binds the
|
|
2569
|
-
* fallback userOpHash to the outer sponsored `lockId`, which made
|
|
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 permanently
|
|
2573
|
-
* under-credited 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 audit PACI5-21 +
|
|
2694
|
-
* `prepareMobileUserOp` 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 (H-04):** 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
|
-
fetchImpl?: typeof fetch;
|
|
3747
|
-
now?: () => number;
|
|
3748
|
-
}
|
|
3749
|
-
/**
|
|
3750
|
-
* Create a native→PT quoter for use as `FeeManager.quoteNativeToFee`.
|
|
3751
|
-
*
|
|
3752
|
-
* Converts ETH gas cost → USDT (via Chainlink ETH/USD) → PT (via subgraph
|
|
3753
|
-
* pool price), returning the fee amount in PT raw units (18 decimals).
|
|
3754
|
-
*
|
|
3755
|
-
* Formula:
|
|
3756
|
-
* feeInPT = amountNative × ethPrice_8dec × ptPerUsdt_18dec / 10^26
|
|
3757
|
-
*
|
|
3758
|
-
* Both prices are cached in-process (default 30s TTL).
|
|
3759
|
-
*
|
|
3760
|
-
* @example
|
|
3761
|
-
* ```ts
|
|
3762
|
-
* fee: {
|
|
3763
|
-
* quoteNativeToFee: createNativePtQuoter({
|
|
3764
|
-
* provider,
|
|
3765
|
-
* pointTokenAddress: "0x...",
|
|
3766
|
-
* chainlinkFeedAddress: getContractAddresses(chainId).chainlinkEthUsd,
|
|
3767
|
-
* }),
|
|
3768
|
-
* }
|
|
3769
|
-
* ```
|
|
3770
|
-
*/
|
|
3771
|
-
declare function createNativePtQuoter(config: NativePtQuoterConfig): (amountNative: bigint) => Promise<bigint>;
|
|
3772
|
-
|
|
3773
|
-
/**
|
|
3774
|
-
* Typed errors thrown by the helpers below — issuer controllers map
|
|
3775
|
-
* these to the appropriate HTTP status. We don't depend on @nestjs/common
|
|
3776
|
-
* here because the SDK is framework-agnostic; the consuming controller
|
|
3777
|
-
* wraps each one in its preferred exception class.
|
|
3778
|
-
*/
|
|
3779
|
-
declare class BundlerNotConfiguredError extends PafiSdkError {
|
|
3780
|
-
readonly code = "BUNDLER_NOT_CONFIGURED";
|
|
3781
|
-
readonly httpStatus: "service_unavailable";
|
|
3782
|
-
constructor();
|
|
3783
|
-
}
|
|
3784
|
-
declare class BundlerRejectedError extends PafiSdkError {
|
|
3785
|
-
readonly code = "BUNDLER_REJECTED";
|
|
3786
|
-
readonly httpStatus: "unprocessable";
|
|
3787
|
-
readonly cause: unknown;
|
|
3788
|
-
constructor(message: string, cause: unknown);
|
|
3789
|
-
}
|
|
3790
|
-
interface RequestPaymasterParams {
|
|
3791
|
-
/** PAFI backend client. When `null` / `undefined` → returns `undefined`. */
|
|
3792
|
-
client: PafiBackendClient | null | undefined;
|
|
3793
|
-
chainId: number;
|
|
3794
|
-
scenario: string;
|
|
3795
|
-
/** UserOp skeleton — must have all gas + fee fields set. */
|
|
3796
|
-
userOp: {
|
|
3797
|
-
sender: Address;
|
|
3798
|
-
nonce: bigint;
|
|
3799
|
-
callData: Hex;
|
|
3800
|
-
callGasLimit: bigint;
|
|
3801
|
-
verificationGasLimit: bigint;
|
|
3802
|
-
preVerificationGas: bigint;
|
|
3803
|
-
maxFeePerGas: bigint;
|
|
3804
|
-
maxPriorityFeePerGas: bigint;
|
|
3805
|
-
};
|
|
3806
|
-
/** Target contract (typically the PointToken). */
|
|
3807
|
-
pointTokenAddress: Address;
|
|
3808
|
-
/**
|
|
3809
|
-
* Function name to surface in audit / paymaster context. Defaults to
|
|
3810
|
-
* a per-scenario sensible value (`mint` / `burn` / `swap` / generic
|
|
3811
|
-
* scenario name).
|
|
3812
|
-
*/
|
|
3813
|
-
functionName?: string;
|
|
3814
|
-
/**
|
|
3815
|
-
* EIP-7702 authorization tuple — REQUIRED for the `delegate`
|
|
3816
|
-
* scenario. Forwarded to sponsor-relayer's `/paymaster/sponsor`
|
|
3817
|
-
* which embeds it into the UserOp before `pm_sponsorUserOperation`,
|
|
3818
|
-
* letting Pimlico simulate the delegation atomically with the
|
|
3819
|
-
* sponsored UserOp. Without this, simulator throws
|
|
3820
|
-
* `AA20 account not deployed`. v0.7.5 added.
|
|
3821
|
-
*/
|
|
3822
|
-
eip7702Auth?: {
|
|
3823
|
-
chainId: string;
|
|
3824
|
-
address: string;
|
|
3825
|
-
nonce: string;
|
|
3826
|
-
r: string;
|
|
3827
|
-
s: string;
|
|
3828
|
-
yParity: string;
|
|
3829
|
-
};
|
|
3830
|
-
/** Optional logger for the "sponsorship declined" warning. */
|
|
3831
|
-
onWarning?: (msg: string) => void;
|
|
3832
|
-
}
|
|
3833
|
-
/**
|
|
3834
|
-
* Thin wrapper around `PafiBackendClient.requestSponsorship` with the
|
|
3835
|
-
* "non-fatal on failure" semantics every issuer wants:
|
|
3836
|
-
*
|
|
3837
|
-
* - When the client is missing → returns `undefined` (the caller falls
|
|
3838
|
-
* back to a self-funded UserOp).
|
|
3839
|
-
* - When the network call throws OR PAFI declines (rate limit, intent
|
|
3840
|
-
* rejection, paymaster outage) → returns `undefined` after logging,
|
|
3841
|
-
* so the controller doesn't hard-fail. The caller's
|
|
3842
|
-
* `prepareMobileUserOp` / `mergePaymasterFields` will gracefully
|
|
3843
|
-
* produce the unsponsored variant.
|
|
3844
|
-
*
|
|
3845
|
-
* Replaces ~30 LoC of try/catch + scenario-to-function mapping every
|
|
3846
|
-
* issuer would copy.
|
|
3847
|
-
*/
|
|
3848
|
-
declare function requestPaymaster(params: RequestPaymasterParams): Promise<Awaited<ReturnType<PafiBackendClient["requestSponsorship"]>> | undefined>;
|
|
3849
|
-
interface RelayUserOpParams {
|
|
3850
|
-
client: PafiBackendClient | null | undefined;
|
|
3851
|
-
/** EntryPoint address — typically `ENTRY_POINT_V08` from core. */
|
|
3852
|
-
entryPoint: typeof ENTRY_POINT_V08 | string;
|
|
3853
|
-
userOp: Record<string, string | null>;
|
|
3854
|
-
/** EIP-7702 authorization (delegation UserOps only). */
|
|
3855
|
-
eip7702Auth?: {
|
|
3856
|
-
chainId: string;
|
|
3857
|
-
address: string;
|
|
3858
|
-
nonce: string;
|
|
3859
|
-
r: string;
|
|
3860
|
-
s: string;
|
|
3861
|
-
yParity: string;
|
|
3862
|
-
};
|
|
3863
|
-
}
|
|
3864
|
-
/**
|
|
3865
|
-
* Submit a serialized UserOp to the Pimlico bundler via PAFI's
|
|
3866
|
-
* sponsor-relayer. Handles the "client missing" / "bundler rejected"
|
|
3867
|
-
* branches as typed errors so the controller can map to HTTP cleanly.
|
|
3868
|
-
*
|
|
3869
|
-
* Every issuer mobile flow has this exact wrapper — moved into SDK
|
|
3870
|
-
* to drop ~30 LoC of try/catch + error-shape boilerplate per
|
|
3871
|
-
* controller.
|
|
3872
|
-
*
|
|
3873
|
-
* Throws:
|
|
3874
|
-
* - `BundlerNotConfiguredError` — caller didn't configure
|
|
3875
|
-
* `PafiBackendClient`. Map to 503.
|
|
3876
|
-
* - `BundlerRejectedError` — bundler returned an error. Map to 422
|
|
3877
|
-
* (the FE can show the reason — usually `AA21` / `AA34` / etc.).
|
|
3878
|
-
*/
|
|
3879
|
-
declare function relayUserOp(params: RelayUserOpParams): Promise<{
|
|
3880
|
-
userOpHash: Hex;
|
|
3881
|
-
}>;
|
|
3882
|
-
|
|
3883
|
-
/**
|
|
3884
|
-
* Registry record returned by `IssuerRegistry.getIssuer()` in V2
|
|
3885
|
-
* dual-bucket. Schema reshape from V1:
|
|
3886
|
-
*
|
|
3887
|
-
* V1: { issuerAddress, signerAddress, name, symbol, active, pointToken, mintingOracle }
|
|
3888
|
-
* V2: { signerAddress, name, active, capitalBase, basisPoints }
|
|
3889
|
-
*
|
|
3890
|
-
* `issuerAddress` is the lookup key the caller passed; we don't
|
|
3891
|
-
* re-emit it here because the caller already has it. `capitalBase` +
|
|
3892
|
-
* `basisPoints` drive the EQUITY-bucket cap (`capitalBase *
|
|
3893
|
-
* basisPoints / 10000`).
|
|
3894
|
-
*/
|
|
3895
|
-
interface IssuerRegistryRecord {
|
|
3896
|
-
signerAddress: Address;
|
|
3897
|
-
name: string;
|
|
3898
|
-
active: boolean;
|
|
3899
|
-
capitalBase: bigint;
|
|
3900
|
-
basisPoints: number;
|
|
3901
|
-
}
|
|
3902
|
-
/**
|
|
3903
|
-
* Equity-bucket cap snapshot computed from the `IssuerRegistryRecord`.
|
|
3904
|
-
*
|
|
3905
|
-
* V1 had a separate `TokenCapRecord` sourced from
|
|
3906
|
-
* `MintingOracle.tokenCaps`; in V2 the oracle is stateless and the
|
|
3907
|
-
* EQUITY cap derives from the registry's `capitalBase` + `basisPoints`.
|
|
3908
|
-
* Kept as a distinct shape (rather than inlining into
|
|
3909
|
-
* `PreValidateMintResult`) so admin tooling can read it without
|
|
3910
|
-
* re-deriving the multiplication.
|
|
3911
|
-
*/
|
|
3912
|
-
interface EquityCapRecord {
|
|
3913
|
-
capitalBase: bigint;
|
|
3914
|
-
basisPoints: number;
|
|
3915
|
-
hardCap: bigint;
|
|
3916
|
-
}
|
|
3917
|
-
interface PreValidateMintResult {
|
|
3918
|
-
/** Registry record read at pre-validation time. */
|
|
3919
|
-
issuer: IssuerRegistryRecord;
|
|
3920
|
-
/** Equity-bucket cap derived from issuer.capitalBase × basisPoints. */
|
|
3921
|
-
equityCap: EquityCapRecord;
|
|
3922
|
-
/** Current on-chain `PointToken.equitySupply()`. */
|
|
3923
|
-
equitySupply: bigint;
|
|
3924
|
-
/** equityCap.hardCap − equitySupply (clamped to 0). */
|
|
3925
|
-
remaining: bigint;
|
|
3926
|
-
}
|
|
3927
|
-
/**
|
|
3928
|
-
* Thrown by `IssuerStateValidator.preValidateMint()`.
|
|
3929
|
-
* `code` maps 1:1 to the HTTP error the issuer API surfaces to clients.
|
|
3930
|
-
*
|
|
3931
|
-
* `MINT_CAP_EXCEEDED` is `safeToRetry: true` because the cap may free
|
|
3932
|
-
* up between requests as other mints expire or settle. The other two
|
|
3933
|
-
* codes are configuration-time states — the FE can't recover by retry.
|
|
3934
|
-
*/
|
|
3935
|
-
type IssuerStateErrorCode = "ISSUER_NOT_REGISTERED" | "ISSUER_INACTIVE" | "MINT_CAP_EXCEEDED";
|
|
3936
|
-
declare class IssuerStateError extends PafiSdkError {
|
|
3937
|
-
readonly httpStatus: "unprocessable";
|
|
3938
|
-
readonly code: IssuerStateErrorCode;
|
|
3939
|
-
readonly details?: Record<string, unknown>;
|
|
3940
|
-
readonly safeToRetry: boolean;
|
|
3941
|
-
constructor(code: IssuerStateErrorCode, message: string, details?: Record<string, unknown>);
|
|
3942
|
-
}
|
|
3943
|
-
|
|
3944
|
-
/**
|
|
3945
|
-
* Pure (framework-agnostic) validator for issuer state.
|
|
3946
|
-
*
|
|
3947
|
-
* Reads IssuerRegistry + PointToken on-chain state and pre-validates
|
|
3948
|
-
* mint requests before the user submits a UserOp. Catching these
|
|
3949
|
-
* off-chain lets issuers fail fast with a clear error rather than
|
|
3950
|
-
* wasting gas on a revert.
|
|
3951
|
-
*
|
|
3952
|
-
* Caching:
|
|
3953
|
-
* - `PointToken.issuer()` — memoized for the process lifetime (immutable)
|
|
3954
|
-
* - Full state (registry + totalSupply) — 30s TTL per PointToken
|
|
3955
|
-
* - Burst calls while a fetch is in-flight share the same Promise
|
|
3956
|
-
* (thundering-herd protection)
|
|
3957
|
-
*
|
|
3958
|
-
* Usage in NestJS: wrap this in an `@Injectable()` service; pass
|
|
3959
|
-
* `PublicClient` and `registryAddress` from your DI container.
|
|
3960
|
-
*/
|
|
3961
|
-
declare class IssuerStateValidator {
|
|
3962
|
-
private readonly provider;
|
|
3963
|
-
private readonly registryAddress;
|
|
3964
|
-
private readonly pointTokenIssuerCache;
|
|
3965
|
-
private readonly stateCache;
|
|
3966
|
-
private readonly inflight;
|
|
3967
|
-
constructor(provider: PublicClient, registryAddress: Address);
|
|
3968
|
-
/**
|
|
3969
|
-
* Convenience factory — reads `registryAddress` from the SDK
|
|
3970
|
-
* `CONTRACT_ADDRESSES` map for the given chain.
|
|
3971
|
-
*/
|
|
3972
|
-
static forChain(provider: PublicClient, chainId: number): IssuerStateValidator;
|
|
3973
|
-
/**
|
|
3974
|
-
* Invalidate cached state for one PointToken, or everything if omitted.
|
|
3975
|
-
* Call after admin txs that change registry or cap settings.
|
|
3976
|
-
*/
|
|
3977
|
-
invalidate(pointToken?: Address): void;
|
|
3978
|
-
/**
|
|
3979
|
-
* Resolve `PointToken.issuer()` once per token and memoize.
|
|
3980
|
-
* The issuer field is set at `initialize()` and never changes.
|
|
3981
|
-
*/
|
|
3982
|
-
getIssuerAddressForPointToken(pointToken: Address): Promise<Address>;
|
|
3983
|
-
/**
|
|
3984
|
-
* Read registry record + totalSupply, with 30s cache and in-flight
|
|
3985
|
-
* deduplication. Does NOT throw on inactive/missing — returns raw state.
|
|
3986
|
-
*/
|
|
3987
|
-
getIssuerState(pointToken: Address): Promise<PreValidateMintResult>;
|
|
3988
|
-
/**
|
|
3989
|
-
* Validate that `amount` PT can be minted on `pointToken` right now.
|
|
3990
|
-
*
|
|
3991
|
-
* Throws `IssuerStateError` with:
|
|
3992
|
-
* - `ISSUER_NOT_REGISTERED` — registry has no record for this issuer
|
|
3993
|
-
* - `ISSUER_INACTIVE` — issuer.active is false
|
|
3994
|
-
* - `MINT_CAP_EXCEEDED` — totalSupply + amount would exceed hardCap
|
|
3995
|
-
*
|
|
3996
|
-
* Returns the fetched state on success so callers can log without a
|
|
3997
|
-
* second RPC round-trip.
|
|
3998
|
-
*/
|
|
3999
|
-
preValidateMint(pointToken: Address, amount: bigint): Promise<PreValidateMintResult>;
|
|
4000
|
-
private fetchIssuerState;
|
|
4001
|
-
}
|
|
4002
|
-
|
|
4003
|
-
/**
|
|
4004
|
-
* SDK-side fallback used when settlement-api is unreachable, returns
|
|
4005
|
-
* 404, or returns 5xx. Keep it permissive enough that an outage doesn't
|
|
4006
|
-
* lock all users out, but tight enough that it's not an abuse vector.
|
|
4007
|
-
*/
|
|
4008
|
-
declare const DEFAULT_REDEMPTION_POLICY: RedemptionPolicy;
|
|
4009
|
-
declare function defaultPolicyFor(issuerId: string): RedemptionPolicy;
|
|
4010
|
-
|
|
4011
|
-
/**
|
|
4012
|
-
* Either a successful policy fetch or a structured failure. We never
|
|
4013
|
-
* throw from `fetchPolicy()` — callers fall back to cache/default on
|
|
4014
|
-
* any failure mode, so a thrown error would just force every caller
|
|
4015
|
-
* to wrap in try/catch.
|
|
4016
|
-
*/
|
|
4017
|
-
type FetchResult = {
|
|
4018
|
-
ok: true;
|
|
4019
|
-
policy: RedemptionPolicy;
|
|
4020
|
-
} | {
|
|
4021
|
-
ok: false;
|
|
4022
|
-
reason: FetchFailureReason;
|
|
4023
|
-
status?: number;
|
|
4024
|
-
};
|
|
4025
|
-
type FetchFailureReason = "TIMEOUT" | "NETWORK" | "NOT_FOUND" | "UNAUTHORIZED" | "SERVER_ERROR" | "INVALID_RESPONSE";
|
|
4026
|
-
declare class SettlementClient {
|
|
4027
|
-
private readonly config;
|
|
4028
|
-
constructor(config: SettlementClientConfig);
|
|
4029
|
-
fetchPolicy(): Promise<FetchResult>;
|
|
4030
|
-
}
|
|
4031
|
-
|
|
4032
|
-
interface UserHistory {
|
|
4033
|
-
/** Total PT redeemed by user in the rolling 24h window. */
|
|
4034
|
-
redeemedLast24hPt: bigint;
|
|
4035
|
-
/** Last redemption timestamp (unix seconds), or null if never. */
|
|
4036
|
-
lastRedeemedAtUnixSec: number | null;
|
|
4037
|
-
}
|
|
4038
|
-
interface EvaluateInput {
|
|
4039
|
-
policy: RedemptionPolicy;
|
|
4040
|
-
policySource: RedemptionPolicySource;
|
|
4041
|
-
history: UserHistory;
|
|
4042
|
-
/** Amount being requested. Pass 0n for a pure preview. */
|
|
4043
|
-
amountPt: bigint;
|
|
4044
|
-
/** Current unix time in seconds (caller-controlled for testability). */
|
|
4045
|
-
nowUnixSec: number;
|
|
4046
|
-
}
|
|
4047
|
-
/**
|
|
4048
|
-
* Pure evaluator. Given a policy + user history snapshot + requested
|
|
4049
|
-
* amount, returns either ALLOW + a preview of the user's remaining
|
|
4050
|
-
* headroom, or DENY + the first failing rule.
|
|
4051
|
-
*
|
|
4052
|
-
* Preview is always populated, even on denial — UI uses it to render
|
|
4053
|
-
* "X PT redeemable now" / "next available at HH:MM" regardless.
|
|
4054
|
-
*/
|
|
4055
|
-
declare function evaluateRedemption(input: EvaluateInput): RedemptionDecision;
|
|
4056
|
-
declare const REDEMPTION_HISTORY_WINDOW_SEC: number;
|
|
4057
|
-
|
|
4058
|
-
/**
|
|
4059
|
-
* In-memory IRedemptionHistoryStore for tests + the bundled NestJS
|
|
4060
|
-
* example. Production issuers should implement this against their
|
|
4061
|
-
* existing burn/audit table — sumRedeemedSince is hot path on every
|
|
4062
|
-
* redemption preview.
|
|
4063
|
-
*/
|
|
4064
|
-
declare class MemoryRedemptionHistoryStore implements IRedemptionHistoryStore {
|
|
4065
|
-
private readonly entries;
|
|
4066
|
-
sumRedeemedSince(user: Address, sinceUnixSec: number, pointTokenAddress?: Address): Promise<bigint>;
|
|
4067
|
-
getLastRedeemedAtUnixSec(user: Address, pointTokenAddress?: Address): Promise<number | null>;
|
|
4068
|
-
recordRedemption(entry: {
|
|
4069
|
-
user: Address;
|
|
4070
|
-
amountPt: bigint;
|
|
4071
|
-
pointTokenAddress?: Address;
|
|
4072
|
-
unixSec: number;
|
|
4073
|
-
}): Promise<void>;
|
|
4074
|
-
}
|
|
4075
|
-
|
|
4076
|
-
declare const PAFI_ISSUER_SDK_VERSION: string;
|
|
4077
|
-
|
|
4078
|
-
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 };
|