@pafi-dev/issuer 0.4.0 → 0.5.1

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,67 +1,73 @@
1
1
  # @pafi-dev/issuer
2
2
 
3
- Issuer backend SDK for the PAFI point token system. Wraps `@pafi-dev/core`
4
- with everything a partner needs to run the off-chain side of the cashout
5
- flow: SIWE authentication, point ledger, policy engine, EIP-712 issuer
6
- signing, relay submission, and mint/burn event indexing.
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)
7
5
 
8
- **Version:** `0.3.0-beta.10`
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.
9
9
 
10
- > This package runs on the **issuer backend server only** (Node.js). Do not
11
- > import into browser bundles — it pulls in `jose`, `node:crypto`, and
12
- > server-side dependencies.
10
+ > **Server-only.** This package pulls in `jose` and `node:crypto`. Do not bundle into a browser app.
11
+
12
+ ---
13
+
14
+ ## Requirements
15
+
16
+ - Node.js >= 18
17
+ - TypeScript >= 5.0
18
+ - `viem` ^2.0.0 and `@pafi-dev/core` ^0.4.0 (peer dependencies)
13
19
 
14
20
  ---
15
21
 
16
22
  ## Installation
17
23
 
18
24
  ```bash
25
+ npm install @pafi-dev/issuer @pafi-dev/core viem
26
+ # or
19
27
  pnpm add @pafi-dev/issuer @pafi-dev/core viem
20
28
  ```
21
29
 
22
30
  ---
23
31
 
24
- ## What this package provides
32
+ ## Modules
25
33
 
26
- | Module | What it does |
27
- |--------|-------------|
28
- | `relay/` | `RelayService.prepareMint()` / `prepareBurn()` — build unsigned UserOps |
29
- | `indexer/` | `PointIndexer` (Mint events) + `BurnIndexer` (Transfer→0x0 events) |
30
- | `auth/` | `AuthService` (SIWE verify + JWT issue) + `authenticateRequest` middleware |
34
+ | Module | What it provides |
35
+ |---|---|
36
+ | `relay/` | `RelayService` — build unsigned UserOps for mint + burn |
37
+ | `auth/` | `AuthService` SIWE verification + JWT issuance; `authenticateRequest` middleware |
31
38
  | `ledger/` | `IPointLedger` interface — implement against your own database |
32
- | `policy/` | `DefaultPolicyEngine` + `IPolicyEngine` interface |
33
- | `pools/` | `PoolsProvider` fetch V4 pool list from PAFI subgraph |
39
+ | `policy/` | `IPolicyEngine`, `DefaultPolicyEngine` off-chain balance gate |
40
+ | `indexer/` | `PointIndexer` (mint events) + `BurnIndexer` (burn events) |
34
41
  | `balance/` | `BalanceAggregator` — merge off-chain ledger + on-chain `balanceOf` |
35
- | `api/` | `IssuerApiHandlers`, `PTRedeemHandler`, `TopUpRedemptionHandler` |
42
+ | `api/` | `IssuerApiHandlers` framework-agnostic HTTP handlers |
43
+ | `pafi-backend/` | `PafiBackendClient` — HTTP client for PAFI paymaster backend |
36
44
 
37
45
  ---
38
46
 
39
47
  ## What you must bring
40
48
 
41
- The SDK deliberately does not ship production implementations for these
42
- three concerns — every issuer's infrastructure is different:
49
+ The SDK deliberately does not ship production implementations for:
43
50
 
44
51
  | What | Why you own it |
45
- |------|---------------|
46
- | **`IPointLedger`** | Your database, your schema, your locking strategy |
47
- | **`ISessionStore`** | Must be shared across pods — Redis, Postgres, etc. |
52
+ |---|---|
53
+ | **`IPointLedger`** | Your database, your schema, your row-level locking strategy |
54
+ | **`ISessionStore`** | Must be shared across pods — use Redis or Postgres |
48
55
  | **Signing wallet** | Private key must never touch server memory — use KMS |
49
56
 
50
- See [`examples/adapters/`](./examples/adapters/) for reference implementations
51
- of all three (Postgres ledger, Redis session store, AWS KMS wallet).
52
-
53
57
  ---
54
58
 
55
59
  ## Quick start
56
60
 
