@pafi-dev/trading 0.4.2 → 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
@@ -3,25 +3,29 @@
3
3
  [![npm](https://img.shields.io/npm/v/@pafi-dev/trading)](https://www.npmjs.com/package/@pafi-dev/trading)
4
4
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
5
5
 
6
- On-chain trading for PAFI: V4 swap quote + UserOp build, Orderly
7
- perp deposit. **Direction-agnostic** since 0.2.0 — quote and swap any
8
- ERC-20 → ERC-20 routable through PAFI's V4 pools.
6
+ On-chain trading for PAFI: V3 swap quote + UserOp build, Orderly
7
+ perp deposit. **Direction-agnostic** — quote and swap any
8
+ ERC-20 → ERC-20 routable through PAFI's Uniswap V3 pools.
9
9
 
10
- **Browser + Node-safe.** Stateless (no DB, no signer, no auth). Peer-deps:
11
- `viem ^2`. Plus `@pafi-dev/core` for primitives.
10
+ **Browser + Node-safe.** Stateless (no DB, no signer, no auth).
11
+ Peer-deps: `viem ^2`. Plus `@pafi-dev/core` for primitives.
12
+
13
+ > PAFI's PT pools are standard Uniswap V3 (no hooks). USDT ↔ PT swaps
14
+ > incur only the standard LP fee on top of the SDK's operator fee.
12
15
 
13
16
  ---
14
17
 
15
18
  ## Why this exists
16
19
 
17
- Issuer backends (`@pafi-dev/issuer`) cover the issuer-signed flows
18
- (claim, redeem). Trading is the FE-callable surface for actions that
19
- don't need an issuer signature:
20
+ Issuer backends (`@pafi-dev/issuer`) cover issuer-signed flows (claim,
21
+ redeem). Trading is the FE-callable surface for actions that don't need
22
+ an issuer signature:
20
23
 
21
- - **Quote** PT → USDT, USDT → PT, PT0 → PT1 — pure on-chain V4 Quoter
24
+ - **Quote** PT → USDT, USDT → PT, PT0 → PT1 — pure on-chain V3 QuoterV2
22
25
  read, multicall'd.
23
26
  - **Build swap UserOp** with operator gas-reimbursement fee in the
24
- input token (auto-quoted via Chainlink). Sponsored + fallback variants.
27
+ input token (auto-quoted via Chainlink + V3 subgraph). Sponsored +
28
+ fallback variants.
25
29
  - **Build perp deposit UserOp** via the PAFI Orderly Relay (Relay
26
30
  covers LayerZero `msg.value`, user pays USDC fee).
27
31
 
@@ -36,6 +40,9 @@ server-canonical quote/swap can wrap `TradingHandlers` directly.
36
40
  pnpm add @pafi-dev/trading @pafi-dev/core viem
37
41
  ```
38
42
 
43
+ Make sure to install **`@pafi-dev/core`** explicitly at top-level
44
+ to avoid pnpm deduping into a nested copy with stale URLs/addresses.
45
+
39
46
  ---
40
47
 
41
48
  ## Quick start (FE)
@@ -66,7 +73,7 @@ const q = await trading.handleQuote({
66
73
  // q.estimatedOutputAmount — bigint USDT raw (6 dec)
67
74
 
68
75
  // Swap — builds UserOp ready to submit via Bundler + EIP-7702.
69
- const swap = await trading.handleSwap({
76
+ const swap = await trading.handleSwap(userAddress, {
70
77
  chainId: 8453,
71
78
  userAddress,
72
79
  inputTokenAddress: POINT_TOKEN,
@@ -74,42 +81,53 @@ const swap = await trading.handleSwap({
74
81
  amount: 100n * 10n ** 18n,
75
82
  aaNonce,
76
83
  pools,
77
- // gasFeeAmount auto-quotes when input is a PT; pass explicit bigint to override.
84
+ // gasFeeAmount auto-quotes; pass explicit bigint to override.
78
85
  // slippageBps auto-picks 50 bps single-hop / 100 bps multi-hop.
79
86
  });
80
87
  // swap.userOp + swap.userOpFallback (when fee > 0)
81
- // swap.hops number of hops in chosen route
88
+ // swap.hops, swap.estimatedOutputAmount, swap.minAmountOut
82
89
  ```
83
90
 
84
91
  ---
85
92
 
86
93
  ## Direction matrix
87
94
 
88
- | Direction | Use case | Hop count | Hook fee | Operator fee token |
95
+ | Direction | Use case | Hop count | Fee | Operator fee token |
89
96
  | --- | --- | --- | --- | --- |
90
- | `PT → USDT` | Cashout | 1 | 10% on this leg | PT |
91
- | `USDT → PT` | Buy PT | 1 | 0% | USDT |
92
- | `PT0 → PT1` | Same-issuer multi-token swap | 2 (via USDT) | 10% on PT0 leg only | PT0 |
93
-
94
- **Same-issuer only** for PT0 → PT1 — caller is responsible for
95
- ensuring both PTs belong to the same issuer. SDK doesn't enforce.
97
+ | `PT → USDT` | Cashout | 1 | 0.3% pool fee | USDT (output) |
98
+ | `USDT → PT` | Buy PT | 1 | 0.3% pool fee | PT (output) |
99
+ | `PT0 → PT1` | Multi-token swap | 2 (via USDT) | 0.3% × 2 hops | output PT |
96
100
 
97
- The PAFI V4 hook charges 10% on PT USDT direction at pool level
98
- (applied inside `UniversalRouter.execute`). Reflected in
99
- `estimatedOutputAmount` from `findBestQuote`.
101
+ **Cross-issuer OK** for PT0 PT1 (e.g. TPT LTR if both issuers ship
102
+ V3 pools with USDT). The router auto-routes through `USDT`.
100
103
 
101
104
  ---
102
105
 
103
106
  ## Operator fee strategy
104
107
 
105
- The operator gas-reimbursement fee is charged in the **input token**
106
- (0.2.0+). Single approve covers `amountIn + gasFeeAmount`. User only
107
- needs to hold a single token.
108
+ The operator gas-reimbursement fee is charged in the **output token**.
109
+ User receives `(net - operatorFee)` instead of `net` after the swap.
108
110
 
109
- | Input | Auto-quote source | Caller needs to override? |
111
+ | Output | Auto-quote source | Caller override |
110
112
  | --- | --- | --- |
111
- | PT | `quoteOperatorFeePt` (Chainlink + V4 subgraph) | No |
112
- | USDT | falls back to 0 — pass `gasFeeAmount` explicitly | Yes (use `quoteOperatorFeeUsdt`) |
113
+ | PT | `quoteOperatorFeePt` (Chainlink + V3 subgraph) | `gasFeeAmountOutput: bigint` |
114
+ | USDT | `quoteOperatorFeeUsdt` (Chainlink only) | `gasFeeAmountOutput: bigint` |
115
+
116
+ ```ts
117
+ // USDT-output swap (e.g. PT → USDT cashout)
118
+ const swap = await trading.handleSwap(user, {
119
+ // ...
120
+ inputTokenAddress: PT,
121
+ outputTokenAddress: USDT,
122
+ // gasFeeAmountOutput auto-quotes ~$0.008 USDT
123
+ });
124
+
125
+ // Or override:
126
+ const swap = await trading.handleSwap(user, {
127
+ // ...
128
+ gasFeeAmountOutput: 10_000n, // 0.01 USDT explicit
129
+ });
130
+ ```
113
131
 
114
132
  ---
115
133
 
@@ -125,7 +143,7 @@ const [poolsA, poolsB] = await Promise.all([
125
143
  ]);
126
144
  const pools = [...poolsA, ...poolsB];
127
145
 
128
- const swap = await trading.handleSwap({
146
+ const swap = await trading.handleSwap(user, {
129
147
  chainId: 8453,
130
148
  userAddress,
131
149
  inputTokenAddress: POINT_TOKEN_0,
@@ -138,9 +156,14 @@ const swap = await trading.handleSwap({
138
156
  console.log(swap.hops); // 2 — went through USDT
139
157
  ```
140
158
 
141
- V4 router auto-picks the best route across `pools + COMMON_POOLS`
159
+ V3 router auto-picks the best route across `pools + COMMON_POOLS`
142
160
  (default `maxHops=3`). Slippage auto-bumps to 100 bps for multi-hop.
143
161
 
162
+ Verify via subgraph that both PTs have indexed pools with USDT:
163
+ ```graphql
164
+ { pafiTokens { id pool { token0 { id symbol } token1 { id symbol } } } }
165
+ ```
166
+
144
167
  ---
145
168
 
146
169
  ## Perp deposit
@@ -149,9 +172,9 @@ V4 router auto-picks the best route across `pools + COMMON_POOLS`
149
172
  const deposit = await trading.handlePerpDeposit({
150
173
  chainId: 8453,
151
174
  userAddress,
152
- amount: 1_000_000n, // 1 USDC (6 dec)
175
+ amount: 1_000_000n, // 1 USDC (6 dec)
153
176
  aaNonce,
154
- brokerId: "orderly", // "orderly" | "woofi_pro" | "logx"
177
+ brokerId: "orderly", // "orderly" | "woofi_pro" | "logx"
155
178
  // viaRelay: true (default) — uses PAFI Orderly Relay (no msg.value needed)
156
179
  pointTokenAddress: POINT_TOKEN, // optional — for PT operator fee
157
180
  });
@@ -168,17 +191,26 @@ native ETH.
168
191
 
169
192
  ## Standalone primitives
170
193
 
171
- For callers that want to compose flows beyond what `TradingHandlers`
172
- provides:
194
+ For callers that want to compose flows beyond `TradingHandlers`:
173
195
 
174
196
  ```ts
175
197
  import {
176
198
  buildSwapUserOp,
199
+ buildSwapUserOpExactOut,
177
200
  findBestQuote,
201
+ findBestQuoteExactOut,
178
202
  simulateSwap,
179
203
  buildUniversalRouterExecuteArgs,
204
+ buildUniversalRouterExecuteArgsExactOut,
205
+ buildV3SwapInputExactIn,
206
+ buildV3SwapInputExactOut,
207
+ buildSwapFromQuote,
180
208
  buildPermit2ApprovalCalldata,
209
+ buildErc20ApprovalCalldata,
210
+ checkAllowance,
181
211
  fetchPafiPools,
212
+ V3_SWAP_EXACT_IN,
213
+ V3_SWAP_EXACT_OUT,
182
214
  } from "@pafi-dev/trading";
183
215
  ```
184
216
 
@@ -189,7 +221,7 @@ simulations.
189
221
 
190
222
  ## Direct path — no AA, no bundler, no sponsor-relayer
191
223
 
192
- `v0.4.0+` ships `swapDirect()` + `perpDepositDirect()` for the FE-only
224
+ Trading ships `swapDirect()` + `perpDepositDirect()` for the FE-only
193
225
  flow where the user pays gas in ETH and broadcasts a single type-2 tx
194
226
  calling its own EIP-7702 delegated bytecode. No Pimlico API key, no
195
227
  sponsor-relayer, no PAFI infra.
@@ -206,7 +238,6 @@ import { createPublicClient, createWalletClient, custom, http, parseUnits } from
206
238
  import { base } from "viem/chains";
207
239
 
208
240
  const publicClient = createPublicClient({ chain: base, transport: http() });
209
- // `walletClient` from Privy embedded wallet, MetaMask, etc.
210
241
  const walletClient = createWalletClient({
211
242
  account: wallet.address,
212
243
  chain: base,
@@ -227,7 +258,6 @@ const result = await swapDirect({
227
258
  // optional: slippageBps, deadline, gasFeeAmountOutput, waitForReceipt
228
259
  });
229
260
 
230
- console.log("Swap tx:", result.txHash);
231
261
  // {
232
262
  // txHash, receipt?, estimatedOutputAmount, minAmountOut, hops,
233
263
  // deadline, feeAmountUsed
@@ -260,8 +290,8 @@ const result = await perpDepositDirect({
260
290
  ```
261
291
 
262
292
  The Relay still charges its own USDC fee (`Relay.quoteTokenFee`) to
263
- cover LayerZero `msg.value`; that's separate from PAFI's operator fee
264
- (which is skipped by default on the direct path).
293
+ cover LayerZero `msg.value`; separate from PAFI's operator fee
294
+ (skipped by default on direct path).
265
295
 
266
296
  ### When to use direct vs. AA path
267
297
 
@@ -280,6 +310,30 @@ the production gas-free UX via sponsor-relayer.
280
310
 
281
311
  ---
282
312
 
313
+ ## Plain Swap (Permit2 sig — FE only, no AA)
314
+
315
+ For external wallets (MetaMask) that can't EIP-7702 delegate. Standard
316
+ Uniswap pattern: ERC-20 approve to PERMIT2, off-chain PermitSingle sig,
317
+ `UR.execute([PERMIT2_PERMIT, V3_SWAP_EXACT_IN], ...)`.
318
+
319
+ ```ts
320
+ // Step 1: one-time ERC-20 approve to Permit2 (skip if allowance set)
321
+ // Step 2: sign Permit2 PermitSingle EIP-712 (skip if sub-allowance valid)
322
+ // Step 3: send UR.execute tx — user pays gas in ETH
323
+
324
+ // See privy-pimlico-eip7702-example/app/components/WalletPanel.tsx
325
+ // handleSwapPlain for the full flow.
326
+ ```
327
+
328
+ **Cold start**: 1 tx + 1 sig + 1 tx (3 popups).
329
+ **Warm** (sub-allowance fresh): 1 tx.
330
+
331
+ > ⚠️ Common bug: `permitExpiration = (1 << 48) - 1` overflows to
332
+ > `65535` in JS (int32 shift). Use `2 ** 48 - 1` or BigInt for
333
+ > uint48 timestamps.
334
+
335
+ ---
336
+
283
337
  ## API reference
284
338
 
285
339
  ### `TradingHandlers`
@@ -292,25 +346,32 @@ Methods:
292
346
 
293
347
  | Method | Purpose |
294
348
  | --- | --- |
295
- | `handleQuote({ inputTokenAddress, outputTokenAddress, amount, pools? })` | V4 quote. Returns `{ inputAmount, estimatedOutputAmount, gasEstimate, quoteError? }` |
296
- | `handleSwap({ userAddress, inputTokenAddress, outputTokenAddress, amount, aaNonce, pools?, gasFeeAmount?, slippageBps? })` | Build UserOp. Returns `{ userOp, userOpFallback?, estimatedOutputAmount, minAmountOut, hops, deadline, feeAmountUsed, feeRecipient }` |
297
- | `handlePerpDeposit({ userAddress, amount, aaNonce, brokerId, viaRelay?, maxRelayFee?, pointTokenAddress?, gasFeePt? })` | Build UserOp. Returns Orderly deposit response |
349
+ | `handleQuote({ inputTokenAddress, outputTokenAddress, amount, pools? })` | V3 exact-input quote. Returns `{ inputAmount, estimatedOutputAmount, gasEstimate, quoteError? }` |
350
+ | `handleQuoteExactOut({ ... })` | V3 exact-output quote |
351
+ | `handleSwap(authedAddr, { userAddress, inputTokenAddress, outputTokenAddress, amount, aaNonce, pools?, gasFeeAmountOutput?, slippageBps? })` | Build UserOp. Returns `{ userOp, userOpFallback?, estimatedOutputAmount, minAmountOut, hops, deadline, feeAmountUsed, feeRecipient }` |
352
+ | `handleSwapExactOut(authedAddr, { ... })` | Build exact-output swap UserOp |
353
+ | `handlePerpDeposit({ userAddress, amount, aaNonce, brokerId, viaRelay?, maxRelayFee?, pointTokenAddress?, gasFeePt? })` | Build perp deposit UserOp |
298
354
 
299
355
  ### Free functions
300
356
 
301
357
  - `findBestQuote(client, chainId, tokenIn, tokenOut, amount, pools, quoterAddress?, maxHops?)`
302
- - `quoteBestRoute`, `quoteExactInput`, `quoteExactInputSingle`
303
- - `buildAllPaths`, `combineRoutes`
304
- - `buildSwapUserOp(params)` — direction-agnostic UserOp builder
305
- - `buildUniversalRouterExecuteArgs`, `buildV4SwapInput`, `buildSwapFromQuote`
358
+ - `findBestQuoteExactOut(...)`
359
+ - `quoteBestRoute`, `quoteBestRouteExactOut`, `quoteExactInput`, `quoteExactInputSingle`, `quoteExactOutput`, `quoteExactOutputSingle`
360
+ - `buildSwapUserOp(params)` / `buildSwapUserOpExactOut(params)` — direction-agnostic UserOp builder
361
+ - `buildUniversalRouterExecuteArgs`, `buildUniversalRouterExecuteArgsExactOut`, `buildV3SwapInputExactIn`, `buildV3SwapInputExactOut`, `buildSwapFromQuote`
306
362
  - `buildPermit2ApprovalCalldata`, `buildErc20ApprovalCalldata`, `checkAllowance`
307
363
  - `simulateSwap` — `eth_call` dry-run
308
- - `fetchPafiPools(chainId, pointTokenAddress, subgraphUrl?)`
309
- - `swapDirect(params)` — FE-direct swap (no AA, user pays gas) — see "Direct path" section
310
- - `perpDepositDirect(params)` — FE-direct perp deposit (no AA, user pays gas)
364
+ - `fetchPafiPools(chainId, pointTokenAddress, subgraphUrl?)` — re-exported from core
365
+ - `swapDirect(params)`, `perpDepositDirect(params)` — FE-direct flows
366
+ - `V3_SWAP_EXACT_IN`, `V3_SWAP_EXACT_OUT`Universal Router V3 command constants
311
367
 
312
368
  ---
313
369
 
370
+ ## References
371
+
372
+ - Fee math: [`docs/FEE_FLOW.md`](../../../docs/FEE_FLOW.md)
373
+ - Architecture: [`ARCHITECTURE.md`](../../ARCHITECTURE.md) at SDK root
374
+
314
375
  ## License
315
376
 
316
377
  Apache-2.0