@pafi-dev/core 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.
Files changed (104) hide show
  1. package/README.md +1157 -0
  2. package/dist/abi/index.cjs +31 -0
  3. package/dist/abi/index.cjs.map +1 -0
  4. package/dist/abi/index.d.cts +2479 -0
  5. package/dist/abi/index.d.ts +2479 -0
  6. package/dist/abi/index.js +31 -0
  7. package/dist/abi/index.js.map +1 -0
  8. package/dist/auth/index.cjs +11 -0
  9. package/dist/auth/index.cjs.map +1 -0
  10. package/dist/auth/index.d.cts +60 -0
  11. package/dist/auth/index.d.ts +60 -0
  12. package/dist/auth/index.js +11 -0
  13. package/dist/auth/index.js.map +1 -0
  14. package/dist/chunk-27V32VNM.cjs +370 -0
  15. package/dist/chunk-27V32VNM.cjs.map +1 -0
  16. package/dist/chunk-2PIXFXA2.js +135 -0
  17. package/dist/chunk-2PIXFXA2.js.map +1 -0
  18. package/dist/chunk-37BBGXTS.js +149 -0
  19. package/dist/chunk-37BBGXTS.js.map +1 -0
  20. package/dist/chunk-4QOWQQCH.cjs +99 -0
  21. package/dist/chunk-4QOWQQCH.cjs.map +1 -0
  22. package/dist/chunk-4RYBJLF3.js +370 -0
  23. package/dist/chunk-4RYBJLF3.js.map +1 -0
  24. package/dist/chunk-5QJZT7VB.cjs +46 -0
  25. package/dist/chunk-5QJZT7VB.cjs.map +1 -0
  26. package/dist/chunk-5YH4D7QF.js +467 -0
  27. package/dist/chunk-5YH4D7QF.js.map +1 -0
  28. package/dist/chunk-BNTVOKYT.cjs +101 -0
  29. package/dist/chunk-BNTVOKYT.cjs.map +1 -0
  30. package/dist/chunk-C4ZDYG4D.js +101 -0
  31. package/dist/chunk-C4ZDYG4D.js.map +1 -0
  32. package/dist/chunk-CAE2BVKI.cjs +136 -0
  33. package/dist/chunk-CAE2BVKI.cjs.map +1 -0
  34. package/dist/chunk-CL3QSI4O.cjs +65 -0
  35. package/dist/chunk-CL3QSI4O.cjs.map +1 -0
  36. package/dist/chunk-CWP4PFOP.cjs +72 -0
  37. package/dist/chunk-CWP4PFOP.cjs.map +1 -0
  38. package/dist/chunk-DSCUHT3F.js +99 -0
  39. package/dist/chunk-DSCUHT3F.js.map +1 -0
  40. package/dist/chunk-FGKLDKJK.cjs +467 -0
  41. package/dist/chunk-FGKLDKJK.cjs.map +1 -0
  42. package/dist/chunk-FIKTQR7H.js +136 -0
  43. package/dist/chunk-FIKTQR7H.js.map +1 -0
  44. package/dist/chunk-FXNG4G22.js +2041 -0
  45. package/dist/chunk-FXNG4G22.js.map +1 -0
  46. package/dist/chunk-GWLEEXM4.cjs +163 -0
  47. package/dist/chunk-GWLEEXM4.cjs.map +1 -0
  48. package/dist/chunk-IPXARZ6F.cjs +135 -0
  49. package/dist/chunk-IPXARZ6F.cjs.map +1 -0
  50. package/dist/chunk-KFWZRL7I.js +65 -0
  51. package/dist/chunk-KFWZRL7I.js.map +1 -0
  52. package/dist/chunk-KW6M5FXY.js +129 -0
  53. package/dist/chunk-KW6M5FXY.js.map +1 -0
  54. package/dist/chunk-MEREIEE4.cjs +129 -0
  55. package/dist/chunk-MEREIEE4.cjs.map +1 -0
  56. package/dist/chunk-O4SMTUOY.js +163 -0
  57. package/dist/chunk-O4SMTUOY.js.map +1 -0
  58. package/dist/chunk-O7LQKCBT.cjs +149 -0
  59. package/dist/chunk-O7LQKCBT.cjs.map +1 -0
  60. package/dist/chunk-P56TUGTS.js +46 -0
  61. package/dist/chunk-P56TUGTS.js.map +1 -0
  62. package/dist/chunk-R4L66ZR5.js +72 -0
  63. package/dist/chunk-R4L66ZR5.js.map +1 -0
  64. package/dist/chunk-S2XRFFP3.cjs +2041 -0
  65. package/dist/chunk-S2XRFFP3.cjs.map +1 -0
  66. package/dist/contract/index.cjs +27 -0
  67. package/dist/contract/index.cjs.map +1 -0
  68. package/dist/contract/index.d.cts +18 -0
  69. package/dist/contract/index.d.ts +18 -0
  70. package/dist/contract/index.js +27 -0
  71. package/dist/contract/index.js.map +1 -0
  72. package/dist/eip712/index.cjs +20 -0
  73. package/dist/eip712/index.cjs.map +1 -0
  74. package/dist/eip712/index.d.cts +83 -0
  75. package/dist/eip712/index.d.ts +83 -0
  76. package/dist/eip712/index.js +20 -0
  77. package/dist/eip712/index.js.map +1 -0
  78. package/dist/index.cjs +412 -0
  79. package/dist/index.cjs.map +1 -0
  80. package/dist/index.d.cts +239 -0
  81. package/dist/index.d.ts +239 -0
  82. package/dist/index.js +412 -0
  83. package/dist/index.js.map +1 -0
  84. package/dist/quoting/index.cjs +19 -0
  85. package/dist/quoting/index.cjs.map +1 -0
  86. package/dist/quoting/index.d.cts +59 -0
  87. package/dist/quoting/index.d.ts +59 -0
  88. package/dist/quoting/index.js +19 -0
  89. package/dist/quoting/index.js.map +1 -0
  90. package/dist/relay/index.cjs +19 -0
  91. package/dist/relay/index.cjs.map +1 -0
  92. package/dist/relay/index.d.cts +62 -0
  93. package/dist/relay/index.d.ts +62 -0
  94. package/dist/relay/index.js +19 -0
  95. package/dist/relay/index.js.map +1 -0
  96. package/dist/swap/index.cjs +29 -0
  97. package/dist/swap/index.cjs.map +1 -0
  98. package/dist/swap/index.d.cts +79 -0
  99. package/dist/swap/index.d.ts +79 -0
  100. package/dist/swap/index.js +29 -0
  101. package/dist/swap/index.js.map +1 -0
  102. package/dist/types-CkRJzrZr.d.cts +98 -0
  103. package/dist/types-CkRJzrZr.d.ts +98 -0
  104. package/package.json +114 -0