57
61
  ```ts
58
62
  import { createIssuerService } from "@pafi-dev/issuer";
63
+ import { getContractAddresses } from "@pafi-dev/core";
59
64
  import { createPublicClient, createWalletClient, http } from "viem";
60
65
  import { base } from "viem/chains";
61
-
62
- // --- Production: replace with KMS-backed wallet (see examples/adapters/kms-signer.ts)
63
- // --- Dev only: raw private key
64
66
  import { privateKeyToAccount } from "viem/accounts";
67
+
68
+ const addrs = getContractAddresses(8453);
69
+
70
+ // Production: replace with a KMS-backed WalletClient (see "Production signing" below)
65
71
  const issuerSignerWallet = createWalletClient({
66
72
  account: privateKeyToAccount(process.env.MINTER_PRIVATE_KEY as `0x${string}`),
67
73
  chain: base,
@@ -70,27 +76,27 @@ const issuerSignerWallet = createWalletClient({
70
76
 
71
77
  const service = createIssuerService({
72
78
  chainId: 8453,
73
- pointTokenAddresses: ["0x<PointToken>"],
79
+ pointTokenAddresses: [addrs.pointToken],
74
80
  contracts: {
75
- relay: "0x<Relay>",
76
- issuerRegistry: "0x<IssuerRegistry>",
77
- usdt: "0x<USDT>",
78
- batchExecutor: "0x<BatchExecutor>",
81
+ relay: "0x92327F5c9383796Dd46D43E0995cc938038A98c4",
82
+ issuerRegistry: addrs.issuerRegistry,
83
+ usdt: addrs.usdt,
84
+ batchExecutor: addrs.batchExecutor,
79
85
  },
80
86
  provider: createPublicClient({ chain: base, transport: http(process.env.RPC_URL) }),
81
87
  auth: {
82
88
  jwtSecret: process.env.JWT_SECRET!,
83
89
  domain: "app.example.com",
84
90
  },
85
- ledger: new YourPostgresLedger(), // implements IPointLedger
91
+ ledger: new YourPostgresLedger(), // implements IPointLedger
86
92
  sessionStore: new YourRedisSessionStore(), // implements ISessionStore
87
93
  claim: {
88
94
  issuerSignerWallet,
89
- batchExecutorAddress: "0x<BatchExecutor>",
95
+ batchExecutorAddress: addrs.batchExecutor,
90
96
  },
91
97
  });
92
98
 
93
- // Mount handlers into your framework (Express, Fastify, NestJS, Hono...)
99
+ // Mount into your framework (Express, Fastify, NestJS, Hono...)
94
100
  app.get("/auth/nonce", () => service.handlers.handleGetNonce());
95
101
  app.post("/auth/login", (req) => service.handlers.handleLogin(req.body));
96
102
  app.get("/user", (req) => service.handlers.handleUser(req.user, req.query));
@@ -101,124 +107,217 @@ app.post("/claim", (req) => service.handlers.handleClaim(req.user, req.body
101
107
 
102
108
  ## Implementing IPointLedger
103
109
 
110
+ Every issuer provides their own database-backed implementation:
111
+
104
112
  ```ts
105
- import type { IPointLedger } from "@pafi-dev/issuer";
113
+ import type { IPointLedger, LockedMintRequest, MintingStatus } from "@pafi-dev/issuer";
106
114
  import type { Address, Hex } from "viem";
107
115
 
108
116
  export class PostgresPointLedger implements IPointLedger {
109
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.
110
121
  async lockForMinting(user: Address, amount: bigint, durationMs: number, tokenAddress?: Address): Promise<string> { ... }
122
+
111
123
  async releaseLock(lockId: string): Promise<void> { ... }
112
124
  async deductBalance(user: Address, amount: bigint, txHash: Hex, tokenAddress?: Address): Promise<void> { ... }
113
125
  async creditBalance(user: Address, amount: bigint, reason: string, tokenAddress?: Address): Promise<void> { ... }
114
126
  async getLockedRequests(user: Address, tokenAddress?: Address): Promise<LockedMintRequest[]> { ... }
115
127
  async updateMintStatus(lockId: string, status: MintingStatus, txHash?: Hex): Promise<void> { ... }
116
- // v1.4 reverse flow (burn → off-chain credit):
128
+
129
+ // Burn flow: reserve → resolve when burn tx confirmed
117
130
  async reservePendingCredit(user: Address, amount: bigint, durationMs: number, tokenAddress?: Address): Promise<string> { ... }
118
131
  async resolveCreditByBurnTx(lockId: string, txHash: Hex): Promise<void> { ... }
119
132
  }
