@pafi-dev/trading
v0.11.2
Published
Stateless on-chain trading handlers for PAFI — swap, quote, perp deposit
Readme
@pafi-dev/trading
On-chain trading for PAFI: V3 swap quote + UserOp build, Orderly perp deposit. Direction-agnostic — quote and swap any ERC-20 → ERC-20 routable through PAFI's Uniswap V3 pools.
Browser + Node-safe. Stateless (no DB, no signer, no auth).
Peer-deps: viem ^2. Plus @pafi-dev/core for primitives.
PAFI's PT pools are standard Uniswap V3 (no hooks). USDT ↔ PT swaps incur only the standard LP fee on top of the SDK's operator fee.
Why this exists
Issuer backends (@pafi-dev/issuer) cover issuer-signed flows (claim,
redeem). Trading is the FE-callable surface for actions that don't need
an issuer signature:
- Quote PT → USDT, USDT → PT, PT0 → PT1 — pure on-chain V3 QuoterV2 read, multicall'd.
- Build swap UserOp with operator gas-reimbursement fee in the input token (auto-quoted via Chainlink + V3 subgraph). Sponsored + fallback variants.
- Build perp deposit UserOp via the PAFI Orderly Relay (Relay
covers LayerZero
msg.value, user pays USDC fee).
Stateless and HTTP-free — issuer backends or FE apps that want a
server-canonical quote/swap can wrap TradingHandlers directly.
Installation
pnpm add @pafi-dev/trading @pafi-dev/core viemMake sure to install @pafi-dev/core explicitly at top-level
to avoid pnpm deduping into a nested copy with stale URLs/addresses.
Quick start (FE)
import { createPublicClient, http } from "viem";
import {
TradingHandlers,
fetchPafiPools,
} from "@pafi-dev/trading";
import { getContractAddresses } from "@pafi-dev/core";
const provider = createPublicClient({ transport: http("https://mainnet.base.org") });
const trading = new TradingHandlers({ provider, chainId: 8453 });
const { usdt, usdc } = getContractAddresses(8453); // usdc is optional, used by perp-deposit fee path
// Pool discovery — loop call when both sides are PT (PT0 → PT1 multi-hop).
const pools = await fetchPafiPools(8453, POINT_TOKEN);
// Quote — direction-agnostic.
const q = await trading.handleQuote({
chainId: 8453,
inputTokenAddress: POINT_TOKEN,
outputTokenAddress: usdt,
amount: 100n * 10n ** 18n, // 100 PT
pools,
});
// q.estimatedOutputAmount — bigint USDT raw (6 dec)
// Swap — builds UserOp ready to submit via Bundler + EIP-7702.
const swap = await trading.handleSwap(userAddress, {
chainId: 8453,
userAddress,
inputTokenAddress: POINT_TOKEN,
outputTokenAddress: usdt,
amount: 100n * 10n ** 18n,
aaNonce,
pools,
// gasFeeAmount auto-quotes; pass explicit bigint to override.
// slippageBps auto-picks 50 bps single-hop / 100 bps multi-hop.
});
// swap.userOp + swap.userOpFallback (when fee > 0)
// swap.hops, swap.estimatedOutputAmount, swap.minAmountOutDirection matrix
| Direction | Use case | Hops | Operator fee token |
| --- | --- | --- | --- |
| PT → USDT | Cashout | 1 | USDT (output) |
| USDT → PT | Buy PT | 1 | PT (output) |
| PT0 → PT1 | Multi-token swap | 2 (via USDT) | output PT |
Pool fee depends on which V3 tier the pool was created at (typically 0.05% or 0.3% — COMMON_POOLS carries both). findBestQuote picks the route with the best amountOut across all tiers.
Cross-issuer OK for PT0 → PT1 (e.g. TPT ↔ LTR if both issuers ship
V3 pools with USDT). The router auto-routes through USDT.
Operator fee strategy
The operator gas-reimbursement fee is charged in the output token.
User receives (net - operatorFee) instead of net after the swap.
| Output | Auto-quote source | Caller override |
| --- | --- | --- |
| PT | quoteOperatorFeePt (Chainlink + V3 subgraph) | gasFeeAmountOutput: bigint |
| USDT | quoteOperatorFeeUsdt (Chainlink only) | gasFeeAmountOutput: bigint |
// USDT-output swap (e.g. PT → USDT cashout)
const swap = await trading.handleSwap(user, {
// ...
inputTokenAddress: PT,
outputTokenAddress: USDT,
// gasFeeAmountOutput auto-quotes ~$0.008 USDT
});
// Or override:
const swap = await trading.handleSwap(user, {
// ...
gasFeeAmountOutput: 10_000n, // 0.01 USDT explicit
});Multi-hop PT → PT1
import { fetchPafiPools, TradingHandlers } from "@pafi-dev/trading";
// Loop call — fetch pools for both PTs, merge.
const [poolsA, poolsB] = await Promise.all([
fetchPafiPools(8453, POINT_TOKEN_0),
fetchPafiPools(8453, POINT_TOKEN_1),
]);
const pools = [...poolsA, ...poolsB];
const swap = await trading.handleSwap(user, {
chainId: 8453,
userAddress,
inputTokenAddress: POINT_TOKEN_0,
outputTokenAddress: POINT_TOKEN_1,
amount: 100n * 10n ** 18n,
aaNonce,
pools,
});
console.log(swap.hops); // 2 — went through USDTV3 router auto-picks the best route across pools + COMMON_POOLS
(default maxHops=3). Slippage auto-bumps to 100 bps for multi-hop.
Verify via subgraph that both PTs have indexed pools with USDT:
{ pafiTokens { id pool { token0 { id symbol } token1 { id symbol } } } }Perp deposit
const deposit = await trading.handlePerpDeposit({
chainId: 8453,
userAddress,
amount: 1_000_000n, // 1 USDC (6 dec)
aaNonce,
brokerId: "orderly", // "orderly" | "woofi_pro" | "logx"
// viaRelay: true (default) — uses PAFI Orderly Relay (no msg.value needed)
pointTokenAddress: POINT_TOKEN, // optional — for PT operator fee
});
// deposit.userOp — submit via Bundler + Paymaster
// deposit.path — "relay" (preferred) or "vault" (direct, requires native ETH)The PAFI Relay covers LayerZero msg.value from its ETH reserve and
charges a USDC fee (Relay.quoteTokenFee). User wallet doesn't need
native ETH.
Standalone primitives
For callers that want to compose flows beyond TradingHandlers:
import {
buildSwapUserOp,
buildSwapUserOpExactOut,
findBestQuote,
findBestQuoteExactOut,
simulateSwap,
buildUniversalRouterExecuteArgs,
buildUniversalRouterExecuteArgsExactOut,
buildV3SwapInputExactIn,
buildV3SwapInputExactOut,
buildPermit2ApprovalCalldata,
buildErc20ApprovalCalldata,
checkAllowance,
fetchPafiPools,
V3_SWAP_EXACT_IN,
V3_SWAP_EXACT_OUT,
} from "@pafi-dev/trading";Use these to build custom direction-agnostic swap flows or pre-flight simulations.
buildSwapFromQuoteis also re-exported but@deprecated— passbestRoute.pathfromfindBestQuote(...)directly intobuildUniversalRouterExecuteArgs(...)instead. Will be removed in a future minor.
Direct path — no AA, no bundler, no sponsor-relayer
Trading ships swapDirect() + perpDepositDirect() for the FE-only
flow where the user pays gas in ETH and broadcasts a single type-2 tx
calling its own EIP-7702 delegated bytecode. No Pimlico API key, no
sponsor-relayer, no PAFI infra.
Precondition: user EOA must already be EIP-7702 delegated (run
delegateDirect() from @pafi-dev/core first; both helpers throw a
clear error otherwise).
swapDirect
import { swapDirect, fetchPafiPools } from "@pafi-dev/trading";
import { createPublicClient, createWalletClient, custom, http, parseUnits } from "viem";
import { base } from "viem/chains";
const publicClient = createPublicClient({ chain: base, transport: http() });
const walletClient = createWalletClient({
account: wallet.address,
chain: base,
transport: custom(await wallet.getEthereumProvider()),
});
const pools = await fetchPafiPools(8453, POINT_TOKEN);
const result = await swapDirect({
userAddress: wallet.address,
chainId: 8453,
inputTokenAddress: POINT_TOKEN,
outputTokenAddress: USDT,
amount: parseUnits("100", 18),
pools,
publicClient,
walletClient,
// optional: slippageBps, deadline, gasFeeAmountOutput, waitForReceipt
});
// {
// txHash, receipt?, estimatedOutputAmount, minAmountOut, hops,
// deadline, feeAmountUsed
// }Operator fee: default 0n (skip — PAFI not sponsoring this swap).
Pass an explicit gasFeeAmountOutput: bigint only if you want to mirror
the sponsored flow's fee transfer.
perpDepositDirect
import { perpDepositDirect } from "@pafi-dev/trading";
const result = await perpDepositDirect({
userAddress: wallet.address,
chainId: 8453,
amount: parseUnits("10", 6), // 10 USDC
brokerId: "orderly", // | "woofi_pro" | "logx"
publicClient,
walletClient,
// optional: maxRelayFee, gasFeeUsdc, waitForReceipt
});
// {
// txHash, receipt?, relayTokenFee, maxFee, netDeposit,
// feeAmountUsed, accountId, brokerHash, usdcAddress, relayAddress
// }The Relay still charges its own USDC fee (Relay.quoteTokenFee) to
cover LayerZero msg.value; separate from PAFI's operator fee
(skipped by default on direct path).
When to use direct vs. AA path
| Aspect | swapDirect / perpDepositDirect | TradingHandlers.handleSwap / handlePerpDeposit |
| --- | --- | --- |
| User pays gas | ETH (native) | Free (sponsor) or ETH (fallback) |
| Bundler required | No | Yes (Pimlico) |
| Sponsor-relayer required | No | Yes (sponsored path) |
| Operator fee charged | Default skip | Default included |
| Round trips | 1 (broadcast tx) | 2+ (paymaster + bundler) |
| Precondition | EIP-7702 delegated | Same |
Use direct for FE dev/test, integrations without a Pimlico key, or production paths where users explicitly opt to pay gas. Use AA path for the production gas-free UX via sponsor-relayer.
Plain Swap (Permit2 sig — FE only, no AA)
For external wallets (MetaMask) that can't EIP-7702 delegate. Standard
Uniswap pattern: ERC-20 approve to PERMIT2, off-chain PermitSingle sig,
UR.execute([PERMIT2_PERMIT, V3_SWAP_EXACT_IN], ...).
// Step 1: one-time ERC-20 approve to Permit2 (skip if allowance set)
// Step 2: sign Permit2 PermitSingle EIP-712 (skip if sub-allowance valid)
// Step 3: send UR.execute tx — user pays gas in ETH
// See privy-pimlico-eip7702-example/app/components/WalletPanel.tsx
// handleSwapPlain for the full flow.Cold start: 1 tx + 1 sig + 1 tx (3 popups). Warm (sub-allowance fresh): 1 tx.
⚠️ Common bug:
permitExpiration = (1 << 48) - 1overflows to65535in JS (int32 shift). Use2 ** 48 - 1or BigInt for uint48 timestamps.
API reference
TradingHandlers
new TradingHandlers({ provider: PublicClient, chainId: number })Methods:
| Method | Purpose |
| --- | --- |
| handleQuote({ inputTokenAddress, outputTokenAddress, amount, pools? }) | V3 exact-input quote. Returns { inputAmount, estimatedOutputAmount, gasEstimate, quoteError? } |
| handleQuoteExactOut({ ... }) | V3 exact-output quote |
| handleSwap(authedAddr, { userAddress, inputTokenAddress, outputTokenAddress, amount, aaNonce, pools?, gasFeeAmountOutput?, slippageBps? }) | Build UserOp. Returns { userOp, userOpFallback?, estimatedOutputAmount, minAmountOut, hops, deadline, feeAmountUsed, feeRecipient } |
| handleSwapExactOut(authedAddr, { ... }) | Build exact-output swap UserOp |
| handlePerpDeposit({ userAddress, amount, aaNonce, brokerId, viaRelay?, maxRelayFee?, pointTokenAddress?, gasFeePt? }) | Build perp deposit UserOp |
Free functions
findBestQuote(client, chainId, tokenIn, tokenOut, amount, pools, quoterAddress?, maxHops?)findBestQuoteExactOut(...)quoteBestRoute,quoteBestRouteExactOut,quoteExactInput,quoteExactInputSingle,quoteExactOutput,quoteExactOutputSinglebuildSwapUserOp(params)/buildSwapUserOpExactOut(params)— direction-agnostic UserOp builderbuildUniversalRouterExecuteArgs,buildUniversalRouterExecuteArgsExactOut,buildV3SwapInputExactIn,buildV3SwapInputExactOut(and the@deprecatedbuildSwapFromQuotealias)buildPermit2ApprovalCalldata,buildErc20ApprovalCalldata,checkAllowancesimulateSwap—eth_calldry-runfetchPafiPools(chainId, pointTokenAddress, subgraphUrl?)— re-exported from coreswapDirect(params),perpDepositDirect(params)— FE-direct flowsV3_SWAP_EXACT_IN,V3_SWAP_EXACT_OUT— Universal Router V3 command constants
References
- Fee math: see
@pafi-dev/coreREADME — Operator fee quoter - Architecture:
ARCHITECTURE.mdat SDK root
License
Apache-2.0