package/README.md ADDED
@@ -0,0 +1,1157 @@
1
+ # @pafi/core
2
+
3
+ The core TypeScript SDK for the PAFI point token system. Provides EIP-712
4
+ signing and verification, EIP-4361 (Sign-In with Ethereum) helpers,
5
+ contract ABIs, Relay calldata encoding, on-chain quoting, and swap
6
+ building.
7
+
8
+ **`@pafi/core` is HTTP-client-free on purpose.** It covers only the
9
+ primitives that need a signer or a provider (or neither) — never the
10
+ HTTP wire. The HTTP contract (routes, request/response types) is owned
11
+ by [`@pafi/issuer`](../issuer/README.md) as the single source of truth,
12
+ and frontends import those types `type`-only so browser bundles never
13
+ pull in server code like `jose` or `node:crypto`.
14
+
15
+ | Runs on | Purpose |
16
+ |---|---|
17
+ | Frontend (web + mobile) | Sign EIP-712 messages, build calldata, quote swaps, construct login messages |
18
+ | Issuer backend | Verify EIP-712 signatures, decode calldata, read on-chain nonces / minter status |
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pnpm add @pafi/core viem
24
+ ```
25
+
26
+ `viem ^2.0.0` is a peer dependency and must be installed alongside this package.
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ ```ts
33
+ import { createPublicClient, createWalletClient, http } from "viem";
34
+ import { privateKeyToAccount } from "viem/accounts";
35
+ import { PafiSDK } from "@pafi/core";
36
+
37
+ const account = privateKeyToAccount("0x...");
38
+
39
+ const sdk = new PafiSDK({
40
+ chainId: 8453,
41
+ rpcUrl: "https://base-mainnet.g.alchemy.com/v2/...",
42
+ signer: createWalletClient({
43
+ account,
44
+ transport: http("https://base-mainnet.g.alchemy.com/v2/..."),
45
+ }),
46
+ pointTokenAddress: "0xPointToken...",
47
+ relayContractAddress: "0xRelay...",
48
+ });
49
+
50
+ // Fetch on-chain nonce then sign a mint request
51
+ const nonce = await sdk.getMintRequestNonce(account.address);
52
+
53
+ const sig = await sdk.signMintRequest({
54
+ to: account.address,
55
+ amount: 1_000_000n,
56
+ nonce,
57
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
58
+ });
59
+
60
+ console.log(sig.serialized);
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Sub-path Imports
66
+
67
+ Every module is available both from the root entry and via its own sub-path export. Use sub-paths to reduce bundle size in tree-shaking-aware environments.
68
+
69
+ | Sub-path | Contents |
70
+ |---|---|
71
+ | `@pafi/core` | All exports (PafiSDK class + everything below) |
72
+ | `@pafi/core/eip712` | `buildMintRequestTypedData`, `buildReceiverConsentTypedData`, `signMintRequest`, `verifyMintRequest`, `signReceiverConsent`, `verifyReceiverConsent`, `buildDomain` |
73
+ | `@pafi/core/relay` | `encodeMintAndSwap`, `decodeMintAndSwap`, `buildRelaySwapParams`, `encodeExtData`, `decodeExtData` |
74
+ | `@pafi/core/contract` | `getMintRequestNonce`, `getReceiverConsentNonce`, `isMinter`, `getTokenName`, `getIssuer`, `isActiveIssuer`, `verifyMintCap`, `getPointTokenIssuer`, `getFeeBasisPoints`, `getSlippageBasisPoints`, `getUsdt` |
75
+ | `@pafi/core/quoting` | `quoteExactInput`, `quoteExactInputSingle`, `quoteBestRoute`, `findBestQuote`, `buildAllPaths`, `combineRoutes` |
76
+ | `@pafi/core/swap` | `checkAllowance`, `buildErc20ApprovalCalldata`, `buildPermit2ApprovalCalldata`, `buildUniversalRouterExecuteArgs`, `buildSwapFromQuote`, `buildV4SwapInput` |
77
+ | `@pafi/core/auth` | `createLoginMessage`, `parseLoginMessage`, `verifyLoginMessage` (EIP-4361) |
78
+ | `@pafi/core/abi` | All contract ABIs |
79
+
80
+ ---
81
+
82
+ ## API Reference
83
+
84
+ ### PafiSDK Class
85
+
86
+ The `PafiSDK` class is a convenience wrapper around all pure functions. All methods delegate to the same pure functions exported from the sub-path modules.
87
+
88
+ #### Constructor
89
+
90
+ ```ts
91
+ import { PafiSDK } from "@pafi/core";
92
+
93
+ const sdk = new PafiSDK(config: PafiSDKConfig);
94
+ ```
95
+
96
+ ```ts
97
+ interface PafiSDKConfig {
98
+ chainId?: number;
99
+ pointTokenAddress?: Address;
100
+ relayContractAddress?: Address;
101
+ signer?: WalletClient; // viem WalletClient
102
+ provider?: PublicClient; // viem PublicClient (takes precedence over rpcUrl)
103
+ rpcUrl?: string; // creates a PublicClient automatically if provider is omitted
104
+ }
105
+ ```
106
+
107
+ All fields are optional at construction time. Missing fields are validated lazily when the relevant method is called, throwing a `ConfigurationError` if a required field is absent.
108
+
109
+ #### Setters
110
+
111
+ ```ts
112
+ sdk.setPointTokenAddress(address: Address): void
113
+ sdk.setRelayContractAddress(address: Address): void
114
+ sdk.setSigner(signer: WalletClient): void
115
+ sdk.setProvider(provider: PublicClient): void
116
+ ```
117
+
118
+ #### Domain
119
+
120
+ ```ts
121
+ // Resolves the EIP-712 domain by reading the token name on-chain.
122
+ // Requires provider, pointTokenAddress, and chainId.
123
+ await sdk.getDomain(): Promise<PointTokenDomainConfig>
124
+ ```
125
+
126
+ ---
127
+
128
+ ### EIP-712 Signing
129
+
130
+ #### Pure functions (from `@pafi/core/eip712`)
131
+
132
+ ```ts
133
+ import {
134
+ buildDomain,
135
+ signMintRequest,
136
+ verifyMintRequest,
137
+ signReceiverConsent,
138
+ verifyReceiverConsent,
139
+ } from "@pafi/core/eip712";
140
+ ```
141
+
142
+ ##### `buildDomain`
143
+
144
+ ```ts
145
+ function buildDomain(config: PointTokenDomainConfig): {
146
+ name: string;
147
+ version: "1";
148
+ chainId: number;
149
+ verifyingContract: Address;
150
+ }
151
+ ```
152
+
153
+ Constructs the EIP-712 domain object. Version is always `"1"`.
154
+
155
+ ##### `signMintRequest`
156
+
157
+ ```ts
158
+ async function signMintRequest(
159
+ walletClient: WalletClient,
160
+ domain: PointTokenDomainConfig,
161
+ message: MintRequest,
162
+ ): Promise<EIP712Signature>
163
+ ```
164
+
165
+ Signs a `MintRequest` struct with the connected wallet.
166
+
167
+ ##### `verifyMintRequest`
168
+
169
+ ```ts
170
+ async function verifyMintRequest(
171
+ domain: PointTokenDomainConfig,
172
+ message: MintRequest,
173
+ signature: Hex,
174
+ expectedMinter: Address,
175
+ ): Promise<SignatureVerification>
176
+ ```
177
+
178
+ Recovers the signer from a `MintRequest` signature and compares against `expectedMinter`.
179
+
180
+ ##### `signReceiverConsent`
181
+
182
+ ```ts
183
+ async function signReceiverConsent(
184
+ walletClient: WalletClient,
185
+ domain: PointTokenDomainConfig,
186
+ message: ReceiverConsent,
187
+ ): Promise<EIP712Signature>
188
+ ```
189
+
190
+ Signs a `ReceiverConsent` struct.
191
+
192
+ ##### `verifyReceiverConsent`
193
+
194
+ ```ts
195
+ async function verifyReceiverConsent(
196
+ domain: PointTokenDomainConfig,
197
+ message: ReceiverConsent,
198
+ signature: Hex,
199
+ expectedReceiver: Address,
200
+ ): Promise<SignatureVerification>
201
+ ```
202
+
203
+ Recovers and validates a `ReceiverConsent` signature.
204
+
205
+ #### PafiSDK methods
206
+
207
+ ```ts
208
+ await sdk.signMintRequest(message: MintRequest): Promise<EIP712Signature>
209
+ await sdk.verifyMintRequest(message: MintRequest, signature: Hex, expectedMinter: Address): Promise<SignatureVerification>
210
+ await sdk.signReceiverConsent(message: ReceiverConsent): Promise<EIP712Signature>
211
+ await sdk.verifyReceiverConsent(message: ReceiverConsent, signature: Hex, expectedReceiver: Address): Promise<SignatureVerification>
212
+ ```
213
+
214
+ All four methods automatically resolve the domain from on-chain state via `getDomain()`.
215
+
216
+ ---
217
+
218
+ ### Relay Calldata
219
+
220
+ #### Pure functions (from `@pafi/core/relay`)
221
+
222
+ ```ts
223
+ import {
224
+ encodeMintAndSwap,
225
+ decodeMintAndSwap,
226
+ encodeMintAndSwapV2,
227
+ decodeMintAndSwapV2,
228
+ } from "@pafi/core/relay";
229
+ ```
230
+
231
+ ##### V1 (Relay.sol)
232
+
233
+ ```ts
234
+ // ABI-encode a mintAndSwap(MintParams, SwapParams) call for Relay.sol
235
+ function encodeMintAndSwap(mint: MintParams, swap: SwapParams): Hex
236
+
237
+ // Decode calldata produced by encodeMintAndSwap
238
+ function decodeMintAndSwap(calldata: Hex): { mint: MintParams; swap: SwapParams }
239
+ ```
240
+
241
+ ##### V2 (RelayV2.sol)
242
+
243
+ ```ts
244
+ // ABI-encode a mintAndSwap(MintParams, SwapParams) call for RelayV2.sol
245
+ function encodeMintAndSwapV2(mint: MintParams, swap: SwapParams): Hex
246
+
247
+ // Decode calldata produced by encodeMintAndSwapV2
248
+ function decodeMintAndSwapV2(calldata: Hex): { mint: MintParams; swap: SwapParams }
249
+ ```
250
+
251
+ #### Example
252
+
253
+ ```ts
254
+ import { encodeMintAndSwap } from "@pafi/core/relay";
255
+
256
+ const calldata = encodeMintAndSwap(
257
+ {
258
+ pointToken: "0xPointToken...",
259
+ receiver: "0xReceiver...",
260
+ amount: 1_000_000n,
261
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
262
+ minterSig: "0x...",
263
+ receiverSig: "0x...",
264
+ },
265
+ {
266
+ path: [
267
+ {
268
+ intermediateCurrency: "0xUSDT...",
269
+ fee: 500,
270
+ tickSpacing: 10,
271
+ hooks: "0x0000000000000000000000000000000000000000",
272
+ hookData: "0x",
273
+ },
274
+ ],
275
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
276
+ },
277
+ );
278
+ ```
279
+
280
+ #### PafiSDK methods
281
+
282
+ ```ts
283
+ sdk.encodeMintAndSwap(mint: MintParams, swap: SwapParams): Hex
284
+ sdk.decodeMintAndSwap(calldata: Hex): { mint: MintParams; swap: SwapParams }
285
+ sdk.encodeExtData(minAmountOut: bigint, feeInUsdt: bigint): Hex
286
+ sdk.buildRelaySwapParams({ quote, minAmountOut, feeInUsdt, deadline }): { swapParams, extData }
287
+ ```
288
+
289
+ ---
290
+
291
+ ### Contract Reads
292
+
293
+ #### PointToken (from `@pafi/core/contract`)
294
+
295
+ ```ts
296
+ import {
297
+ getMintRequestNonce,
298
+ getReceiverConsentNonce,
299
+ isMinter,
300
+ getTokenName,
301
+ getPointTokenIssuerAddress,
302
+ } from "@pafi/core/contract";
303
+ ```
304
+
305
+ | Function | Signature | Description |
306
+ |---|---|---|
307
+ | `getMintRequestNonce` | `(client, pointToken, receiver) => Promise<bigint>` | Current `MintRequest` nonce for `receiver` |
308
+ | `getReceiverConsentNonce` | `(client, pointToken, receiver) => Promise<bigint>` | Current `ReceiverConsent` nonce for `receiver` |
309
+ | `isMinter` | `(client, pointToken, account) => Promise<boolean>` | Returns `true` if `account` has the minter role |
310
+ | `getTokenName` | `(client, pointToken) => Promise<string>` | ERC-20 `name()` — used as the EIP-712 domain name |
311
+ | `getPointTokenIssuerAddress` | `(client, pointToken) => Promise<Address>` | Issuer address registered on the token |
312
+
313
+ #### IssuerRegistry
314
+
315
+ ```ts
316
+ import { getIssuer, isActiveIssuer } from "@pafi/core/contract";
317
+ ```
318
+
319
+ | Function | Signature | Description |
320
+ |---|---|---|
321
+ | `getIssuer` | `(client, registryAddress, issuer) => Promise<Issuer>` | Full `Issuer` struct for the given address |
322
+ | `isActiveIssuer` | `(client, registryAddress, issuer) => Promise<boolean>` | Whether the issuer is currently active |
323
+
324
+ #### MintingOracle
325
+
326
+ ```ts
327
+ import { verifyMintCap, getPointTokenIssuer } from "@pafi/core/contract";
328
+ ```
329
+
330
+ | Function | Signature | Description |
331
+ |---|---|---|
332
+ | `verifyMintCap` | `(client, oracleAddress, issuer, amount) => Promise<void>` | Reverts if `amount` would exceed the issuer's mint cap |
333
+ | `getPointTokenIssuer` | `(client, oracleAddress, pointToken) => Promise<Address>` | Maps a point token address back to its issuer |
334
+
335
+ #### Relay helpers
336
+
337
+ ```ts
338
+ import { getFeeBasisPoints, getSlippageBasisPoints, getUsdt } from "@pafi/core/contract";
339
+ ```
340
+
341
+ | Function | Signature | Description |
342
+ |---|---|---|
343
+ | `getFeeBasisPoints` | `(client, relayAddress) => Promise<bigint>` | Protocol fee in basis points |
344
+ | `getSlippageBasisPoints` | `(client, relayAddress) => Promise<bigint>` | Slippage tolerance in basis points |
345
+ | `getUsdt` | `(client, relayAddress) => Promise<Address>` | USDT address used by the Relay contract |
346
+
347
+ #### PafiSDK methods
348
+
349
+ ```ts
350
+ await sdk.getMintRequestNonce(receiver: Address): Promise<bigint>
351
+ await sdk.getReceiverConsentNonce(receiver: Address): Promise<bigint>
352
+ ```
353
+
354
+ #### PafiSDK quoting + swap methods
355
+
356
+ ```ts
357
+ // Quote — finds the best route via multicall
358
+ await sdk.findBestQuote(tokenIn, tokenOut, amount, pools?, quoterAddress?): Promise<BestQuote>
359
+
360
+ // Build UniversalRouter execute args from a quote
361
+ sdk.buildSwapFromQuote({ quote, currencyIn, currencyOut, amountIn, minAmountOut }): { commands, inputs }
362
+
363
+ // Build Relay SwapParams + extData from a quote
364
+ sdk.buildRelaySwapParams({ quote, minAmountOut, feeInUsdt, deadline }): { swapParams, extData }
365
+
366
+ // Encode extData for ReceiverConsent signing
367
+ sdk.encodeExtData(minAmountOut, feeInUsdt): Hex
368
+ ```
369
+
370
+ ---
371
+
372
+ ### Quoting
373
+
374
+ #### Pure functions (from `@pafi/core/quoting`)
375
+
376
+ ```ts
377
+ import {
378
+ quoteExactInput,
379
+ quoteExactInputSingle,
380
+ quoteBestRoute,
381
+ findBestQuote,
382
+ buildAllPaths,
383
+ combineRoutes,
384
+ } from "@pafi/core/quoting";
385
+ ```
386
+
387
+ ##### `quoteExactInput`
388
+
389
+ ```ts
390
+ async function quoteExactInput(
391
+ client: PublicClient,
392
+ quoterAddress: Address,
393
+ exactCurrency: Address,
394
+ path: PathKey[],
395
+ exactAmount: bigint,
396
+ ): Promise<QuoteResult>
397
+ ```
398
+
399
+ Quotes a multi-hop exact-input swap against the V4 quoter.
400
+
401
+ ##### `quoteExactInputSingle`
402
+
403
+ ```ts
404
+ async function quoteExactInputSingle(
405
+ client: PublicClient,
406
+ quoterAddress: Address,
407
+ poolKey: PoolKey,
408
+ zeroForOne: boolean,
409
+ exactAmount: bigint,
410
+ hookData: Hex,
411
+ ): Promise<{ amountOut: bigint; gasEstimate: bigint }>
412
+ ```
413
+
414
+ Quotes a single-hop swap given an explicit pool key and swap direction.
415
+
416
+ ##### `quoteBestRoute`
417
+
418
+ ```ts
419
+ async function quoteBestRoute(
420
+ client: PublicClient,
421
+ quoterAddress: Address,
422
+ exactCurrency: Address,
423
+ routes: PathKey[][],
424
+ exactAmount: bigint,
425
+ ): Promise<BestQuote>
426
+ ```
427
+
428
+ Quotes multiple routes via a single `multicall` RPC call and returns the one with the highest `amountOut`. Routes that fail (e.g. pool does not exist) are silently dropped. Throws if no routes succeed.
429
+
430
+ ##### `findBestQuote`
431
+
432
+ ```ts
433
+ async function findBestQuote(
434
+ client: PublicClient,
435
+ chainId: number,
436
+ tokenIn: Address,
437
+ tokenOut: Address,
438
+ exactAmount: bigint,
439
+ pools?: PoolKey[],
440
+ quoterAddress?: Address,
441
+ maxHops?: number,
442
+ ): Promise<BestQuote>
443
+ ```
444
+
445
+ One-call quoting helper. Merges caller-provided `pools` with `COMMON_POOLS[chainId]`, builds all possible paths via `buildAllPaths`, then quotes them all via multicall. Uses `V4_QUOTER_ADDRESSES[chainId]` unless `quoterAddress` is provided.
446
+
447
+ ##### `buildAllPaths`
448
+
449
+ ```ts
450
+ function buildAllPaths(
451
+ pools: PoolKey[],
452
+ tokenIn: Address,
453
+ tokenOut: Address,
454
+ maxHops?: number,
455
+ ): PathKey[][]
456
+ ```
457
+
458
+ Builds all possible swap paths from `tokenIn` to `tokenOut` using DFS through the given pools. Each pool is used at most once per path. Returns an array of `PathKey[]` routes (up to `maxHops`, default 3).
459
+
460
+ ##### `combineRoutes`
461
+
462
+ ```ts
463
+ function combineRoutes(
464
+ chainId: number,
465
+ pointTokenAddress: Address,
466
+ ): PoolKey[]
467
+ ```
468
+
469
+ Merges `POINT_TOKEN_POOLS[chainId][pointTokenAddress]` and `COMMON_POOLS[chainId]` into a single pool list, with point token pools listed first.
470
+
471
+ #### Example
472
+
473
+ ```ts
474
+ import { findBestQuote } from "@pafi/core/quoting";
475
+
476
+ // Provide only your token-specific pools — COMMON_POOLS are merged automatically
477
+ const pointTokenPools = [
478
+ { currency0: USDC, currency1: pointToken, fee: 3000, tickSpacing: 60, hooks: ZERO },
479
+ ];
480
+
481
+ const { bestRoute, allRoutes } = await findBestQuote(
482
+ client,
483
+ 8453, // Base chain ID — auto-resolves quoter address
484
+ pointToken,
485
+ USDC,
486
+ 1_000_000n,
487
+ pointTokenPools, // merged with COMMON_POOLS[8453]
488
+ );
489
+
490
+ console.log(bestRoute.amountOut, bestRoute.gasEstimate);
491
+ console.log(`Found ${allRoutes.length} valid routes`);
492
+ ```
493
+
494
+ ---
495
+
496
+ ### Swap Building
497
+
498
+ #### Pure functions (from `@pafi/core/swap`)
499
+
500
+ ```ts
501
+ import {
502
+ checkAllowance,
503
+ buildErc20ApprovalCalldata,
504
+ buildPermit2ApprovalCalldata,
505
+ buildUniversalRouterExecuteArgs,
506
+ buildV4SwapInput,
507
+ } from "@pafi/core/swap";
508
+ ```
509
+
510
+ ##### `checkAllowance`
511
+
512
+ ```ts
513
+ async function checkAllowance(
514
+ client: PublicClient,
515
+ token: Address,
516
+ owner: Address,
517
+ spender: Address,
518
+ ): Promise<bigint>
519
+ ```
520
+
521
+ Reads the ERC-20 allowance of `spender` on behalf of `owner`.
522
+
523
+ ##### `buildErc20ApprovalCalldata`
524
+
525
+ ```ts
526
+ function buildErc20ApprovalCalldata(spender: Address, amount: bigint): Hex
527
+ ```
528
+
529
+ Encodes `ERC20.approve(spender, amount)`.
530
+
531
+ ##### `buildPermit2ApprovalCalldata`
532
+
533
+ ```ts
534
+ function buildPermit2ApprovalCalldata(
535
+ token: Address,
536
+ spender: Address,
537
+ amount: bigint,
538
+ expiration: number,
539
+ ): Hex
540
+ ```
541
+
542
+ Encodes `Permit2.approve(token, spender, amount, expiration)`.
543
+
544
+ ##### `buildUniversalRouterExecuteArgs`
545
+
546
+ ```ts
547
+ function buildUniversalRouterExecuteArgs(
548
+ currencyIn: Address,
549
+ path: PathKey[],
550
+ amountIn: bigint,
551
+ minAmountOut: bigint,
552
+ outputCurrency: Address,
553
+ ): { commands: Hex; inputs: Hex[] }
554
+ ```
555
+
556
+ Builds the `commands` and `inputs` arguments for `UniversalRouter.execute`. The command sequence is `V4_SWAP` (`0x10`). Actions encoded inside the payload are `SWAP_EXACT_IN → SETTLE_ALL → TAKE_ALL`.
557
+
558
+ ##### `buildV4SwapInput`
559
+
560
+ ```ts
561
+ function buildV4SwapInput(
562
+ currencyIn: Address,
563
+ path: PathKey[],
564
+ amountIn: bigint,
565
+ minAmountOut: bigint,
566
+ outputCurrency: Address,
567
+ ): Hex
568
+ ```
569
+
570
+ Encodes only the V4_SWAP command payload (`inputs[0]`). Use `buildUniversalRouterExecuteArgs` unless you need the raw payload for custom command composition.
571
+
572
+ ##### `buildSwapFromQuote`
573
+
574
+ ```ts
575
+ function buildSwapFromQuote(params: {
576
+ quote: QuoteResult;
577
+ currencyIn: Address;
578
+ currencyOut: Address;
579
+ amountIn: bigint;
580
+ minAmountOut: bigint;
581
+ }): { commands: Hex; inputs: Hex[] }
582
+ ```
583
+
584
+ Build UniversalRouter execute args directly from a `QuoteResult` (returned by `findBestQuote` or `quoteBestRoute`). The caller provides `minAmountOut` after applying their own slippage tolerance.
585
+
586
+ #### Example
587
+
588
+ ```ts
589
+ import { findBestQuote } from "@pafi/core/quoting";
590
+ import { buildSwapFromQuote } from "@pafi/core/swap";
591
+
592
+ // 1. Quote
593
+ const { bestRoute } = await findBestQuote(client, 8453, tokenIn, USDC, amountIn, myPools);
594
+
595
+ // 2. Build swap calldata (apply slippage externally)
596
+ const minReceive = bestRoute.amountOut * 99n / 100n; // 1% slippage
597
+ const { commands, inputs } = buildSwapFromQuote({
598
+ quote: bestRoute,
599
+ currencyIn: tokenIn,
600
+ currencyOut: USDC,
601
+ amountIn,
602
+ minAmountOut: minReceive,
603
+ });
604
+
605
+ // 3. Execute via UniversalRouter
606
+ ```
607
+
608
+ ---
609
+
610
+ ### Auth helpers (EIP-4361 — Sign-In With Ethereum)
611
+
612
+ #### Pure functions (from `@pafi/core/auth`)
613
+
614
+ ```ts
615
+ import {
616
+ createLoginMessage,
617
+ parseLoginMessage,
618
+ verifyLoginMessage,
619
+ } from "@pafi/core/auth";
620
+ ```
621
+
622
+ These are stateless helpers — they build, parse, and verify EIP-4361
623
+ login messages without touching the network. The frontend uses them to
624
+ construct the message the wallet signs; the backend uses `verifyLoginMessage`
625
+ inside its own `AuthService` (see `@pafi/issuer`).
626
+
627
+ ```ts
628
+ function createLoginMessage(params: {
629
+ domain: string; // e.g. "app.example.com"
630
+ address: Address; // user's wallet
631
+ chainId: number;
632
+ nonce: string; // from issuer backend /auth/nonce
633
+ uri: string; // e.g. "https://app.example.com"
634
+ statement?: string; // human-readable sign-in message
635
+ version?: string; // defaults to "1"
636
+ issuedAt?: Date;
637
+ expirationTime?: Date;
638
+ notBefore?: Date;
639
+ requestId?: string;
640
+ }): string
641
+
642
+ function parseLoginMessage(message: string): LoginMessageParams
643
+
644
+ async function verifyLoginMessage(
645
+ message: string,
646
+ signature: Hex,
647
+ ): Promise<{ valid: boolean; address: Address }>
648
+ ```
649
+
650
+ #### PafiSDK helpers
651
+
652
+ ```ts
653
+ // Build a login message for the current signer + chain
654
+ await sdk.createLoginMessage(params: Omit<LoginMessageParams, "address" | "chainId">): Promise<string>
655
+
656
+ // Sign a login message with the current signer (personal_sign)
657
+ await sdk.signLoginMessage(message: string): Promise<Hex>
658
+ ```
659
+
660
+ The SDK does not post the signed message for you — the frontend calls
661
+ its own `fetch()` against the issuer backend:
662
+
663
+ ```ts
664
+ // 1. Fetch nonce from the issuer backend
665
+ const { nonce } = await fetch(`${ISSUER_URL}/auth/nonce`).then(r => r.json());
666
+
667
+ // 2. Build + sign the login message
668
+ const message = await sdk.createLoginMessage({
669
+ domain: "app.example.com",
670
+ uri: "https://app.example.com",
671
+ nonce,
672
+ });
673
+ const signature = await sdk.signLoginMessage(message);
674
+
675
+ // 3. POST to the issuer backend, receive a JWT
676
+ const { token, userAddress } = await fetch(`${ISSUER_URL}/auth/login`, {
677
+ method: "POST",
678
+ headers: { "Content-Type": "application/json" },
679
+ body: JSON.stringify({ message, signature }),
680
+ }).then(r => r.json());
681
+
682
+ // 4. Store the JWT and attach it to subsequent protected requests
683
+ // (handled by the frontend's own state management — @pafi/core does
684
+ // not manage tokens)
685
+ ```
686
+
687
+ > **Why isn't there an HTTP client?** The HTTP contract (routes,
688
+ > request/response shapes) is owned by `@pafi/issuer`. Frontends import
689
+ > those types `type`-only and build their own `fetch()` calls. This
690
+ > keeps the frontend bundle free of server-side dependencies like
691
+ > `jose` and `node:crypto`, and ensures the protocol has exactly one
692
+ > source of truth.
693
+
694
+ ---
695
+
696
+ ### ABIs
697
+
698
+ All ABIs are available from `@pafi/core/abi` or from the root import.
699
+
700
+ ```ts
701
+ import {
702
+ pointTokenAbi,
703
+ relayAbi,
704
+ relayV2Abi,
705
+ issuerRegistryAbi,
706
+ pointTokenFactoryAbi,
707
+ mintingOracleAbi,
708
+ erc20Abi,
709
+ universalRouterAbi,
710
+ permit2Abi,
711
+ v4QuoterAbi,
712
+ } from "@pafi/core/abi";
713
+ ```
714
+
715
+ | Export | Contract |
716
+ |---|---|
717
+ | `pointTokenAbi` | PointToken ERC-20 with minting and nonce functions |
718
+ | `relayAbi` | Relay.sol — `mintAndSwap`, fee and slippage reads |
719
+ | `relayV2Abi` | RelayV2.sol — same interface, new deployment |
720
+ | `issuerRegistryAbi` | IssuerRegistry — `getIssuer`, `isActiveIssuer` |
721
+ | `pointTokenFactoryAbi` | PointTokenFactory |
722
+ | `mintingOracleAbi` | MintingOracle — `verifyMintCap`, `pointTokenToIssuer` |
723
+ | `erc20Abi` | Standard ERC-20 (`allowance`, `approve`, `transfer`, `balanceOf`) |
724
+ | `universalRouterAbi` | Uniswap UniversalRouter — `execute` |
725
+ | `permit2Abi` | Uniswap Permit2 — `approve` |
726
+ | `v4QuoterAbi` | Uniswap V4 Quoter — `quoteExactInput`, `quoteExactInputSingle` |
727
+
728
+ ---
729
+
730
+ ### Constants
731
+
732
+ ```ts
733
+ import {
734
+ SUPPORTED_CHAINS,
735
+ COMMON_TOKENS,
736
+ COMMON_POOLS,
737
+ POINT_TOKEN_POOLS,
738
+ mintRequestTypes,
739
+ receiverConsentTypes,
740
+ } from "@pafi/core";
741
+ ```
742
+
743
+ | Constant | Type | Description |
744
+ |---|---|---|
745
+ | `SUPPORTED_CHAINS` | `Record<number, ChainConfig>` | Chain ID to chain metadata map |
746
+ | `COMMON_TOKENS` | `Record<number, Record<string, Address>>` | Chain ID to `{ symbol → address }` token map |
747
+ | `COMMON_POOLS` | `Record<number, PoolKey[]>` | Chain ID to common pool list (e.g. stablecoin pairs) |
748
+ | `POINT_TOKEN_POOLS` | `Record<number, Record<Address, PoolKey[]>>` | Chain ID to per-point-token pool list |
749
+ | `mintRequestTypes` | `const` | EIP-712 typed data types for `MintRequest` |
750
+ | `receiverConsentTypes` | `const` | EIP-712 typed data types for `ReceiverConsent` |
751
+
752
+ `COMMON_POOLS` and `POINT_TOKEN_POOLS` are consumed by `combineRoutes` to assemble candidate routes for quoting.
753
+
754
+ ---
755
+
756
+ ### Errors
757
+
758
+ All errors extend `PafiSDKError`, which extends `Error`.
759
+
760
+ ```ts
761
+ import { PafiSDKError, ConfigurationError, SigningError, ApiError } from "@pafi/core";
762
+ ```
763
+
764
+ | Class | Thrown when |
765
+ |---|---|
766
+ | `PafiSDKError` | Base class — not thrown directly |
767
+ | `ConfigurationError` | A required field (`pointTokenAddress`, `signer`, `provider`, etc.) is not set when a method requires it |
768
+ | `SigningError` | A signing operation fails |
769
+ | `ApiError` | Generic HTTP error helper for callers writing their own `fetch()` against the issuer backend; carries an optional `status` |
770
+
771
+ #### Example
772
+
773
+ ```ts
774
+ import { ConfigurationError, SigningError } from "@pafi/core";
775
+
776
+ try {
777
+ await sdk.signMintRequest(message);
778
+ } catch (err) {
779
+ if (err instanceof ConfigurationError) {
780
+ console.error(`SDK misconfigured: ${err.message}`);
781
+ } else if (err instanceof SigningError) {
782
+ console.error(`Signing failed: ${err.message}`);
783
+ }
784
+ }
785
+ ```
786
+
787
+ ---
788
+
789
+ ## EIP-712 Type Reference
790
+
791
+ The PointToken contract is the authoritative EIP-712 domain contract.
792
+
793
+ ### Domain
794
+
795
+ ```
796
+ name: <token name from ERC-20 name()>
797
+ version: "1"
798
+ chainId: <network chain ID>
799
+ verifyingContract: <PointToken address>
800
+ ```
801
+
802
+ ### MintRequest
803
+
804
+ ```
805
+ MintRequest(address to, uint256 amount, uint256 nonce, uint256 deadline)
806
+ ```
807
+
808
+ | Field | Type | Description |
809
+ |---|---|---|
810
+ | `to` | `address` | Recipient of the minted tokens |
811
+ | `amount` | `uint256` | Token amount to mint |
812
+ | `nonce` | `uint256` | Anti-replay nonce from `mintRequestNonces(to)` |
813
+ | `deadline` | `uint256` | Unix timestamp after which the signature expires |
814
+
815
+ The minter (issuer signer) signs this struct. The nonce must match `pointToken.mintRequestNonces(to)` at the time of execution.
816
+
817
+ ### ReceiverConsent
818
+
819
+ ```
820
+ ReceiverConsent(address onBehalfOf, address originalReceiver, uint256 amount, uint256 nonce, uint256 deadline)
821
+ ```
822
+
823
+ | Field | Type | Description |
824
+ |---|---|---|
825
+ | `onBehalfOf` | `address` | Address whose points are being redirected |
826
+ | `originalReceiver` | `address` | The original recipient who is granting consent |
827
+ | `amount` | `uint256` | Token amount involved in the consent |
828
+ | `nonce` | `uint256` | Anti-replay nonce from `receiverConsentNonces(originalReceiver)` |
829
+ | `deadline` | `uint256` | Unix timestamp after which the signature expires |
830
+
831
+ The original receiver signs this struct to authorize minting on behalf of another address.
832
+
833
+ ---
834
+
835
+ ## PointToken Mint Paths
836
+
837
+ The Relay contract supports three mint paths depending on which signatures are provided:
838
+
839
+ | Path | Signer | Receiver consent required | Description |
840
+ |---|---|---|---|
841
+ | 1 | Minter (issuer signer) | No | Issuer mints directly to `receiver`. Only the `minterSig` is validated. |
842
+ | 2 | Minter + Receiver | Yes | Issuer mints on behalf of `receiver`, who has pre-signed a `ReceiverConsent`. Both signatures are validated. |
843
+ | 3 | Minter + Relayer | No | Issuer authorizes a relayer to execute the mint-and-swap. The relayer submits the transaction. |
844
+
845
+ In all paths, the `MintParams.minterSig` is a `MintRequest` EIP-712 signature. Path 2 additionally requires `MintParams.receiverSig`, which is a `ReceiverConsent` EIP-712 signature.
846
+
847
+ ---
848
+
849
+ ## Type Reference
850
+
851
+ ### Core types
852
+
853
+ ```ts
854
+ interface MintRequest {
855
+ to: Address;
856
+ amount: bigint;
857
+ nonce: bigint;
858
+ deadline: bigint;
859
+ }
860
+
861
+ interface ReceiverConsent {
862
+ onBehalfOf: Address;
863
+ originalReceiver: Address;
864
+ amount: bigint;
865
+ nonce: bigint;
866
+ deadline: bigint;
867
+ }
868
+
869
+ interface EIP712Signature {
870
+ v: number;
871
+ r: Hex;
872
+ s: Hex;
873
+ serialized: Hex;
874
+ }
875
+
876
+ interface SignatureVerification {
877
+ isValid: boolean;
878
+ recoveredAddress: Address;
879
+ }
880
+
881
+ interface PointTokenDomainConfig {
882
+ name: string;
883
+ verifyingContract: Address;
884
+ chainId: number;
885
+ }
886
+ ```
887
+
888
+ ### Relay types
889
+
890
+ ```ts
891
+ interface MintParams {
892
+ pointToken: Address;
893
+ receiver: Address;
894
+ amount: bigint;
895
+ deadline: bigint;
896
+ minterSig: Hex;
897
+ receiverSig: Hex;
898
+ }
899
+
900
+ interface PathKey {
901
+ intermediateCurrency: Address;
902
+ fee: number;
903
+ tickSpacing: number;
904
+ hooks: Address;
905
+ hookData: Hex;
906
+ }
907
+
908
+ interface SwapParams {
909
+ path: PathKey[];
910
+ deadline: bigint;
911
+ }
912
+ ```
913
+
914
+ ### Pool and issuer types
915
+
916
+ ```ts
917
+ interface PoolKey {
918
+ currency0: Address;
919
+ currency1: Address;
920
+ fee: number;
921
+ tickSpacing: number;
922
+ hooks: Address;
923
+ }
924
+
925
+ interface Issuer {
926
+ issuerAddress: Address;
927
+ signerAddress: Address;
928
+ name: string;
929
+ symbol: string;
930
+ declaredTotalSupply: bigint;
931
+ capBasisPoints: number;
932
+ active: boolean;
933
+ pointToken: Address;
934
+ mintingOracle: Address;
935
+ }
936
+ ```
937
+
938
+ ### Quoting types
939
+
940
+ ```ts
941
+ interface QuoteResult {
942
+ amountOut: bigint;
943
+ gasEstimate: bigint;
944
+ path: PathKey[];
945
+ }
946
+
947
+ interface BestQuote {
948
+ bestRoute: QuoteResult;
949
+ allRoutes: QuoteResult[];
950
+ }
951
+ ```
952
+
953
+ ---
954
+
955
+ ## Integration Guide
956
+
957
+ ### Flow 1: Direct swap via UniversalRouter
958
+
959
+ Use this when a user wants to swap tokens directly (not through the PAFI Relay).
960
+
961
+ ```ts
962
+ import { PafiSDK } from "@pafi/core";
963
+ import { UNIVERSAL_ROUTER_ADDRESSES } from "@pafi/core";
964
+ import { universalRouterAbi } from "@pafi/core/abi";
965
+
966
+ const sdk = new PafiSDK({
967
+ chainId: 8453,
968
+ rpcUrl: "https://base-mainnet.g.alchemy.com/v2/...",
969
+ signer: walletClient,
970
+ });
971
+
972
+ // 1. Quote — discovers all routes and picks the best via multicall
973
+ const { bestRoute, allRoutes } = await sdk.findBestQuote(
974
+ tokenIn, // e.g. WETH address
975
+ USDC, // output token
976
+ parseEther("1"), // input amount
977
+ myPools, // optional: your own pools (merged with COMMON_POOLS)
978
+ );
979
+
980
+ console.log(`Best route: ${formatUnits(bestRoute.amountOut, 6)} USDC`);
981
+ console.log(`Found ${allRoutes.length} valid routes`);
982
+
983
+ // 2. Build swap calldata — caller decides minAmountOut (slippage)
984
+ const minReceive = bestRoute.amountOut * 99n / 100n; // 1% slippage
985
+ const { commands, inputs } = sdk.buildSwapFromQuote({
986
+ quote: bestRoute,
987
+ currencyIn: tokenIn,
988
+ currencyOut: USDC,
989
+ amountIn: parseEther("1"),
990
+ minAmountOut: minReceive,
991
+ });
992
+
993
+ // 3. Approve (one-time): token → Permit2 → UniversalRouter
994
+ // ERC20.approve(PERMIT2, MAX)
995
+ // Permit2.approve(token, UNIVERSAL_ROUTER, MAX, MAX_EXPIRY)
996
+
997
+ // 4. Execute swap
998
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 1800);
999
+ await walletClient.writeContract({
1000
+ address: UNIVERSAL_ROUTER_ADDRESSES[8453],
1001
+ abi: universalRouterAbi,
1002
+ functionName: "execute",
1003
+ args: [commands, inputs, deadline],
1004
+ });
1005
+ ```
1006
+
1007
+ ### Flow 2: Mint-and-swap via Relay (the PAFI cash-out flow)
1008
+
1009
+ The frontend signs, the issuer backend submits. This is the primary PAFI flow.
1010
+
1011
+ #### Frontend (signs + sends to backend)
1012
+
1013
+ The SDK builds the EIP-712 typed data object. You can sign it with any signer:
1014
+ - **viem WalletClient** — use `sdk.signReceiverConsent()` directly
1015
+ - **Privy / WalletConnect / external** — use `sdk.buildReceiverConsentTypedData()` to get the typed data, then pass it to the external signer's `signTypedData`
1016
+
1017
+ ```ts
1018
+ import { PafiSDK } from "@pafi/core";
1019
+
1020
+ const sdk = new PafiSDK({
1021
+ chainId: 8453,
1022
+ rpcUrl: "https://base-mainnet.g.alchemy.com/v2/...",
1023
+ pointTokenAddress: "0xPointToken...",
1024
+ // signer is optional — not needed if using Privy/external signer
1025
+ });
1026
+
1027
+ // 1. Quote
1028
+ const { bestRoute } = await sdk.findBestQuote(
1029
+ pointTokenAddress, USDC, amount, pointTokenPools,
1030
+ );
1031
+
1032
+ // 2. Set swap terms
1033
+ const minReceive = bestRoute.amountOut * 99n / 100n;
1034
+ const feeInUsdt = 1_000_000n; // 1 USDC relayer fee
1035
+ const extData = sdk.encodeExtData(minReceive, feeInUsdt);
1036
+
1037
+ // 3. Read nonce + build typed data
1038
+ const nonce = await sdk.getReceiverConsentNonce(userAddress);
1039
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 1800);
1040
+ const consentMessage = {
1041
+ onBehalfOf: relayAddress,
1042
+ originalReceiver: userAddress,
1043
+ amount,
1044
+ nonce,
1045
+ deadline,
1046
+ extData,
1047
+ };
1048
+
1049
+ // 4a. Sign with Privy (or any external signer)
1050
+ const typedData = await sdk.buildReceiverConsentTypedData(consentMessage);
1051
+ const receiverSig = await privy.signTypedData(typedData);
1052
+ // receiverSig is a 0x-prefixed hex string (65 bytes)
1053
+
1054
+ // 4b. Or sign with viem WalletClient (if signer is set on SDK)
1055
+ // const { serialized: receiverSig } = await sdk.signReceiverConsent(consentMessage);
1056
+
1057
+ // 5. Send to issuer backend (HTTP — not part of @pafi/core)
1058
+ await fetch(`${ISSUER_URL}/claim-and-swap`, {
1059
+ method: "POST",
1060
+ headers: { "Content-Type": "application/json" },
1061
+ body: JSON.stringify({
1062
+ receiver: userAddress,
1063
+ amount: amount.toString(),
1064
+ deadline: deadline.toString(),
1065
+ receiverSig,
1066
+ path: bestRoute.path,
1067
+ minAmountOut: minReceive.toString(),
1068
+ feeInUsdt: feeInUsdt.toString(),
1069
+ }),
1070
+ });
1071
+ ```
1072
+
1073
+ #### Issuer backend (signs + submits tx)
1074
+
1075
+ The backend can sign with a viem WalletClient, or use `buildMintRequestTypedData`
1076
+ to get the typed data for an external signer (HSM/KMS).
1077
+
1078
+ ```ts
1079
+ import { PafiSDK } from "@pafi/core";
1080
+ import { relayAbi } from "@pafi/core/abi";
1081
+
1082
+ const sdk = new PafiSDK({
1083
+ chainId: 8453,
1084
+ rpcUrl: "...",
1085
+ pointTokenAddress: "0xPointToken...",
1086
+ // signer is optional — not needed if using HSM/KMS via buildMintRequestTypedData
1087
+ });
1088
+
1089
+ // 1. Issuer signs MintRequest
1090
+ const mintNonce = await sdk.getMintRequestNonce(receiver);
1091
+ const mintMessage = { to: receiver, amount, nonce: mintNonce, deadline };
1092
+
1093
+ // Option A: Sign with viem WalletClient (set signer on SDK)
1094
+ // const { serialized: minterSig } = await sdk.signMintRequest(mintMessage);
1095
+
1096
+ // Option B: Build typed data for external signer (HSM/KMS/Privy)
1097
+ const typedData = await sdk.buildMintRequestTypedData(mintMessage);
1098
+ const minterSig = await externalSigner.signTypedData(typedData);
1099
+
1100
+ // 2. Build Relay params from the frontend's quote path
1101
+ const { swapParams, extData } = sdk.buildRelaySwapParams({
1102
+ quote: { path: requestBody.path },
1103
+ minAmountOut: BigInt(requestBody.minAmountOut),
1104
+ feeInUsdt: BigInt(requestBody.feeInUsdt),
1105
+ deadline: BigInt(requestBody.deadline),
1106
+ });
1107
+
1108
+ // 3. Submit to Relay contract
1109
+ await relayerWallet.writeContract({
1110
+ address: relayAddress,
1111
+ abi: relayAbi,
1112
+ functionName: "mintAndSwap",
1113
+ args: [
1114
+ {
1115
+ pointToken: pointTokenAddress,
1116
+ receiver,
1117
+ amount,
1118
+ deadline,
1119
+ minterSig,
1120
+ receiverSig: requestBody.receiverSig,
1121
+ extData,
1122
+ },
1123
+ swapParams,
1124
+ ],
1125
+ });
1126
+ ```
1127
+
1128
+ ---
1129
+
1130
+ ## Development
1131
+
1132
+ This package is part of the `pafi-sdk` monorepo. To work on it in isolation:
1133
+
1134
+ ```bash
1135
+ cd packages/core
1136
+ pnpm build # Compile TypeScript with tsup (ESM + CJS dual output)
1137
+ pnpm test # Run vitest test suite
1138
+ pnpm typecheck # Type-check without emitting (tsc --noEmit)
1139
+ ```
1140
+
1141
+ From the monorepo root:
1142
+
1143
+ ```bash
1144
+ pnpm build # Build all packages
1145
+ pnpm test # Test all packages
1146
+ pnpm typecheck # Type-check all packages
1147
+ ```
1148
+
1149
+ ### Output
1150
+
1151
+ `tsup` produces dual ESM + CJS output under `dist/`. Each sub-path export (`./eip712`, `./relay`, etc.) has its own entry in the bundle with full type declarations.
1152
+
1153
+ ---
1154
+
1155
+ ## License
1156
+
1157
+ UNLICENSED