120
133
  ```
121
134
 
122
- Critical: `lockForMinting` must use a **row-level lock** (e.g. `SELECT ... FOR UPDATE`)
123
- so concurrent requests from multiple pods cannot read the same available balance and
124
- double-spend.
125
-
126
- See `examples/adapters/postgres-ledger.ts` for a full Prisma implementation.
127
-
128
135
  ---
129
136
 
130
137
  ## Production signing — KMS
131
138
 
132
- The `claim.issuerSignerWallet` must be backed by a hardware key in production.
133
- The SDK accepts any viem `WalletClient` — wire it to AWS KMS by giving viem
134
- a custom account:
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:
135
141
 
136
142
  ```ts
137
- import { createKmsWalletClient } from "./adapters/kms-signer";
138
143
  import { KMSClient } from "@aws-sdk/client-kms";
144
+ import { createWalletClient, http } from "viem";
145
+ import { base } from "viem/chains";
139
146
 
140
- const kms = new KMSClient({ region: "us-east-1" });
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
+ });
141
162
 
142
- const issuerSignerWallet = createKmsWalletClient({
143
- kms,
144
- keyId: process.env.KMS_MINTER_KEY_ID!,
145
- address: process.env.KMS_MINTER_ADDRESS! as `0x${string}`,
163
+ const issuerSignerWallet = createWalletClient({
164
+ account: kmsAccount,
146
165
  chain: base,
166
+ transport: http(process.env.RPC_URL),
147
167
  });
148
168
  ```
149
169
 
150
- See `examples/adapters/kms-signer.ts` for the full implementation including
151
- DER→(r,s) conversion and v-byte recovery.
152
-
153
170
  ---
154
171
 
155
- ## Indexers
172
+ ## Event indexers
173
+
174
+ Indexers sync on-chain mint/burn events back to the off-chain ledger.
156
175
 
157
176
  ```ts
158
177
  import { PointIndexer, BurnIndexer } from "@pafi-dev/issuer";
178
+ import { getContractAddresses } from "@pafi-dev/core";
159
179
 
160
- // Watches Transfer(0x0 → user) — deducts off-chain balance when mint confirmed.
161
- const pointIndexer = new PointIndexer({
180
+ const addrs = getContractAddresses(8453);
181
+
182
+ // Mint indexer: Transfer(0x0 → user) → deducts off-chain locked balance
183
+ const mintIndexer = new PointIndexer({
162
184
  provider: publicClient,
163
- pointTokenAddress: "0x<PointToken>",
185
+ pointTokenAddress: addrs.pointToken,
164
186
  ledger,
165
- cursorStore, // PostgresCursorStore recommended
187
+ cursorStore, // persists last-processed block; use Postgres or Redis
166
188
  fromBlock: 28_000_000n,
167
189
  confirmations: 2,
168
190
  pollIntervalMs: 3_000,
169
191
  });
170
- pointIndexer.start();
171
192
 
172
- // Watches Transfer(user → 0x0) resolves pending credit when burn confirmed.
193
+ // Burn indexer: Transfer(user → 0x0) resolves pending off-chain credit
173
194
  const burnIndexer = new BurnIndexer({
174
195
  provider: publicClient,
175
- pointTokenAddress: "0x<PointToken>",
196
+ pointTokenAddress: addrs.pointToken,
176
197
  ledger,
177
198
  cursorStore,
178
199
  matchLockId: async (evt) => {
179
- // Return the lockId from ledger.reservePendingCredit() that matches
180
- // this burn event. Typically a DB lookup by (from, amount, status=PENDING).
181
- return yourDb.findPendingCreditLockId(evt.from, evt.amount);
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);
182
203
  },
183
204
  });
205
+
206
+ mintIndexer.start();
184
207
  burnIndexer.start();
185
208
  ```
186
209
 
187
- Indexers must be **singletons** run exactly one instance per token per deployment.
188
- Use a Kubernetes Deployment with `replicas: 1`, not a Deployment behind a load balancer.
210
+ > **Indexers must be singletons.** Run exactly one instance per token per deployment.
211
+ > Use a Kubernetes `Deployment` with `replicas: 1`.
212
+
213
+ ---
214
+
215
+ ## PafiBackendClient
216
+
217
+ HTTP client for requesting paymaster sponsorship from the PAFI backend:
218
+
219
+ ```ts
220
+ import { PafiBackendClient } from "@pafi-dev/issuer";
221
+
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
+
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
+ });
242
+
243
+ // sponsorship.paymaster
244
+ // sponsorship.paymasterData
245
+ // sponsorship.paymasterVerificationGasLimit
246
+ // sponsorship.paymasterPostOpGasLimit
247
+ // sponsorship.expiresAt
248
+ ```
189
249
 
190
250
  ---
191
251
 
192
- ## Error codes
252
+ ## Error handling
253
+
254
+ ### AuthError
255
+
256
+ Thrown by `AuthService` and `authenticateRequest` middleware:
257
+
258
+ ```ts
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
+ }
273
+ ```
274
+
275
+ ### PafiBackendError
276
+
277
+ Thrown by `PafiBackendClient`:
193
278
 
194
279
  ```ts
195
280
  import { PafiBackendError, type PafiBackendErrorCode } from "@pafi-dev/issuer";
196
281
 
197
- type PafiBackendErrorCode =
198
- | "RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_EXCEEDED_DAILY" | "RATE_LIMIT_EXCEEDED_PER_USER"
199
- | "RATE_LIMITER_UNAVAILABLE"
200
- | "INTENT_REJECTED" | "MINT_CAP_EXCEEDED" | "ISSUER_INACTIVE"
201
- | "ISSUER_UNAUTHORIZED" | "USER_UNAUTHORIZED"
202
- | "BAD_REQUEST" | "INTERNAL_ERROR" | "TIMEOUT" | "NETWORK_ERROR";
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
+ }
203
299
  ```
204
300
 
205
301
  ---
206
302
 
207
303
  ## Changelog
208
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
+
209
310
  ### 0.3.0-beta.10
210
- - `MemoryPointLedger` removed from public exports — each issuer implements `IPointLedger` against their own DB
211
- - `PrivateKeySigner` / `IIssuerSigner` removed — SDK accepts viem `WalletClient` directly; see `examples/adapters/kms-signer.ts`
212
- - `ledger` is now a required field in `IssuerServiceConfig` (was optional with in-memory default)
213
- - `handleClaim` added to `IssuerApiHandlers` — enforces policy evaluation atomically before signing
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`
214
315
 
215
316
  ### 0.3.0-beta.9
216
- - `handleBuildConsentTypedData` validates `receiverConsent.originalReceiver` against authenticated user
217
- - `PTRedeemHandler` enforces on-chain balance check before signing BurnRequest
317
+ - `PTRedeemHandler` enforces on-chain balance check before signing `BurnRequest`
218
318
 
219
319
  ### 0.3.0-beta.8
220
320
  - `BalanceAggregator`, `BurnIndexer`, `PTRedeemHandler`, `TopUpRedemptionHandler`
221
- - `BurnIndexer.matchLockId` is now required (throws if not provided)
222
321
 
223
322
  ### 0.3.0-alpha.0
224
323
  - Initial `RelayService`, `PointIndexer`, `AuthService`, `IssuerApiHandlers`
package/dist/index.cjs CHANGED
@@ -2062,20 +2062,20 @@ function createIssuerService(config) {
2062
2062
  }
2063
2063
  }
2064
2064
  return {
2065
- authService,
2066
- sessionStore,
2065
+ auth: authService,
2066
+ session: sessionStore,
2067
2067
  ledger,
2068
2068
  policy,
2069
- relayService,
2070
- feeManager,
2069
+ relay: relayService,
2070
+ fee: feeManager,
2071
2071
  indexers,
2072
2072
  indexer: firstIndexer,
2073
- handlers
2073
+ api: handlers
2074
2074
  };
2075
2075
  }
2076
2076
 
2077
2077
  // src/index.ts
2078
- var PAFI_ISSUER_SDK_VERSION = "0.1.0";
2078
+ var PAFI_ISSUER_SDK_VERSION = "0.4.0";
2079
2079
  // Annotate the CommonJS export names for ESM import in node:
2080
2080
  0 && (module.exports = {
2081
2081
  AuthError,