@pafi-dev/issuer 0.3.0-beta.9 → 0.5.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/README.md CHANGED
@@ -1,352 +1,323 @@
1
1
  # @pafi-dev/issuer
2
2
 
3
- Issuer backend boilerplate for the PAFI point token system. Wraps
4
- `@pafi-dev/core` with everything a partner needs to run the off-chain side
5
- of the mint-and-cash-out flow: authentication, point ledger, policy
6
- engine, EIP-712 issuer signing, relay submission, mint event indexing,
7
- and framework-agnostic HTTP handlers.
8
-
9
- ## What's new in 0.3.0-alpha.0 — sponsored UserOp flow (v1.4 preview)
10
-
11
- Preview release that wires the SDK into the new **sponsored path**:
12
- user EOAs delegate to a BatchExecutor via EIP-7702, UserOps are
13
- signed by the user and submitted to a Bundler, and gas is paid by
14
- Coinbase Paymaster — proxied through the new PAFI-hosted
15
- `paymaster-proxy` service.
16
-
17
- **What ships in this alpha:**
18
-
19
- - `PafiBackendClient` — HTTP client for `paymaster-proxy` with retry +
20
- backoff on `safeToRetry` errors (opt-in via `retry.maxAttempts`)
21
- - `BalanceAggregator` — merge off-chain + on-chain balance into one number
22
- - `BurnIndexer` — watches `Transfer(user → 0x0)` events, finalizes
23
- pending off-chain credits for the reverse flow
24
- - `IPointLedger.reservePendingCredit()` / `resolveCreditByBurnTx()` —
25
- additive ledger methods for burn→credit flow
26
- - `ApiConfigResponse.pafiWebUrl` + `contracts.pointTokens[]` — new
27
- fields surfaced by `handleConfig()`
28
- - `FeeManager` slimmed: no more `operatorWallet` /
29
- `usdtAddress` / `nativeWrappedAddress`; rebalancing gone (operator
30
- doesn't hold ETH anymore); `quoteNativeToFee` replaces
31
- `quoteNativeToUsdt` so fee currency is caller-chosen (PT for
32
- mint/burn, USDT for swap/perp_deposit)
33
-
34
- **What's still blocked (comes in 0.3.0-beta / stable):**
35
-
36
- - `processMint()` / `processBurn()` — blocked on SC Relayer v2 ABI
37
- (blocker B1). Current `processMintAndCashOut()` is `@deprecated`
38
- but still works
39
- - `PTRedeemHandler` + `TopUpRedemptionHandler` — blocked on B1 +
40
- `paymaster-proxy` staging env being live
41
- - Deletion of legacy `claimAndSwap` flow — kept for v0.2.x consumers
42
- until 2.0
43
-
44
- **Install the alpha:**
3
+ [![npm](https://img.shields.io/npm/v/@pafi-dev/issuer)](https://www.npmjs.com/package/@pafi-dev/issuer)
4
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
45
5
 
46
- ```bash
47
- pnpm add @pafi-dev/issuer@0.3.0-alpha.0 @pafi-dev/core@0.3.0-alpha.0
48
- ```
6
+ Issuer backend SDK for the PAFI point token system. Wraps `@pafi-dev/core` with everything
7
+ a partner needs to run the off-chain side of the cashout flow: SIWE authentication, point
8
+ ledger, policy engine, EIP-712 issuer signing, relay submission, and mint/burn event indexing.
49
9
 
50
- Pin the exact version, or use the `@alpha` dist-tag.
10
+ > **Server-only.** This package pulls in `jose` and `node:crypto`. Do not bundle into a browser app.
51
11
 
52
- ## What's new in 0.2.0 — multi-token support
12
+ ---
53
13
 
54
- An issuer backend can now support more than one PointToken in a single
55
- service instance. Pass `pointTokenAddresses: Address[]` in
56
- `IssuerServiceConfig`; the factory creates one `PointIndexer` per token
57
- and handlers validate the requested `pointTokenAddress` against the
58
- configured set.
14
+ ## Requirements
59
15
 
60
- `IPointLedger` methods take an optional `tokenAddress?: Address` param.
61
- Single-token issuers can ignore it — legacy 0.1.x code keeps working.
62
- Multi-token issuers must persist balances keyed by `(user, token)`.
16
+ - Node.js >= 18
17
+ - TypeScript >= 5.0
18
+ - `viem` ^2.0.0 and `@pafi-dev/core` ^0.4.0 (peer dependencies)
63
19
 
64
- Legacy `pointTokenAddress: Address` still works as a shorthand for a
65
- one-element list. No code change is required if you only have one token.
20
+ ---
66
21
 
67
- ## Install
22
+ ## Installation
68
23
 
69
24
  ```bash
25
+ npm install @pafi-dev/issuer @pafi-dev/core viem
26
+ # or
70
27
  pnpm add @pafi-dev/issuer @pafi-dev/core viem
71
28
  ```
72
29
 
73
- Peer deps: `viem ^2.0`, `@pafi-dev/core` (workspace), `jose ^5` (transitive).
30
+ ---
31
+
32
+ ## Modules
33
+
34
+ | Module | What it provides |
35
+ |---|---|
36
+ | `relay/` | `RelayService` — build unsigned UserOps for mint + burn |
37
+ | `auth/` | `AuthService` — SIWE verification + JWT issuance; `authenticateRequest` middleware |
38
+ | `ledger/` | `IPointLedger` interface — implement against your own database |
39
+ | `policy/` | `IPolicyEngine`, `DefaultPolicyEngine` — off-chain balance gate |
40
+ | `indexer/` | `PointIndexer` (mint events) + `BurnIndexer` (burn events) |
41
+ | `balance/` | `BalanceAggregator` — merge off-chain ledger + on-chain `balanceOf` |
42
+ | `api/` | `IssuerApiHandlers` — framework-agnostic HTTP handlers |
43
+ | `pafi-backend/` | `PafiBackendClient` — HTTP client for PAFI paymaster backend |
44
+
45
+ ---
46
+
47
+ ## What you must bring
74
48
 
75
- ## At a glance
49
+ The SDK deliberately does not ship production implementations for:
50
+
51
+ | What | Why you own it |
52
+ |---|---|
53
+ | **`IPointLedger`** | Your database, your schema, your row-level locking strategy |
54
+ | **`ISessionStore`** | Must be shared across pods — use Redis or Postgres |
55
+ | **Signing wallet** | Private key must never touch server memory — use KMS |
56
+
57
+ ---
58
+
59
+ ## Quick start
76
60
 
77
61
  ```ts
78
- import { createIssuerService, PrivateKeySigner } from "@pafi-dev/issuer";
62
+ import { createIssuerService } from "@pafi-dev/issuer";
63
+ import { getContractAddresses } from "@pafi-dev/core";
79
64
  import { createPublicClient, createWalletClient, http } from "viem";
80
- import { privateKeyToAccount } from "viem/accounts";
81
65
  import { base } from "viem/chains";
66
+ import { privateKeyToAccount } from "viem/accounts";
67
+
68
+ const addrs = getContractAddresses(8453);
82
69
 
83
- const provider = createPublicClient({ chain: base, transport: http(RPC_URL) });
84
- const operatorWallet = createWalletClient({
85
- account: privateKeyToAccount(OPERATOR_PK),
70
+ // Production: replace with a KMS-backed WalletClient (see "Production signing" below)
71
+ const issuerSignerWallet = createWalletClient({
72
+ account: privateKeyToAccount(process.env.MINTER_PRIVATE_KEY as `0x${string}`),
86
73
  chain: base,
87
- transport: http(RPC_URL),
74
+ transport: http(process.env.RPC_URL),
88
75
  });
89
76
 
90
77
  const service = createIssuerService({
91
78
  chainId: 8453,
92
- pointTokenAddress: "0x5F...",
93
- relayAddress: "0x92...",
94
- contracts: { pointToken, relay, issuerRegistry, usdt },
95
- provider,
96
- operatorWallet,
79
+ pointTokenAddresses: [addrs.pointToken],
80
+ contracts: {
81
+ relay: "0x92327F5c9383796Dd46D43E0995cc938038A98c4",
82
+ issuerRegistry: addrs.issuerRegistry,
83
+ usdt: addrs.usdt,
84
+ batchExecutor: addrs.batchExecutor,
85
+ },
86
+ provider: createPublicClient({ chain: base, transport: http(process.env.RPC_URL) }),
97
87
  auth: {
98
88
  jwtSecret: process.env.JWT_SECRET!,
99
89
  domain: "app.example.com",
100
90
  },
101
- // ⚠️ NOT for production — replace with a KMS-backed IIssuerSigner
102
- signer: new PrivateKeySigner({ privateKey: ISSUER_PK, chain: base }),
91
+ ledger: new YourPostgresLedger(), // implements IPointLedger
92
+ sessionStore: new YourRedisSessionStore(), // implements ISessionStore
93
+ claim: {
94
+ issuerSignerWallet,
95
+ batchExecutorAddress: addrs.batchExecutor,
96
+ },
103
97
  });
104
98
 
105
- // Wire `service.handlers` into Express / Fastify / Hono — see
106
- // examples/express-issuer/ for a full runnable server.
99
+ // Mount into your framework (Express, Fastify, NestJS, Hono...)
100
+ app.get("/auth/nonce", () => service.handlers.handleGetNonce());
101
+ app.post("/auth/login", (req) => service.handlers.handleLogin(req.body));
102
+ app.get("/user", (req) => service.handlers.handleUser(req.user, req.query));
103
+ app.post("/claim", (req) => service.handlers.handleClaim(req.user, req.body));
107
104
  ```
108
105
 
109
- ## What you get
106
+ ---
110
107
 
111
- | Component | Default | Replace for production? |
112
- | ----------------------- | --------------------- | ------------------------------------- |
113
- | `IPointLedger` | `MemoryPointLedger` | ✅ Postgres / Prisma |
114
- | `ISessionStore` | `MemorySessionStore` | ✅ Redis |
115
- | `IPolicyEngine` | `DefaultPolicyEngine` | ➕ extend for KYC / caps / claim budget |
116
- | `IIssuerSigner` | `PrivateKeySigner` | 🚨 **MUST replace** — KMS / HSM / MPC |
117
- | `AuthService` | ✅ built-in | — |
118
- | `RelayService` | ✅ built-in | — |
119
- | `FeeManager` | ✅ built-in (opt-in) | injection-based — no DEX lock-in |
120
- | `MintingGateway` | ✅ built-in | — |
121
- | `PointIndexer` | ✅ built-in (polling) | — |
122
- | `IssuerApiHandlers` | ✅ built-in | — |
108
+ ## Implementing IPointLedger
123
109
 
124
- See [`examples/adapters/`](./examples/adapters) for reference
125
- implementations of the three interfaces you must replace.
110
+ Every issuer provides their own database-backed implementation:
126
111
 
127
- ## The minting flow
112
+ ```ts
113
+ import type { IPointLedger, LockedMintRequest, MintingStatus } from "@pafi-dev/issuer";
114
+ import type { Address, Hex } from "viem";
115
+
116
+ export class PostgresPointLedger implements IPointLedger {
117
+ async getBalance(user: Address, tokenAddress?: Address): Promise<bigint> { ... }
118
+
119
+ // Must use a row-level lock (SELECT ... FOR UPDATE) to prevent
120
+ // concurrent pods from reading the same available balance and double-spending.
121
+ async lockForMinting(user: Address, amount: bigint, durationMs: number, tokenAddress?: Address): Promise<string> { ... }
122
+
123
+ async releaseLock(lockId: string): Promise<void> { ... }
124
+ async deductBalance(user: Address, amount: bigint, txHash: Hex, tokenAddress?: Address): Promise<void> { ... }
125
+ async creditBalance(user: Address, amount: bigint, reason: string, tokenAddress?: Address): Promise<void> { ... }
126
+ async getLockedRequests(user: Address, tokenAddress?: Address): Promise<LockedMintRequest[]> { ... }
127
+ async updateMintStatus(lockId: string, status: MintingStatus, txHash?: Hex): Promise<void> { ... }
128
+
129
+ // Burn flow: reserve → resolve when burn tx confirmed
130
+ async reservePendingCredit(user: Address, amount: bigint, durationMs: number, tokenAddress?: Address): Promise<string> { ... }
131
+ async resolveCreditByBurnTx(lockId: string, txHash: Hex): Promise<void> { ... }
132
+ }
133
+ ```
128
134
 
129
- The SDK's central orchestrator is `MintingGateway.processMintAndCashOut()`,
130
- which runs the full 11-step flow documented in
131
- [`docs/PAFI_ISSUER_SDK_SPEC.md`](../../../pafi-backend/docs/PAFI_ISSUER_SDK_SPEC.md):
135
+ ---
132
136
 
133
- ```
134
- 1. Validate request fields (cheap rejects)
135
- 2. Verify ReceiverConsent signature via @pafi-dev/core
136
- 3. Check off-chain balance via ledger
137
- 4. Check locked requests via ledger
138
- 5. Run policy engine (balance + cap + issuer rules)
139
- 6. Lock the requested amount in the ledger
140
- 7. Sign MintRequest as the issuer (via IIssuerSigner)
141
- 8. Build Relay calldata (MintParams + SwapParams)
142
- 9. Submit via RelayService (simulate → write → receipt)
143
- 10. Return { txHash, lockId, block/gas }
144
- 11. PointIndexer finalizes the ledger on the Mint event (out of band)
145
- ```
137
+ ## Production signing — KMS
146
138
 
147
- **The gateway never deducts the balance directly.** Deduction is
148
- driven by the `PointIndexer` when it observes the on-chain Mint event.
149
- That makes the system crash-safe: if the gateway dies between
150
- broadcast and receipt, the indexer still finalizes the ledger the next
151
- time it polls.
139
+ In production the `issuerSignerWallet` must be backed by a hardware key.
140
+ The SDK accepts any viem `WalletClient` wire it to AWS KMS via a custom account:
141
+
142
+ ```ts
143
+ import { KMSClient } from "@aws-sdk/client-kms";
144
+ import { createWalletClient, http } from "viem";
145
+ import { base } from "viem/chains";
146
+
147
+ // Custom viem account that delegates signTypedData to KMS.
148
+ // Full DER→(r,s) conversion + v-byte recovery omitted for brevity.
149
+ const kmsAccount = toAccount({
150
+ address: process.env.KMS_MINTER_ADDRESS as `0x${string}`,
151
+ async signTypedData(typedData) {
152
+ const digest = hashTypedData(typedData);
153
+ const { Signature } = await kms.send(new SignCommand({
154
+ KeyId: process.env.KMS_MINTER_KEY_ID!,
155
+ Message: digest,
156
+ MessageType: "DIGEST",
157
+ SigningAlgorithm: "ECDSA_SHA_256",
158
+ }));
159
+ return derToViem(Signature!); // convert DER → 65-byte viem hex signature
160
+ },
161
+ });
152
162
 
153
- ### Lock release semantics
163
+ const issuerSignerWallet = createWalletClient({
164
+ account: kmsAccount,
165
+ chain: base,
166
+ transport: http(process.env.RPC_URL),
167
+ });
168
+ ```
154
169
 
155
- Every `MintingGatewayError` carries a `safeToRetry: boolean` flag that
156
- tells the API layer whether the underlying ledger lock was released:
170
+ ---
157
171
 
158
- | Error code | Lock released? | `safeToRetry` |
159
- | --------------------------- | -------------- | ------------- |
160
- | `INVALID_REQUEST` | never locked | `true` |
161
- | `INVALID_CONSENT_SIGNATURE` | never locked | `true` |
162
- | `CONSENT_EXPIRED` | never locked | `true` |
163
- | `POLICY_REJECTED` | never locked | `true` |
164
- | `INSUFFICIENT_BALANCE` | never locked | `true` |
165
- | `SIGNER_FAILED` | ✅ released | `true` |
166
- | `RELAY_SIMULATION_FAILED` | ✅ released | `true` |
167
- | `RELAY_SUBMIT_FAILED` | ✅ released | `true` |
168
- | `RELAY_REVERTED` | 🔒 kept | `false` |
169
- | `RELAY_TIMEOUT` | 🔒 kept | `false` |
172
+ ## Event indexers
170
173
 
171
- `RELAY_REVERTED` and `RELAY_TIMEOUT` keep the lock because the tx may
172
- still be in the mempool or already mined — releasing would enable a
173
- double-spend on retry. Your API should surface `409 Conflict` in these
174
- cases and require manual reconciliation.
174
+ Indexers sync on-chain mint/burn events back to the off-chain ledger.
175
175
 
176
- ## Authentication
176
+ ```ts
177
+ import { PointIndexer, BurnIndexer } from "@pafi-dev/issuer";
178
+ import { getContractAddresses } from "@pafi-dev/core";
179
+
180
+ const addrs = getContractAddresses(8453);
181
+
182
+ // Mint indexer: Transfer(0x0 → user) → deducts off-chain locked balance
183
+ const mintIndexer = new PointIndexer({
184
+ provider: publicClient,
185
+ pointTokenAddress: addrs.pointToken,
186
+ ledger,
187
+ cursorStore, // persists last-processed block; use Postgres or Redis
188
+ fromBlock: 28_000_000n,
189
+ confirmations: 2,
190
+ pollIntervalMs: 3_000,
191
+ });
177
192
 
178
- Wallet-based login uses EIP-4361 (Sign-In with Ethereum):
193
+ // Burn indexer: Transfer(user 0x0) → resolves pending off-chain credit
194
+ const burnIndexer = new BurnIndexer({
195
+ provider: publicClient,
196
+ pointTokenAddress: addrs.pointToken,
197
+ ledger,
198
+ cursorStore,
199
+ matchLockId: async (evt) => {
200
+ // Return the lockId from reservePendingCredit() that matches this burn.
201
+ // Typically a DB lookup by (from, amount, status=PENDING).
202
+ return db.findPendingCreditLockId(evt.from, evt.amount);
203
+ },
204
+ });
179
205
 
206
+ mintIndexer.start();
207
+ burnIndexer.start();
180
208
  ```
181
- Frontend Issuer backend
182
- ──────── ──────────────
183
- 1. GET /auth/nonce → authService.getNonce()
184
- ← { nonce }
185
- 2. Build EIP-4361 message with
186
- nonce (via PafiSDK or
187
- @pafi-dev/core createLoginMessage)
188
- 3. Sign with wallet
189
- 4. POST /auth/login → authService.login(message, sig)
190
- { message, signature } → parse → verify → consume nonce
191
- → create session → issue JWT
192
- ← { token, userAddress, expiresAt }
193
- 5. Authorization: Bearer <jwt> → authenticateRequest()
194
- → verify JWT + session
195
- → pass { userAddress, chainId } to handlers
196
- ```
197
-
198
- Key property: a failed signature does **not** burn the nonce. Users can
199
- retry with the correct wallet on the same nonce without hitting the
200
- backend again.
201
209
 
202
- ## HTTP endpoints
210
+ > **Indexers must be singletons.** Run exactly one instance per token per deployment.
211
+ > Use a Kubernetes `Deployment` with `replicas: 1`.
203
212
 
204
- The handlers are framework-agnostic — `async` functions that take plain
205
- inputs and return plain outputs. Wrap them in Express / Fastify / Hono /
206
- whatever you use. See `examples/express-issuer/server.ts` for the full
207
- wiring.
213
+ ---
208
214
 
209
- | Method | Path | Auth | Handler |
210
- | ------ | ---------------- | ---- | -------------------- |
211
- | GET | /auth/nonce | no | `handleGetNonce` |
212
- | POST | /auth/login | no | `handleLogin` |
213
- | GET | /config | no | `handleConfig` |
214
- | GET | /gas-fee | no | `handleGasFee` |
215
- | POST | /auth/logout | yes | `handleLogout` |
216
- | GET | /pools | yes | `handlePools` |
217
- | GET | /user | yes | `handleUser` |
218
- | POST | /claim-and-swap | yes | `handleClaimAndSwap` |
215
+ ## PafiBackendClient
219
216
 
220
- ## Security notes
217
+ HTTP client for requesting paymaster sponsorship from the PAFI backend:
221
218
 
222
- - 🚨 **Never deploy `PrivateKeySigner` to production.** The private key lives in process memory and is trivially extractable from a compromised host. Use `examples/adapters/kms-signer.ts` as a starting point for a KMS/HSM integration.
223
- - 🔑 **`jwtSecret` must be at least 16 characters (32+ recommended) and rotated periodically.** Sessions survive a rotation because the JWT lookup cross-checks the session store — but existing tokens signed with the old secret will fail `verifyToken`, forcing users to re-login.
224
- - 🕐 **The default `MemorySessionStore` does not survive restarts.** Nonces and sessions are both lost. Use `examples/adapters/redis-session-store.ts` for anything past local development.
225
- - 🔒 **The `MintingGatewayError.safeToRetry` flag is load-bearing.** Do not ignore it in your API layer — surfacing `RELAY_REVERTED` as a retryable 400 will cause double-spend the moment a tx lands after the first response.
219
+ ```ts
220
+ import { PafiBackendClient } from "@pafi-dev/issuer";
226
221
 
227
- ## Tests
222
+ const pafiClient = new PafiBackendClient({
223
+ url: "https://api-dev.pacificfinance.org/api/sponsor",
224
+ issuerId: "gg56",
225
+ apiKey: process.env.PAFI_API_KEY!,
226
+ retry: { maxAttempts: 3, maxRetryAfterMs: 5_000 },
227
+ });
228
228
 
229
- The package ships with 142 unit tests covering every collaborator + the
230
- full gateway flow through mocked providers. Run them with:
229
+ const sponsorship = await pafiClient.requestSponsorship({
230
+ chainId: 8453,
231
+ scenario: "mint",
232
+ userOp: {
233
+ sender: userAddress,
234
+ callData: userOp.callData,
235
+ // ...gas fields
236
+ },
237
+ target: {
238
+ contractAddress: addrs.pointToken,
239
+ functionSelector: "0x...",
240
+ },
241
+ });
231
242
 
232
- ```bash
233
- pnpm --filter @pafi-dev/issuer test
243
+ // sponsorship.paymaster
244
+ // sponsorship.paymasterData
245
+ // sponsorship.paymasterVerificationGasLimit
246
+ // sponsorship.paymasterPostOpGasLimit
247
+ // sponsorship.expiresAt
234
248
  ```
235
249
 
236
- No network or on-chain state is required — all tests are hermetic.
250
+ ---
251
+
252
+ ## Error handling
253
+
254
+ ### AuthError
237
255
 
238
- ## Exports
256
+ Thrown by `AuthService` and `authenticateRequest` middleware:
239
257
 
240
258
  ```ts
241
- import {
242
- // Factory (recommended entry point)
243
- createIssuerService,
244
- type IssuerService,
245
- type IssuerServiceConfig,
246
-
247
- // Ledger
248
- MemoryPointLedger,
249
- type IPointLedger,
250
- type LockedMintRequest,
251
- type MintingStatus,
252
-
253
- // Policy
254
- DefaultPolicyEngine,
255
- type IPolicyEngine,
256
- type PolicyDecision,
257
- type PolicyEvalRequest,
258
-
259
- // Signer
260
- PrivateKeySigner, // DEV ONLY
261
- type IIssuerSigner,
262
-
263
- // Auth
264
- AuthService,
265
- AuthError,
266
- type AuthErrorCode,
267
- type AuthContext,
268
- MemorySessionStore,
269
- type ISessionStore,
270
- type Session,
271
- NonceManager,
272
- authenticateRequest,
273
-
274
- // Relay + fees
275
- RelayService,
276
- RelayError,
277
- type RelayErrorCode,
278
- type SubmitMintAndSwapParams,
279
- type RelayResult,
280
- type OperatorWalletLike,
281
- FeeManager,
282
- type FeeManagerConfig,
283
-
284
- // Gateway
285
- MintingGateway,
286
- MintingGatewayError,
287
- type MintingGatewayErrorCode,
288
- type MintAndCashOutRequest,
289
- type MintAndCashOutResponse,
290
- encodeExtData,
291
-
292
- // Indexer
293
- PointIndexer,
294
- type PointIndexerConfig,
295
- type MintEvent,
296
- type IIndexerCursorStore,
297
- InMemoryCursorStore,
298
-
299
- // API handlers + HTTP contract (source of truth — FE imports these type-only)
300
- IssuerApiHandlers,
301
- type IssuerApiHandlersConfig,
302
- type ApiConfigResponse,
303
- type ApiNonceResponse,
304
- type ApiLoginRequest,
305
- type ApiLoginResponse,
306
- type ApiGasFeeResponse,
307
- type ApiPoolsRequest,
308
- type ApiPoolsResponse,
309
- type ApiUserRequest,
310
- type ApiUserResponse,
311
- type ApiClaimAndSwapRequest,
312
- type ApiClaimAndSwapResponse,
313
- type PoolsProvider,
314
- } from "@pafi-dev/issuer";
259
+ import { AuthError, type AuthErrorCode } from "@pafi-dev/issuer";
260
+
261
+ // AuthErrorCode:
262
+ // "INVALID_MESSAGE" | "INVALID_SIGNATURE" | "NONCE_MISMATCH"
263
+ // "NONCE_EXPIRED" | "TOKEN_EXPIRED" | "TOKEN_INVALID"
264
+ // "SESSION_NOT_FOUND" | "DOMAIN_MISMATCH" | "CHAIN_MISMATCH"
265
+
266
+ try {
267
+ const user = await authService.login(message, signature);
268
+ } catch (err) {
269
+ if (err instanceof AuthError) {
270
+ console.log(err.code); // AuthErrorCode
271
+ }
272
+ }
315
273
  ```
316
274
 
317
- ### HTTP contract ownership
275
+ ### PafiBackendError
318
276
 
319
- `@pafi-dev/issuer` is the **single source of truth** for the HTTP API contract
320
- between the issuer backend and any frontend / mobile / SDK consumer. The
321
- request/response types above are defined once here, then imported
322
- **type-only** by frontend code so browser bundles never pull in server
323
- code (`jose`, `node:crypto`, indexer polling loops, etc):
277
+ Thrown by `PafiBackendClient`:
324
278
 
325
279
  ```ts
326
- // Frontend type-only import, erased at build time
327
- import type {
328
- ApiLoginRequest,
329
- ApiClaimAndSwapRequest,
330
- ApiUserResponse,
331
- } from "@pafi-dev/issuer";
332
-
333
- // Build your own fetch call with full type safety
334
- const res = await fetch(`${API_URL}/claim-and-swap`, {
335
- method: "POST",
336
- headers: {
337
- "Content-Type": "application/json",
338
- Authorization: `Bearer ${token}`,
339
- },
340
- body: JSON.stringify(request satisfies ApiClaimAndSwapRequest),
341
- });
342
- const body = (await res.json()) as ApiClaimAndSwapResponse;
280
+ import { PafiBackendError, type PafiBackendErrorCode } from "@pafi-dev/issuer";
281
+
282
+ // PafiBackendErrorCode:
283
+ // "RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_EXCEEDED_DAILY" | "RATE_LIMIT_EXCEEDED_PER_USER"
284
+ // "RATE_LIMITER_UNAVAILABLE"
285
+ // "INTENT_REJECTED" | "MINT_CAP_EXCEEDED" | "ISSUER_INACTIVE"
286
+ // "ISSUER_UNAUTHORIZED" | "USER_UNAUTHORIZED"
287
+ // "PAYMASTER_UNAVAILABLE" | "TARGET_NOT_ALLOWLISTED"
288
+ // "BAD_REQUEST" | "INTERNAL_ERROR" | "TIMEOUT" | "NETWORK_ERROR"
289
+
290
+ try {
291
+ await pafiClient.requestSponsorship(request);
292
+ } catch (err) {
293
+ if (err instanceof PafiBackendError) {
294
+ if (err.code === "RATE_LIMIT_EXCEEDED") {
295
+ // err.retryAfter — seconds to wait before retrying
296
+ }
297
+ }
298
+ }
343
299
  ```
344
300
 
345
- **`@pafi-dev/core` deliberately does NOT ship an HTTP client** — it handles
346
- only cryptography (EIP-712 / EIP-4361), contract reads, and V4 quoting.
347
- That's what keeps the frontend bundle small and keeps the protocol
348
- contract in exactly one place.
301
+ ---
302
+
303
+ ## Changelog
304
+
305
+ ### 0.4.0
306
+ - `PafiBackendClient` added — HTTP client for PAFI paymaster backend with retry logic and bigint serialization
307
+ - `SponsorAuth` support — issuer signs EIP-712 authorization for FE to request paymaster sponsorship
308
+ - `PAYMASTER_UNAVAILABLE`, `TARGET_NOT_ALLOWLISTED` added to `PafiBackendErrorCode`
309
+
310
+ ### 0.3.0-beta.10
311
+ - `MemoryPointLedger` removed from public exports — each issuer must implement `IPointLedger`
312
+ - `PrivateKeySigner` / `IIssuerSigner` removed — SDK accepts viem `WalletClient` directly
313
+ - `ledger` is now a required field in `IssuerServiceConfig`
314
+ - `handleClaim` added to `IssuerApiHandlers`
315
+
316
+ ### 0.3.0-beta.9
317
+ - `PTRedeemHandler` enforces on-chain balance check before signing `BurnRequest`
349
318
 
350
- ## License
319
+ ### 0.3.0-beta.8
320
+ - `BalanceAggregator`, `BurnIndexer`, `PTRedeemHandler`, `TopUpRedemptionHandler`
351
321
 
352
- Apache-2.0
322
+ ### 0.3.0-alpha.0
323
+ - Initial `RelayService`, `PointIndexer`, `AuthService`, `IssuerApiHandlers`