@pafi-dev/issuer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1303 @@
1
+ import { Address, Hex, PublicClient, Chain } from 'viem';
2
+ import { PointTokenDomainConfig, MintRequest, EIP712Signature, MintParams, SwapParams, ReceiverConsent, PathKey, PoolKey } from '@pafi-dev/core';
3
+ export { encodeExtData } from '@pafi-dev/core';
4
+
5
+ /**
6
+ * Lifecycle of a minting request as tracked by the issuer's point ledger.
7
+ *
8
+ * PENDING ── on-chain mint confirmed ──▶ MINTED
9
+ * │
10
+ * ├── deadline elapsed without mint ─▶ EXPIRED
11
+ * │
12
+ * └── tx reverted / cancelled ──────▶ FAILED
13
+ */
14
+ type MintingStatus = "PENDING" | "MINTED" | "EXPIRED" | "FAILED";
15
+ /** A locked-amount entry tracking an in-flight mint request. */
16
+ interface LockedMintRequest {
17
+ /** Opaque lock id (used to release / update later) */
18
+ lockId: string;
19
+ userAddress: Address;
20
+ amount: bigint;
21
+ /** Lifecycle status */
22
+ status: MintingStatus;
23
+ /** When the lock was created (epoch ms) */
24
+ createdAt: number;
25
+ /** When the lock auto-expires if not resolved (epoch ms) */
26
+ expiresAt: number;
27
+ /** On-chain transaction hash, set once the mint is confirmed */
28
+ txHash?: Hex;
29
+ }
30
+ /**
31
+ * Issuer point ledger interface — the source of truth for off-chain user
32
+ * point balances and in-flight minting requests.
33
+ *
34
+ * Issuers replace the in-memory default with their own database-backed
35
+ * implementation (Postgres, Redis, etc.).
36
+ */
37
+ interface IPointLedger {
38
+ /** Get a user's available off-chain point balance (excluding locked). */
39
+ getBalance(userAddress: Address): Promise<bigint>;
40
+ /**
41
+ * Lock an amount for a pending mint request. Locked amounts are reserved
42
+ * but not yet deducted; they protect against double-spend during the
43
+ * EIP-712 validity window.
44
+ *
45
+ * @param lockDurationMs how long the lock should be held before auto-expiry
46
+ * @returns lockId — opaque handle used by `releaseLock` / `updateMintStatus`
47
+ * @throws if the user's available balance is below `amount`
48
+ */
49
+ lockForMinting(userAddress: Address, amount: bigint, lockDurationMs: number): Promise<string>;
50
+ /** Release a previously created lock (e.g. on tx failure / cancel). */
51
+ releaseLock(lockId: string): Promise<void>;
52
+ /**
53
+ * Permanently deduct an amount from a user's balance after the on-chain
54
+ * mint has been observed by the indexer. Should also resolve any matching
55
+ * lock so the funds aren't double-counted.
56
+ */
57
+ deductBalance(userAddress: Address, amount: bigint, txHash: Hex): Promise<void>;
58
+ /** Credit points to a user's balance (e.g. from merchant activity). */
59
+ creditBalance(userAddress: Address, amount: bigint, reason: string): Promise<void>;
60
+ /** List currently-pending locked mint requests for a user. */
61
+ getLockedRequests(userAddress: Address): Promise<LockedMintRequest[]>;
62
+ /**
63
+ * Transition a lock to a new lifecycle status. The on-chain tx hash is
64
+ * supplied when the status is `MINTED`.
65
+ */
66
+ updateMintStatus(lockId: string, status: MintingStatus, txHash?: Hex): Promise<void>;
67
+ }
68
+
69
+ /**
70
+ * In-memory IPointLedger implementation for development and tests.
71
+ *
72
+ * NOT for production — state is lost on restart. Issuers should ship their
73
+ * own database-backed implementation.
74
+ *
75
+ * Concurrency model: single-process, single-threaded (Node.js event loop).
76
+ * The lock check + insert is atomic within a tick because no awaits sit
77
+ * between balance read and lock write.
78
+ */
79
+ declare class MemoryPointLedger implements IPointLedger {
80
+ private balances;
81
+ private locks;
82
+ private nextLockId;
83
+ private now;
84
+ constructor(opts?: {
85
+ now?: () => number;
86
+ });
87
+ getBalance(userAddress: Address): Promise<bigint>;
88
+ getLockedRequests(userAddress: Address): Promise<LockedMintRequest[]>;
89
+ creditBalance(userAddress: Address, amount: bigint, _reason: string): Promise<void>;
90
+ lockForMinting(userAddress: Address, amount: bigint, lockDurationMs: number): Promise<string>;
91
+ releaseLock(lockId: string): Promise<void>;
92
+ deductBalance(userAddress: Address, amount: bigint, txHash: Hex): Promise<void>;
93
+ updateMintStatus(lockId: string, status: MintingStatus, txHash?: Hex): Promise<void>;
94
+ /**
95
+ * Auto-expire any PENDING lock past its expiry. Called lazily on every
96
+ * read/write so the in-memory state stays self-cleaning without a timer.
97
+ */
98
+ private purgeExpired;
99
+ private lockedTotalFor;
100
+ }
101
+
102
+ /**
103
+ * Input to a policy evaluation. Policy engines use this to decide whether
104
+ * a user's mint request should be approved.
105
+ */
106
+ interface PolicyEvalRequest {
107
+ userAddress: Address;
108
+ amount: bigint;
109
+ pointTokenAddress: Address;
110
+ chainId: number;
111
+ }
112
+ /** Outcome of a policy evaluation. */
113
+ interface PolicyDecision {
114
+ approved: boolean;
115
+ /**
116
+ * Human-readable reason for rejection (empty on approval). Callers surface
117
+ * this back to the user, so keep it short and non-leaky.
118
+ */
119
+ reason?: string;
120
+ }
121
+ /**
122
+ * Policy engine — evaluates whether a minting request passes issuer rules.
123
+ * Issuers extend the default implementation to add KYC, volume caps, claim
124
+ * budgets, etc.
125
+ */
126
+ interface IPolicyEngine {
127
+ evaluate(request: PolicyEvalRequest): Promise<PolicyDecision>;
128
+ }
129
+
130
+ /**
131
+ * Options for constructing a DefaultPolicyEngine.
132
+ *
133
+ * `mintingOracleAddress` and `provider` are optional — if omitted, the
134
+ * engine skips the on-chain cap check (useful for dev/testing).
135
+ *
136
+ * `verifyMintCap` is injectable so tests can simulate cap rejections
137
+ * without needing a real contract.
138
+ */
139
+ interface DefaultPolicyEngineOptions {
140
+ ledger: IPointLedger;
141
+ provider?: PublicClient;
142
+ mintingOracleAddress?: Address;
143
+ /**
144
+ * Override the on-chain cap check. Defaults to `@pafi/core`'s
145
+ * `verifyMintCap`, which reverts if the issuer's declared supply would
146
+ * be exceeded. A rejected check should throw; returning normally is pass.
147
+ */
148
+ verifyMintCap?: (client: PublicClient, oracle: Address, issuer: Address, amount: bigint) => Promise<void>;
149
+ /**
150
+ * Resolve a point-token address to the issuer address for the cap check.
151
+ * Required iff `mintingOracleAddress + provider` are supplied.
152
+ */
153
+ resolveIssuer?: (pointToken: Address) => Promise<Address>;
154
+ }
155
+ /**
156
+ * Default policy engine — performs two checks:
157
+ *
158
+ * 1. **Off-chain balance** — the user must have at least `amount` of
159
+ * unlocked point balance in the issuer ledger.
160
+ * 2. **On-chain cap** — (optional) calls the MintingOracle via
161
+ * `verifyMintCap` to confirm that minting this amount would not exceed
162
+ * the issuer's declared total supply.
163
+ *
164
+ * Issuers extend this class (or implement `IPolicyEngine` directly) to add
165
+ * KYC, volume caps, claim budgets, or anti-abuse rules.
166
+ */
167
+ declare class DefaultPolicyEngine implements IPolicyEngine {
168
+ private readonly ledger;
169
+ private readonly provider?;
170
+ private readonly mintingOracleAddress?;
171
+ private readonly verifyMintCap?;
172
+ private readonly resolveIssuer?;
173
+ constructor(opts: DefaultPolicyEngineOptions);
174
+ evaluate(request: PolicyEvalRequest): Promise<PolicyDecision>;
175
+ }
176
+
177
+ /**
178
+ * Issuer signer — produces the MintRequest EIP-712 signature that the Relay
179
+ * contract verifies against the issuer's on-chain authorized minter.
180
+ *
181
+ * This is a trust boundary: the default `PrivateKeySigner` holds the key in
182
+ * process memory and is intended for local development only. Production
183
+ * issuers replace this with an HSM/KMS/MPC integration.
184
+ */
185
+ interface IIssuerSigner {
186
+ /**
187
+ * Sign a `MintRequest` message against the point token's EIP-712 domain.
188
+ * The returned signature is what the Relay contract passes to
189
+ * `PointToken.verify` during `mintAndSwap`.
190
+ */
191
+ signMintRequest(domain: PointTokenDomainConfig, message: MintRequest): Promise<EIP712Signature>;
192
+ /** Get the signer's on-chain address (used for verification / logging). */
193
+ getAddress(): Promise<Address>;
194
+ }
195
+
196
+ interface PrivateKeySignerOptions {
197
+ /** 0x-prefixed 32-byte hex private key */
198
+ privateKey: Hex;
199
+ /**
200
+ * Chain metadata for the viem WalletClient. Only the chain id is actually
201
+ * used for signing; a minimal stub is acceptable (it does not need to
202
+ * match the deployed chain config beyond id).
203
+ */
204
+ chain: Chain;
205
+ /**
206
+ * Optional RPC URL. `signTypedData` is offline, so this can usually be
207
+ * left unset — viem only requires a transport to construct the client.
208
+ */
209
+ rpcUrl?: string;
210
+ }
211
+ /**
212
+ * Local-key implementation of `IIssuerSigner`. Wraps viem's `signTypedData`
213
+ * via the shared `@pafi/core` `signMintRequest` helper.
214
+ *
215
+ * ⚠️ **NOT for production use.** The private key lives in process memory
216
+ * and is trivially extractable from a compromised host. Replace with an
217
+ * HSM/KMS/MPC-backed `IIssuerSigner` before deployment.
218
+ */
219
+ declare class PrivateKeySigner implements IIssuerSigner {
220
+ private readonly account;
221
+ private readonly walletClient;
222
+ constructor(opts: PrivateKeySignerOptions);
223
+ signMintRequest(domain: PointTokenDomainConfig, message: MintRequest): Promise<EIP712Signature>;
224
+ getAddress(): Promise<Address>;
225
+ }
226
+
227
+ /**
228
+ * A server-issued session created after a successful wallet login. The
229
+ * token id is embedded in the JWT so sessions can be revoked without
230
+ * invalidating the JWT signing key.
231
+ */
232
+ interface Session {
233
+ tokenId: string;
234
+ userAddress: Address;
235
+ chainId: number;
236
+ issuedAt: Date;
237
+ expiresAt: Date;
238
+ }
239
+ /**
240
+ * Backend store for login nonces and active sessions.
241
+ *
242
+ * The default in-memory implementation is fine for local development but
243
+ * production issuers should plug in Redis / a SQL table so sessions
244
+ * survive restarts and are visible across replicas.
245
+ */
246
+ interface ISessionStore {
247
+ /**
248
+ * Create a single-use login nonce and return it to the caller. Consumers
249
+ * must call {@link consumeNonce} exactly once when the client returns a
250
+ * signed login message quoting this nonce.
251
+ */
252
+ createNonce(): Promise<string>;
253
+ /**
254
+ * Atomically validate + consume a login nonce. Returns `true` if the
255
+ * nonce was known and unexpired (and is now removed); `false` otherwise.
256
+ */
257
+ consumeNonce(nonce: string): Promise<boolean>;
258
+ /** Persist a newly issued session. */
259
+ createSession(session: Session): Promise<void>;
260
+ /** Look up a session by token id, or `null` if unknown / revoked / expired. */
261
+ getSession(tokenId: string): Promise<Session | null>;
262
+ /** Revoke a session by token id (logout). No-op if not found. */
263
+ revokeSession(tokenId: string): Promise<void>;
264
+ /**
265
+ * Revoke every session for a user address. Used when an issuer needs to
266
+ * force re-login (e.g. after a permissions change or suspected abuse).
267
+ */
268
+ revokeAllSessions(userAddress: Address): Promise<void>;
269
+ }
270
+
271
+ interface MemorySessionStoreOptions {
272
+ /**
273
+ * How long a login nonce is valid before auto-expiry. Defaults to 5 min,
274
+ * matching typical SIWE recommendations.
275
+ */
276
+ nonceTtlMs?: number;
277
+ /** Clock override for tests. */
278
+ now?: () => number;
279
+ }
280
+ /**
281
+ * In-memory `ISessionStore` — Map-backed nonces and sessions with lazy
282
+ * expiry sweeps on every read/write.
283
+ *
284
+ * Not safe across processes or restarts; intended for dev/test only.
285
+ */
286
+ declare class MemorySessionStore implements ISessionStore {
287
+ private nonces;
288
+ private sessions;
289
+ private readonly nonceTtlMs;
290
+ private readonly now;
291
+ constructor(opts?: MemorySessionStoreOptions);
292
+ createNonce(): Promise<string>;
293
+ consumeNonce(nonce: string): Promise<boolean>;
294
+ createSession(session: Session): Promise<void>;
295
+ getSession(tokenId: string): Promise<Session | null>;
296
+ revokeSession(tokenId: string): Promise<void>;
297
+ revokeAllSessions(userAddress: Address): Promise<void>;
298
+ private purgeExpiredNonces;
299
+ private purgeExpiredSessions;
300
+ }
301
+
302
+ /**
303
+ * Thin wrapper around an `ISessionStore` exposing nonce generation and
304
+ * single-use consumption as its own named surface. AuthService uses it
305
+ * internally, and issuers can also use it directly for other flows that
306
+ * need replay protection (e.g. a CSRF-style challenge).
307
+ */
308
+ declare class NonceManager {
309
+ private readonly store;
310
+ constructor(store: ISessionStore);
311
+ /** Generate a fresh login nonce. The store is responsible for TTL. */
312
+ generate(): Promise<string>;
313
+ /**
314
+ * Atomically validate + consume a nonce. Returns `true` iff the nonce
315
+ * was known and unexpired (and is now removed from the store).
316
+ */
317
+ consume(nonce: string): Promise<boolean>;
318
+ }
319
+
320
+ interface AuthServiceConfig {
321
+ sessionStore: ISessionStore;
322
+ /** HMAC secret for signing JWTs (HS256). At least 32 bytes recommended. */
323
+ jwtSecret: string;
324
+ /**
325
+ * JWT lifetime passed to `jose`. Accepts any of `jose`'s time strings
326
+ * (`"24h"`, `"7d"`, `"45m"`, …). Defaults to `"24h"`.
327
+ */
328
+ jwtExpiresIn?: string;
329
+ /**
330
+ * Expected domain in the login message, e.g. `"app.example.com"`. The
331
+ * SIWE spec requires the frontend to build the message with this exact
332
+ * domain; the backend rejects any mismatch.
333
+ */
334
+ domain: string;
335
+ /** Expected chain id. */
336
+ chainId: number;
337
+ /** Clock override for tests. */
338
+ now?: () => Date;
339
+ }
340
+ interface LoginResult {
341
+ token: string;
342
+ userAddress: Address;
343
+ tokenId: string;
344
+ expiresAt: Date;
345
+ }
346
+ interface AuthContext {
347
+ userAddress: Address;
348
+ chainId: number;
349
+ tokenId: string;
350
+ }
351
+ /**
352
+ * Wallet-based authentication service implementing the EIP-4361 (Sign-In
353
+ * With Ethereum) login flow with server-issued JWTs.
354
+ *
355
+ * Flow:
356
+ * 1. `getNonce()` — client fetches a nonce, stores it locally
357
+ * 2. Client constructs a login message with the nonce + signs with wallet
358
+ * 3. `login(message, signature)` — server validates fields + signature,
359
+ * consumes the nonce, persists a session, returns a JWT
360
+ * 4. Protected endpoints call `verifyToken(token)` to resolve the user
361
+ * context; the session is re-checked on every call, so `logout()`
362
+ * revokes access immediately.
363
+ */
364
+ declare class AuthService {
365
+ private readonly sessionStore;
366
+ private readonly jwtSecret;
367
+ private readonly jwtExpiresIn;
368
+ private readonly domain;
369
+ private readonly chainId;
370
+ private readonly nonceManager;
371
+ private readonly now;
372
+ constructor(config: AuthServiceConfig);
373
+ /** Generate a fresh login nonce. */
374
+ getNonce(): Promise<string>;
375
+ /**
376
+ * Verify a signed login message and issue a JWT on success.
377
+ * Throws an `AuthError` on any validation failure.
378
+ */
379
+ login(message: string, signature: Hex): Promise<LoginResult>;
380
+ /** Revoke the session backing the given JWT (logout). */
381
+ logout(token: string): Promise<void>;
382
+ /**
383
+ * Verify a JWT and return the authenticated user context. Throws an
384
+ * `AuthError` if the token is missing, malformed, expired, revoked, or
385
+ * signed by a different key.
386
+ */
387
+ verifyToken(token: string): Promise<AuthContext>;
388
+ }
389
+
390
+ /**
391
+ * Extracts a Bearer token from an `Authorization` header value and verifies
392
+ * it via the supplied `AuthService`. Framework-agnostic — issuers wrap this
393
+ * in their framework's middleware pattern (Express, Fastify, Hono, …).
394
+ *
395
+ * Throws an `AuthError` if the header is missing, malformed, or the token
396
+ * fails verification. Callers should translate the `err.code` into an
397
+ * appropriate HTTP status (401 for auth failures, 500 for anything else).
398
+ */
399
+ declare function authenticateRequest(authHeader: string | undefined, authService: AuthService): Promise<AuthContext>;
400
+
401
+ /**
402
+ * Error codes surfaced by the auth subsystem. Issuers can pattern-match on
403
+ * `err.code` to translate into framework-specific HTTP responses.
404
+ */
405
+ 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";
406
+ declare class AuthError extends Error {
407
+ readonly code: AuthErrorCode;
408
+ constructor(code: AuthErrorCode, message: string);
409
+ }
410
+
411
+ /**
412
+ * Parameters for a single `mintAndSwap` relay submission. This is the
413
+ * exact shape the Relay contract expects, with the two structs the
414
+ * `@pafi/core` calldata helpers build. The RelayService wires these
415
+ * through viem's `writeContract`.
416
+ */
417
+ interface SubmitMintAndSwapParams {
418
+ mint: MintParams;
419
+ swap: SwapParams;
420
+ }
421
+ /**
422
+ * Result of a relay submission. `txHash` is returned immediately after
423
+ * the tx is broadcast; `receipt` is present only when the caller opted to
424
+ * wait for confirmation.
425
+ */
426
+ interface RelayResult {
427
+ txHash: Hex;
428
+ blockNumber?: bigint;
429
+ gasUsed?: bigint;
430
+ status?: "success" | "reverted";
431
+ }
432
+ /**
433
+ * Errors raised by RelayService carry a `code` so the MintingGateway (or
434
+ * any caller) can decide whether to release the ledger lock.
435
+ */
436
+ type RelayErrorCode = "NOT_CONFIGURED" | "ENCODE_FAILED" | "SIMULATION_FAILED" | "SUBMIT_FAILED" | "TX_REVERTED" | "TIMEOUT";
437
+ declare class RelayError extends Error {
438
+ readonly code: RelayErrorCode;
439
+ readonly cause?: unknown;
440
+ constructor(code: RelayErrorCode, message: string, cause?: unknown);
441
+ }
442
+ /**
443
+ * Interface an operator wallet must satisfy for the RelayService. Matches
444
+ * the subset of viem's `WalletClient` we actually call, so tests can pass
445
+ * a minimal mock instead of a full client.
446
+ */
447
+ interface OperatorWalletLike {
448
+ writeContract: (args: {
449
+ address: Address;
450
+ abi: readonly unknown[];
451
+ functionName: string;
452
+ args: readonly unknown[];
453
+ account?: {
454
+ address: Address;
455
+ };
456
+ }) => Promise<Hex>;
457
+ account?: {
458
+ address: Address;
459
+ } | undefined;
460
+ }
461
+
462
+ interface RelayServiceConfig {
463
+ /** Address of the deployed Relay contract (chain-specific). */
464
+ relayAddress: Address;
465
+ /** Operator wallet that pays gas and receives the operator fee. */
466
+ operatorWallet: OperatorWalletLike;
467
+ /**
468
+ * Provider used for pre-flight simulation and receipt waiting. Optional —
469
+ * if omitted, the service still broadcasts but returns only `txHash`
470
+ * (caller is responsible for confirmation tracking).
471
+ */
472
+ provider?: PublicClient;
473
+ /**
474
+ * If a provider is supplied, wait up to this many milliseconds for a
475
+ * receipt before timing out. Default: 60_000 (one minute).
476
+ */
477
+ confirmationTimeoutMs?: number;
478
+ /**
479
+ * Whether to run `simulateContract` before submitting. Catches most
480
+ * reverts locally without wasting gas. Default: true (when provider is
481
+ * supplied).
482
+ */
483
+ simulateBeforeSubmit?: boolean;
484
+ }
485
+ /**
486
+ * Submits `mintAndSwap` transactions to the Relay contract on behalf of
487
+ * the issuer. This is the single place the operator wallet is touched; the
488
+ * MintingGateway calls into `submitMintAndSwap()` after it has signed the
489
+ * MintRequest and verified the ReceiverConsent.
490
+ *
491
+ * The service is intentionally thin: calldata encoding stays in `@pafi/core`
492
+ * (`encodeMintAndSwap`), so on-chain ABI changes only ripple through one
493
+ * package.
494
+ */
495
+ declare class RelayService {
496
+ private readonly relayAddress;
497
+ private readonly operatorWallet;
498
+ private readonly provider?;
499
+ private readonly confirmationTimeoutMs;
500
+ private readonly simulateBeforeSubmit;
501
+ constructor(config: RelayServiceConfig);
502
+ /** Address the operator wallet is broadcasting from (for logging). */
503
+ operatorAddress(): Address | undefined;
504
+ /**
505
+ * Build calldata for the Relay `mintAndSwap` function. Kept public so
506
+ * callers (e.g. the MintingGateway) can log or persist the encoded call
507
+ * for audit before broadcasting.
508
+ */
509
+ encodeCall(params: SubmitMintAndSwapParams): Hex;
510
+ /**
511
+ * Submit a `mintAndSwap` transaction. Flow:
512
+ *
513
+ * 1. (optional) pre-flight simulate via provider
514
+ * 2. writeContract through the operator wallet
515
+ * 3. (optional) wait for the receipt and surface gasUsed / status
516
+ *
517
+ * Throws a typed `RelayError` on any failure so the MintingGateway can
518
+ * decide whether to release the ledger lock (`SUBMIT_FAILED` and
519
+ * `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
520
+ * need manual review because the tx may still land).
521
+ */
522
+ submitMintAndSwap(params: SubmitMintAndSwapParams): Promise<RelayResult>;
523
+ }
524
+
525
+ interface FeeManagerConfig {
526
+ /** Provider used for gas price + balance reads. */
527
+ provider: PublicClient;
528
+ /** Operator wallet whose native balance the manager monitors. */
529
+ operatorWallet: OperatorWalletLike;
530
+ /** USDT token address on the target chain (used for rebalance swaps). */
531
+ usdtAddress: Address;
532
+ /** Wrapped-native token address (WETH on Base/Ethereum, WMATIC, etc). */
533
+ nativeWrappedAddress: Address;
534
+ /**
535
+ * Typical gas used by a `mintAndSwap` transaction. Default: 500_000. The
536
+ * manager multiplies this by current gas price to get the native cost,
537
+ * then converts to USDT via the injected `quoteNativeToUsdt`.
538
+ */
539
+ mintAndSwapGasUnits?: bigint;
540
+ /**
541
+ * Safety margin applied to the gas estimate before charging the user.
542
+ * Expressed as a basis-point multiplier, e.g. 12_000 = 120%. Default 12_000.
543
+ */
544
+ gasPremiumBps?: number;
545
+ /**
546
+ * Price conversion: given an amount of native token (wei), return the
547
+ * equivalent amount of USDT. Injected so the manager is chain-agnostic —
548
+ * production wires this to `@pafi/core` V4 quoting or an oracle feed.
549
+ */
550
+ quoteNativeToUsdt: (amountNative: bigint) => Promise<bigint>;
551
+ /**
552
+ * Rebalance trigger: when the operator's native balance falls below
553
+ * `rebalanceThresholdWei`, `rebalanceIfNeeded()` swaps `rebalanceUsdtAmount`
554
+ * worth of USDT into native. Both optional — omit to disable rebalancing.
555
+ */
556
+ rebalanceThresholdWei?: bigint;
557
+ rebalanceUsdtAmount?: bigint;
558
+ /**
559
+ * Actual swap executor — the manager calls this when a rebalance is
560
+ * triggered. Injected so the SDK does not hard-code a DEX choice; the
561
+ * issuer wires it to the UniversalRouter (via `@pafi/core swap/`) or
562
+ * whatever liquidity venue they trust. Required iff the rebalance
563
+ * fields above are set.
564
+ */
565
+ swapUsdtToNative?: (amountUsdt: bigint) => Promise<void>;
566
+ }
567
+ /**
568
+ * Manages the operator wallet's economics:
569
+ *
570
+ * 1. `estimateGasFee()` — how many USDT to deduct from the swap proceeds
571
+ * to cover the operator's gas cost for the upcoming `mintAndSwap`.
572
+ * 2. `rebalanceIfNeeded()` — when the operator's native balance gets
573
+ * low, swap some of the accumulated USDT fee back into native gas
574
+ * token so the operator never runs dry.
575
+ *
576
+ * Both calculations are intentionally injection-based: gas estimation and
577
+ * USDT→native swapping both depend on DEX state, which the SDK deliberately
578
+ * does not own. Issuers supply the conversion + swap functions.
579
+ */
580
+ declare class FeeManager {
581
+ private readonly provider;
582
+ private readonly operatorWallet;
583
+ private readonly mintAndSwapGasUnits;
584
+ private readonly gasPremiumBps;
585
+ private readonly quoteNativeToUsdt;
586
+ private readonly rebalanceThresholdWei?;
587
+ private readonly rebalanceUsdtAmount?;
588
+ private readonly swapUsdtToNative?;
589
+ constructor(config: FeeManagerConfig);
590
+ /**
591
+ * Estimate the USDT fee the operator should charge for a single
592
+ * `mintAndSwap`. Computed as:
593
+ *
594
+ * nativeCost = gasUnits × gasPrice
595
+ * premiumNativeCost = nativeCost × premiumBps / 10_000
596
+ * usdtFee = quoteNativeToUsdt(premiumNativeCost)
597
+ */
598
+ estimateGasFee(): Promise<bigint>;
599
+ /**
600
+ * Check the operator's native balance and, if it has dropped below the
601
+ * configured threshold, trigger a USDT→native rebalance via the injected
602
+ * `swapUsdtToNative` function.
603
+ *
604
+ * Returns `true` if a rebalance was performed, `false` otherwise.
605
+ * Silently no-ops when rebalance is not configured.
606
+ */
607
+ rebalanceIfNeeded(): Promise<boolean>;
608
+ }
609
+
610
+ /**
611
+ * End-user request for a full "burn points → receive USDT" flow. The
612
+ * receiver has already signed the `ReceiverConsent` EIP-712 message on
613
+ * the frontend; the issuer backend runs everything else.
614
+ */
615
+ interface MintAndCashOutRequest {
616
+ /** Address owning the off-chain points (== receiver in the consent). */
617
+ userAddress: Address;
618
+ /** Point token contract to mint. */
619
+ pointTokenAddress: Address;
620
+ /** Chain id the relay will submit on. */
621
+ chainId: number;
622
+ /**
623
+ * EIP-712 domain for `pointTokenAddress`. The gateway passes this
624
+ * straight through to `@pafi/core` `verifyReceiverConsent` and the
625
+ * issuer signer. Callers typically fetch it once via
626
+ * `PafiSDK.getDomain()` and cache it.
627
+ */
628
+ domain: PointTokenDomainConfig;
629
+ /**
630
+ * Receiver consent message + signature, pre-built by the frontend. The
631
+ * message specifies `onBehalfOf = relay contract` and
632
+ * `originalReceiver = userAddress`, plus the amount, nonce, deadline,
633
+ * and encoded extData.
634
+ */
635
+ receiverConsent: ReceiverConsent;
636
+ receiverSignature: Hex;
637
+ /**
638
+ * Swap path for the USDT output. Empty array = "no swap" (rare — only
639
+ * useful for testing). Normally a single hop pointToken → USDT.
640
+ */
641
+ swapPath: PathKey[];
642
+ /** Swap deadline (unix seconds). */
643
+ swapDeadline: bigint;
644
+ /**
645
+ * Lock TTL (ms) to apply to the off-chain balance. The gateway computes
646
+ * a safe default from `receiverConsent.deadline` if omitted.
647
+ */
648
+ lockDurationMs?: number;
649
+ }
650
+ /**
651
+ * Result returned to the caller after a successful `processMintAndCashOut`.
652
+ * The `lockId` is preserved so the indexer can correlate the on-chain
653
+ * Mint event back to the ledger row (though that correlation is typically
654
+ * done by `(userAddress, amount)` in the default `MemoryPointLedger`).
655
+ */
656
+ interface MintAndCashOutResponse {
657
+ txHash: Hex;
658
+ lockId: string;
659
+ blockNumber?: bigint;
660
+ gasUsed?: bigint;
661
+ }
662
+ /**
663
+ * Error codes a MintingGateway can surface. Callers (API handlers) map
664
+ * these to HTTP status codes. The `SAFE_TO_RETRY` field tells the caller
665
+ * whether the underlying lock was released — if not, the user should
666
+ * wait before retrying to avoid double-spend.
667
+ */
668
+ type MintingGatewayErrorCode = "INVALID_REQUEST" | "INVALID_CONSENT_SIGNATURE" | "CONSENT_EXPIRED" | "POLICY_REJECTED" | "INSUFFICIENT_BALANCE" | "SIGNER_FAILED" | "RELAY_SIMULATION_FAILED" | "RELAY_SUBMIT_FAILED" | "RELAY_REVERTED" | "RELAY_TIMEOUT";
669
+ declare class MintingGatewayError extends Error {
670
+ readonly code: MintingGatewayErrorCode;
671
+ /**
672
+ * True if the ledger lock was released before this error was thrown,
673
+ * meaning the user can safely retry. False means the funds are still
674
+ * locked (e.g. tx may still land on-chain) and retry would double-spend.
675
+ */
676
+ readonly safeToRetry: boolean;
677
+ readonly cause?: unknown;
678
+ constructor(code: MintingGatewayErrorCode, message: string, opts: {
679
+ safeToRetry: boolean;
680
+ cause?: unknown;
681
+ });
682
+ }
683
+
684
+ interface MintingGatewayConfig {
685
+ ledger: IPointLedger;
686
+ policy: IPolicyEngine;
687
+ signer: IIssuerSigner;
688
+ relayService: RelayService;
689
+ /**
690
+ * Clock override for tests. Defaults to `() => Date.now()`. Used to
691
+ * compute safe lock TTLs and to check consent deadlines.
692
+ */
693
+ now?: () => number;
694
+ /**
695
+ * Extra buffer (ms) added on top of `(consent.deadline - now)` when the
696
+ * caller doesn't supply a `lockDurationMs`. Default: 60_000 (one minute
697
+ * — roughly 2× the Base L2 block confirmation time).
698
+ */
699
+ defaultLockBufferMs?: number;
700
+ }
701
+ /**
702
+ * The MintingGateway is the central orchestrator that turns a user's
703
+ * signed ReceiverConsent into an on-chain `mintAndSwap` tx and a
704
+ * consistent off-chain ledger update.
705
+ *
706
+ * 11-step flow (per `PAFI_ISSUER_SDK_SPEC.md` §4.3):
707
+ *
708
+ * 1. Field validation (cheap rejects before any crypto)
709
+ * 2. Verify ReceiverConsent signature via `@pafi/core`
710
+ * 3. Check off-chain balance via ledger
711
+ * 4. Check locked requests via ledger
712
+ * 5. Run policy engine (balance + on-chain cap + issuer rules)
713
+ * 6. Lock the requested amount in the ledger
714
+ * 7. Sign MintRequest as issuer
715
+ * 8. Build MintParams + SwapParams
716
+ * 9. Submit via RelayService (encode + simulate + broadcast + wait)
717
+ * 10. Return { txHash, lockId, receipt fields }
718
+ * 11. On any failure, release the lock IF it's safe to retry — i.e. we
719
+ * know the tx cannot still land on-chain. If the tx may have made
720
+ * it (`TX_REVERTED` or `TIMEOUT`), we leave the lock alone and
721
+ * surface `safeToRetry: false` so the caller doesn't double-spend.
722
+ *
723
+ * The gateway deliberately does NOT deduct the balance here. Deduction
724
+ * happens in the `PointIndexer` when the on-chain Mint event is observed,
725
+ * which is what makes the system crash-safe: if the gateway dies between
726
+ * broadcast and receipt, the indexer will still finalize the ledger.
727
+ */
728
+ declare class MintingGateway {
729
+ private readonly ledger;
730
+ private readonly policy;
731
+ private readonly signer;
732
+ private readonly relayService;
733
+ private readonly now;
734
+ private readonly defaultLockBufferMs;
735
+ constructor(config: MintingGatewayConfig);
736
+ processMintAndCashOut(request: MintAndCashOutRequest): Promise<MintAndCashOutResponse>;
737
+ private computeLockDurationMs;
738
+ /**
739
+ * Map a RelayError to a MintingGatewayError, releasing the lock only
740
+ * when the tx definitely did not land. `TX_REVERTED` and `TIMEOUT`
741
+ * leave the lock in place because the tx may still be in the mempool
742
+ * or already mined — releasing would enable a double-spend on retry.
743
+ * Always throws.
744
+ */
745
+ private handleRelayFailure;
746
+ /**
747
+ * Release a lock, swallowing any secondary error. We never want a lock
748
+ * release failure to mask the original error — the lock will auto-expire
749
+ * via TTL anyway.
750
+ */
751
+ private releaseLockSafely;
752
+ }
753
+
754
+ /** Decoded Transfer(from=0x0 → to) event used to finalize a mint. */
755
+ interface MintEvent {
756
+ /** Destination address (the user who received the minted points) */
757
+ to: Address;
758
+ /** Amount minted (in wei / base units) */
759
+ amount: bigint;
760
+ /** Block number the mint was included in */
761
+ blockNumber: bigint;
762
+ /** Transaction hash that emitted the event */
763
+ txHash: Hex;
764
+ /** Log index within the tx, for deterministic ordering */
765
+ logIndex: number;
766
+ }
767
+ /**
768
+ * Cursor persistence interface — the indexer reports the next block
769
+ * number it is about to process so the caller can write it to Redis /
770
+ * Postgres / a file. The SDK does not own persistence because every
771
+ * issuer has their own storage stack.
772
+ */
773
+ interface IIndexerCursorStore {
774
+ /** Return the last persisted cursor (`undefined` on first run). */
775
+ load(): Promise<bigint | undefined>;
776
+ /** Persist a new cursor value. Called after each successful batch. */
777
+ save(blockNumber: bigint): Promise<void>;
778
+ }
779
+ /**
780
+ * No-op cursor store. Useful when the caller wants to drive the cursor
781
+ * entirely via `processBlockRange()` and doesn't need persistence.
782
+ */
783
+ declare class InMemoryCursorStore implements IIndexerCursorStore {
784
+ private cursor;
785
+ load(): Promise<bigint | undefined>;
786
+ save(blockNumber: bigint): Promise<void>;
787
+ }
788
+
789
+ interface PointIndexerConfig {
790
+ provider: PublicClient;
791
+ pointTokenAddress: Address;
792
+ ledger: IPointLedger;
793
+ /**
794
+ * Block to start from on first run. Ignored if the cursor store already
795
+ * has a value. Defaults to `0n`.
796
+ */
797
+ fromBlock?: bigint;
798
+ /**
799
+ * Persistent cursor store. Defaults to an `InMemoryCursorStore` which
800
+ * only survives inside the current process.
801
+ */
802
+ cursorStore?: IIndexerCursorStore;
803
+ /**
804
+ * Reorg safety: only treat events as final after this many confirmations.
805
+ * The indexer reads `latestBlock - confirmations` as its high-water mark.
806
+ * Default: 3 (a conservative number for Base L2).
807
+ */
808
+ confirmations?: number;
809
+ /**
810
+ * How many blocks to scan per `getLogs` call. Keeps RPC pages bounded.
811
+ * Default: 2000 (comfortably under most provider page limits).
812
+ */
813
+ batchSize?: number;
814
+ /**
815
+ * Polling interval (ms) used by `start()`. Default: 5000 (5s).
816
+ */
817
+ pollIntervalMs?: number;
818
+ /** Clock override for tests. */
819
+ now?: () => number;
820
+ }
821
+ /**
822
+ * Watches `PointToken.Transfer(from=0x0 → to)` events and finalizes the
823
+ * issuer ledger on each confirmed mint.
824
+ *
825
+ * Finalization strategy (per event):
826
+ * 1. Find a PENDING locked mint request for `to` with matching amount.
827
+ * 2. Call `ledger.deductBalance(to, amount, txHash)` — this is idempotent
828
+ * in the default `MemoryPointLedger` because deducting also resolves
829
+ * the matching lock.
830
+ * 3. If no matching lock is found (e.g. a manual `PointToken.mint()` call
831
+ * or a race where the lock already expired), log and skip — this
832
+ * intentionally does NOT credit the ledger, because the gateway is
833
+ * the only sanctioned way to spawn a mint.
834
+ *
835
+ * Reorg safety: events are only processed when they are at least
836
+ * `confirmations` blocks deep. A new `getLogs` call on every poll picks
837
+ * up finalized blocks from the persisted cursor.
838
+ *
839
+ * Pure-polling design (rather than `watchContractEvent`) keeps the SDK
840
+ * compatible with HTTP-only providers and with RPC rotation.
841
+ */
842
+ declare class PointIndexer {
843
+ private readonly provider;
844
+ private readonly pointTokenAddress;
845
+ private readonly ledger;
846
+ private readonly cursorStore;
847
+ private readonly startBlock;
848
+ private readonly confirmations;
849
+ private readonly batchSize;
850
+ private readonly pollIntervalMs;
851
+ private running;
852
+ private timer;
853
+ constructor(config: PointIndexerConfig);
854
+ /** Begin polling. Schedules `tick()` on a loop. */
855
+ start(): void;
856
+ /** Stop polling. Safe to call multiple times. */
857
+ stop(): void;
858
+ /**
859
+ * Run one poll cycle: load cursor → scan [cursor, safeHead] in
860
+ * `batchSize` chunks → persist new cursor. Swallows any error and
861
+ * schedules the next tick. Visible for test harnesses via a public name.
862
+ */
863
+ tick(): Promise<void>;
864
+ private scheduleNext;
865
+ /**
866
+ * Scan `[from, to]` inclusive for mint events in `batchSize` chunks.
867
+ * Callers can use this directly to backfill a specific range without
868
+ * engaging `start()`. On completion, the cursor is advanced to `to + 1`.
869
+ */
870
+ processBlockRange(from: bigint, to: bigint): Promise<void>;
871
+ private decodeMintEvents;
872
+ /**
873
+ * Finalize a single mint event: match it to a PENDING lock in the
874
+ * ledger, then call `deductBalance` (which also resolves the lock in
875
+ * the default `MemoryPointLedger`).
876
+ *
877
+ * No-matching-lock is a valid state: it means either the lock already
878
+ * expired, or the mint was authorized out-of-band (e.g. a direct
879
+ * `PointToken.mint()` from an EOA minter for testing). In that case we
880
+ * do NOT touch the ledger — crediting here would silently allow the
881
+ * issuer to mint without going through the gateway.
882
+ */
883
+ private finalize;
884
+ }
885
+
886
+ interface ApiConfigResponse {
887
+ chainId: number;
888
+ contracts: {
889
+ pointToken?: Address;
890
+ relay?: Address;
891
+ issuerRegistry?: Address;
892
+ pointTokenFactory?: Address;
893
+ mintingOracle?: Address;
894
+ poolManager?: Address;
895
+ usdt?: Address;
896
+ };
897
+ }
898
+ interface ApiNonceResponse {
899
+ nonce: string;
900
+ }
901
+ interface ApiLoginRequest {
902
+ message: string;
903
+ signature: Hex;
904
+ }
905
+ interface ApiLoginResponse {
906
+ token: string;
907
+ userAddress: Address;
908
+ /** Unix ms when the JWT (and backing session) expires. */
909
+ expiresAt: number;
910
+ }
911
+ interface ApiGasFeeResponse {
912
+ /** Gas fee quoted in USDT (6-decimal base units). */
913
+ gasFeeUsdt: bigint;
914
+ }
915
+ interface ApiPoolsRequest {
916
+ chainId: number;
917
+ pointTokenAddress: Address;
918
+ }
919
+ interface ApiPoolsResponse {
920
+ pools: PoolKey[];
921
+ }
922
+ interface ApiUserRequest {
923
+ chainId: number;
924
+ userAddress: Address;
925
+ pointTokenAddress: Address;
926
+ }
927
+ interface ApiUserResponse {
928
+ mintRequestNonce: bigint;
929
+ receiverConsentNonce: bigint;
930
+ balance: bigint;
931
+ isMinter: boolean;
932
+ }
933
+ interface ApiClaimAndSwapRequest {
934
+ chainId: number;
935
+ pointTokenAddress: Address;
936
+ /**
937
+ * EIP-712 domain for the point token. Frontends read this once (via
938
+ * `PafiSDK.getDomain()` or a direct contract call) and include it on
939
+ * every claim request so the backend doesn't need to re-read it.
940
+ */
941
+ domain: PointTokenDomainConfig;
942
+ /** The full signed ReceiverConsent message. */
943
+ receiverConsent: ReceiverConsent;
944
+ /** Detached EIP-712 signature (`serialized` form from `@pafi/core`). */
945
+ receiverSignature: Hex;
946
+ /** Swap hop(s) from pointToken → USDT. */
947
+ swapPath: PathKey[];
948
+ /** Unix seconds. */
949
+ swapDeadline: bigint;
950
+ }
951
+ interface ApiClaimAndSwapResponse {
952
+ txHash: Hex;
953
+ lockId: string;
954
+ blockNumber?: bigint;
955
+ gasUsed?: bigint;
956
+ }
957
+ interface ApiBuildConsentTypedDataRequest {
958
+ chainId: number;
959
+ pointTokenAddress: Address;
960
+ receiverConsent: ReceiverConsent;
961
+ }
962
+ interface ApiBuildConsentTypedDataResponse {
963
+ typedData: {
964
+ domain: {
965
+ name: string;
966
+ version: string;
967
+ chainId: number;
968
+ verifyingContract: Address;
969
+ };
970
+ types: Record<string, {
971
+ name: string;
972
+ type: string;
973
+ }[]>;
974
+ primaryType: string;
975
+ message: Record<string, unknown>;
976
+ };
977
+ }
978
+ type PoolsProvider = (request: ApiPoolsRequest) => Promise<ApiPoolsResponse>;
979
+
980
+ interface IssuerApiHandlersConfig {
981
+ authService: AuthService;
982
+ gateway: MintingGateway;
983
+ ledger: IPointLedger;
984
+ /** Used by `handleUser` to read on-chain nonces and minter status. */
985
+ provider: PublicClient;
986
+ pointTokenAddress: Address;
987
+ chainId: number;
988
+ contracts: ApiConfigResponse["contracts"];
989
+ /** Required by `handleGasFee`; omit to disable the endpoint. */
990
+ feeManager?: FeeManager;
991
+ /** Required by `handlePools`; omit to disable the endpoint. */
992
+ poolsProvider?: PoolsProvider;
993
+ }
994
+ /**
995
+ * Framework-agnostic HTTP handlers that match the endpoints a `PafiSDK`
996
+ * frontend expects to call. Issuers wrap these in Express / Fastify /
997
+ * Hono / whatever — the handlers take plain inputs and return plain
998
+ * outputs, with `AuthError` / `MintingGatewayError` surfaced as typed
999
+ * exceptions.
1000
+ *
1001
+ * Every protected handler takes a pre-verified `userAddress` as its first
1002
+ * argument. The issuer's middleware wraps `authenticateRequest()` from
1003
+ * `@pafi/issuer` to extract that address from the Bearer token before
1004
+ * routing.
1005
+ */
1006
+ declare class IssuerApiHandlers {
1007
+ private readonly authService;
1008
+ private readonly gateway;
1009
+ private readonly ledger;
1010
+ private readonly provider;
1011
+ private readonly pointTokenAddress;
1012
+ private readonly chainId;
1013
+ private readonly contracts;
1014
+ private readonly feeManager?;
1015
+ private readonly poolsProvider?;
1016
+ constructor(config: IssuerApiHandlersConfig);
1017
+ /** `GET /auth/nonce` */
1018
+ handleGetNonce(): Promise<ApiNonceResponse>;
1019
+ /** `POST /auth/login` */
1020
+ handleLogin(body: ApiLoginRequest): Promise<ApiLoginResponse>;
1021
+ /**
1022
+ * `GET /config?chainId=<id>`
1023
+ *
1024
+ * Returns the contract addresses and chain id that the frontend SDK
1025
+ * needs to build EIP-712 messages and interact with on-chain.
1026
+ */
1027
+ handleConfig(chainId: number): Promise<ApiConfigResponse>;
1028
+ /** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
1029
+ handleGasFee(): Promise<ApiGasFeeResponse>;
1030
+ /** `POST /auth/logout` */
1031
+ handleLogout(token: string): Promise<void>;
1032
+ /**
1033
+ * `GET /pools?chainId=<id>&pointToken=<addr>`
1034
+ *
1035
+ * Delegates to the injected `PoolsProvider`. The handler itself does
1036
+ * not know where pools come from — that's an issuer decision.
1037
+ */
1038
+ handlePools(_userAddress: Address, request: ApiPoolsRequest): Promise<ApiPoolsResponse>;
1039
+ /**
1040
+ * `GET /user?chainId=<id>&user=<addr>&pointToken=<addr>`
1041
+ *
1042
+ * Returns per-user state the frontend needs to build a fresh
1043
+ * `ReceiverConsent`: on-chain nonces + minter status + off-chain
1044
+ * balance.
1045
+ */
1046
+ handleUser(userAddress: Address, request: ApiUserRequest): Promise<ApiUserResponse>;
1047
+ /**
1048
+ * `POST /build-consent-typed-data`
1049
+ *
1050
+ * Backend builds the full EIP-712 typed data payload for a
1051
+ * ReceiverConsent message. The domain (name, version, chainId,
1052
+ * verifyingContract) is read from the PointToken contract — mobile
1053
+ * never needs to know or hardcode these values. Forward the
1054
+ * response directly to `wallet.signTypedData()`.
1055
+ *
1056
+ * This ensures a single source of truth for domain + types, and
1057
+ * makes contract upgrades (domain version bump) transparent to
1058
+ * mobile apps — no app store review needed.
1059
+ */
1060
+ handleBuildConsentTypedData(userAddress: Address, request: ApiBuildConsentTypedDataRequest): Promise<ApiBuildConsentTypedDataResponse>;
1061
+ /**
1062
+ * `POST /claim-and-swap`
1063
+ *
1064
+ * The terminal handler: forwards the verified consent to the
1065
+ * MintingGateway, which runs the 11-step flow.
1066
+ */
1067
+ handleClaimAndSwap(userAddress: Address, request: ApiClaimAndSwapRequest): Promise<ApiClaimAndSwapResponse>;
1068
+ }
1069
+
1070
+ /**
1071
+ * Config for `createSubgraphPoolsProvider`.
1072
+ */
1073
+ interface SubgraphPoolsProviderConfig {
1074
+ /**
1075
+ * Fully qualified subgraph GraphQL endpoint. Example:
1076
+ * `https://graph.pacificfinance.org/subgraphs/name/pafi`
1077
+ */
1078
+ subgraphUrl: string;
1079
+ /**
1080
+ * Cache TTL in milliseconds. Pool discovery is near-static — a 30s
1081
+ * cache removes subgraph load without meaningfully delaying UX.
1082
+ * Set to 0 to disable caching. Default: 30_000.
1083
+ */
1084
+ cacheTtlMs?: number;
1085
+ /**
1086
+ * Optional fetch override for test harnesses. Defaults to global `fetch`.
1087
+ */
1088
+ fetchImpl?: typeof fetch;
1089
+ /**
1090
+ * Optional clock override for tests.
1091
+ */
1092
+ now?: () => number;
1093
+ }
1094
+ /**
1095
+ * Create a `PoolsProvider` backed by the PAFI subgraph.
1096
+ *
1097
+ * Queries the `pafiTokens` entity for the given `pointTokenAddress`,
1098
+ * reads its linked `Pool`, and returns a single-element `PoolKey[]`.
1099
+ * Multiple pools per token would require a subgraph schema change.
1100
+ *
1101
+ * The result is cached in-process with a short TTL (default 30s). Pool
1102
+ * discovery is near-static so this avoids hammering the subgraph without
1103
+ * blocking config changes for long.
1104
+ *
1105
+ * Returns `{ pools: [] }` if:
1106
+ * - the token is not registered on PAFI yet (no PafiToken entity)
1107
+ * - the token is registered but its pool has not been initialised
1108
+ * - the subgraph is unreachable or returns an error (logs to console,
1109
+ * does not throw — callers should handle empty pool list gracefully)
1110
+ *
1111
+ * Assumes the PAFI subgraph schema. Issuers with a custom subgraph must
1112
+ * implement `PoolsProvider` themselves instead of using this helper.
1113
+ *
1114
+ * @example
1115
+ * ```ts
1116
+ * import { createSubgraphPoolsProvider, createIssuerService } from "@pafi/issuer";
1117
+ *
1118
+ * const service = createIssuerService({
1119
+ * // ...other config
1120
+ * poolsProvider: createSubgraphPoolsProvider({
1121
+ * subgraphUrl: "https://graph.pacificfinance.org/subgraphs/name/pafi",
1122
+ * }),
1123
+ * });
1124
+ * ```
1125
+ */
1126
+ declare function createSubgraphPoolsProvider(config: SubgraphPoolsProviderConfig): PoolsProvider;
1127
+
1128
+ /**
1129
+ * Config for `createSubgraphNativeUsdtQuoter`.
1130
+ */
1131
+ interface SubgraphNativeUsdtQuoterConfig {
1132
+ /**
1133
+ * Fully qualified subgraph GraphQL endpoint. Same URL used by
1134
+ * `createSubgraphPoolsProvider`.
1135
+ */
1136
+ subgraphUrl: string;
1137
+ /**
1138
+ * Decimals of the USDT token. Defaults to 6 (standard USDT/USDC on
1139
+ * Base, Ethereum, Polygon). Override for chains where USDT uses a
1140
+ * different decimals value.
1141
+ */
1142
+ usdtDecimals?: number;
1143
+ /**
1144
+ * Decimals of the native token (ETH on Base/mainnet/Arbitrum/Optimism,
1145
+ * MATIC on Polygon). Default: 18.
1146
+ */
1147
+ nativeDecimals?: number;
1148
+ /**
1149
+ * Cache TTL in milliseconds. ETH price drifts slowly relative to gas
1150
+ * estimation needs — a 30s cache keeps fees stable across bursts of
1151
+ * requests without letting them go stale during volatile markets.
1152
+ * Set to 0 to disable caching. Default: 30_000.
1153
+ */
1154
+ cacheTtlMs?: number;
1155
+ /**
1156
+ * Fallback price (USDT per native token, human-readable float) used
1157
+ * when the subgraph is unreachable. This keeps the backend operational
1158
+ * during subgraph outages rather than bricking cashouts. The fee will
1159
+ * be slightly off but the operator still gets paid. Default: 3000.
1160
+ */
1161
+ fallbackEthPriceUsd?: number;
1162
+ /** Optional fetch override for test harnesses. */
1163
+ fetchImpl?: typeof fetch;
1164
+ /** Optional clock override for tests. */
1165
+ now?: () => number;
1166
+ }
1167
+ /**
1168
+ * Create a `quoteNativeToUsdt` function backed by the PAFI subgraph's
1169
+ * `Bundle.ethPriceUSD`.
1170
+ *
1171
+ * Used by `FeeManager.estimateGasFee()` to convert the operator's native
1172
+ * gas cost into the USDT amount deducted from the user's cashout. Price
1173
+ * precision is not critical here — a 1-2% drift is acceptable since
1174
+ * the operator already takes a `gasPremiumBps` buffer.
1175
+ *
1176
+ * The result is cached in-process with a short TTL (default 30s). If the
1177
+ * subgraph is unreachable, falls back to `fallbackEthPriceUsd` so gas
1178
+ * estimation doesn't block cashouts during a subgraph outage.
1179
+ *
1180
+ * @example
1181
+ * ```ts
1182
+ * import { createSubgraphNativeUsdtQuoter, createIssuerService } from "@pafi/issuer";
1183
+ *
1184
+ * const service = createIssuerService({
1185
+ * // ...other config
1186
+ * fee: {
1187
+ * quoteNativeToUsdt: createSubgraphNativeUsdtQuoter({
1188
+ * subgraphUrl: "https://graph.pacificfinance.org/subgraphs/name/pafi",
1189
+ * }),
1190
+ * },
1191
+ * });
1192
+ * ```
1193
+ */
1194
+ declare function createSubgraphNativeUsdtQuoter(config: SubgraphNativeUsdtQuoterConfig): (amountNative: bigint) => Promise<bigint>;
1195
+
1196
+ /**
1197
+ * Top-level configuration for `createIssuerService`. Everything except
1198
+ * the chain metadata, wallets, auth secret, and `signer` is optional and
1199
+ * falls back to the in-memory dev defaults — that makes the happy path
1200
+ * a single-call wire-up while still letting production issuers plug in
1201
+ * their own ledger, session store, policy engine, and KMS signer.
1202
+ */
1203
+ interface IssuerServiceConfig {
1204
+ chainId: number;
1205
+ /** Address of the deployed PointToken (one per issuer instance). */
1206
+ pointTokenAddress: Address;
1207
+ /** Address of the deployed Relay contract. */
1208
+ relayAddress: Address;
1209
+ /**
1210
+ * Full contract address bundle returned verbatim by `handleConfig` so
1211
+ * the frontend SDK can pick them up.
1212
+ */
1213
+ contracts: ApiConfigResponse["contracts"];
1214
+ /**
1215
+ * Shared `PublicClient` used for on-chain reads, simulation, indexer
1216
+ * polling, and gas-price lookups. Must be pointed at the target chain.
1217
+ */
1218
+ provider: PublicClient;
1219
+ /** Operator wallet — pays gas, receives the operator fee. */
1220
+ operatorWallet: OperatorWalletLike;
1221
+ auth: {
1222
+ jwtSecret: string;
1223
+ /** SIWE-style login-message domain, e.g. `"app.example.com"`. */
1224
+ domain: string;
1225
+ /** Passed straight to `jose` (`"24h"`, `"7d"`, …). Default `"24h"`. */
1226
+ jwtExpiresIn?: string;
1227
+ };
1228
+ /**
1229
+ * Issuer signer. No default — production issuers MUST plug in an
1230
+ * HSM/KMS-backed implementation. For local development use
1231
+ * `PrivateKeySigner` directly.
1232
+ */
1233
+ signer: IIssuerSigner;
1234
+ ledger?: IPointLedger;
1235
+ policy?: IPolicyEngine;
1236
+ sessionStore?: ISessionStore;
1237
+ /**
1238
+ * Fee management config. If omitted the `handleGasFee` endpoint will
1239
+ * throw "not configured" at request time. Pass any subset of fields
1240
+ * to opt in — provider + operatorWallet are inherited from the outer
1241
+ * config automatically.
1242
+ */
1243
+ fee?: Omit<FeeManagerConfig, "provider" | "operatorWallet">;
1244
+ /**
1245
+ * Pool discovery function for `handlePools`. If omitted the endpoint
1246
+ * throws "not configured" at request time.
1247
+ */
1248
+ poolsProvider?: PoolsProvider;
1249
+ indexer?: {
1250
+ fromBlock?: bigint;
1251
+ cursorStore?: IIndexerCursorStore;
1252
+ confirmations?: number;
1253
+ batchSize?: number;
1254
+ pollIntervalMs?: number;
1255
+ /**
1256
+ * If `true`, the factory calls `indexer.start()` before returning.
1257
+ * Default: `false` — the caller decides when to begin polling.
1258
+ */
1259
+ autoStart?: boolean;
1260
+ };
1261
+ relay?: {
1262
+ simulateBeforeSubmit?: boolean;
1263
+ confirmationTimeoutMs?: number;
1264
+ };
1265
+ gateway?: {
1266
+ /** Extra lock TTL buffer beyond consent deadline. Default 60_000 ms. */
1267
+ defaultLockBufferMs?: number;
1268
+ };
1269
+ }
1270
+ interface IssuerService {
1271
+ authService: AuthService;
1272
+ sessionStore: ISessionStore;
1273
+ ledger: IPointLedger;
1274
+ policy: IPolicyEngine;
1275
+ signer: IIssuerSigner;
1276
+ relayService: RelayService;
1277
+ feeManager: FeeManager | undefined;
1278
+ gateway: MintingGateway;
1279
+ indexer: PointIndexer;
1280
+ handlers: IssuerApiHandlers;
1281
+ }
1282
+ /**
1283
+ * Wire a fully-functional issuer service from a single config object.
1284
+ * Returns every constructed collaborator so the caller can also use the
1285
+ * indexer or relay service directly outside the HTTP layer.
1286
+ *
1287
+ * Defaults:
1288
+ * - `ledger` → `MemoryPointLedger`
1289
+ * - `sessionStore` → `MemorySessionStore`
1290
+ * - `policy` → `DefaultPolicyEngine({ ledger })`
1291
+ * - `feeManager` → undefined (handleGasFee throws until configured)
1292
+ * - `poolsProvider` → undefined (handlePools throws until configured)
1293
+ * - `indexer.autoStart` → false
1294
+ *
1295
+ * Throws synchronously if any required field (`signer`, `provider`,
1296
+ * `operatorWallet`, `auth.jwtSecret`, `pointTokenAddress`, …) is missing.
1297
+ */
1298
+ declare function createIssuerService(config: IssuerServiceConfig): IssuerService;
1299
+
1300
+ /** SDK package version — bumped on every release */
1301
+ declare const PAFI_ISSUER_SDK_VERSION = "0.1.0";
1302
+
1303
+ export { type ApiBuildConsentTypedDataRequest, type ApiBuildConsentTypedDataResponse, type ApiClaimAndSwapRequest, type ApiClaimAndSwapResponse, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, DefaultPolicyEngine, type DefaultPolicyEngineOptions, FeeManager, type FeeManagerConfig, type IIndexerCursorStore, type IIssuerSigner, type IPointLedger, type IPolicyEngine, type ISessionStore, InMemoryCursorStore, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerService, type IssuerServiceConfig, type LockedMintRequest, type LoginResult, MemoryPointLedger, MemorySessionStore, type MemorySessionStoreOptions, type MintAndCashOutRequest, type MintAndCashOutResponse, type MintEvent, MintingGateway, type MintingGatewayConfig, MintingGatewayError, type MintingGatewayErrorCode, type MintingStatus, NonceManager, type OperatorWalletLike, PAFI_ISSUER_SDK_VERSION, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, type PoolsProvider, PrivateKeySigner, type PrivateKeySignerOptions, RelayError, type RelayErrorCode, type RelayResult, RelayService, type RelayServiceConfig, type Session, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type SubmitMintAndSwapParams, authenticateRequest, createIssuerService, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider };