@pafi-dev/trading 0.1.7 → 0.1.8

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/dist/index.cjs CHANGED
@@ -141,7 +141,20 @@ var TradingHandlers = class {
141
141
  gasFeePt,
142
142
  feeRecipient: request.feeRecipient ?? userAddress
143
143
  });
144
- return { userOp, estimatedUsdtOut, minAmountOut, deadline };
144
+ const userOpFallback = gasFeePt > 0n ? (0, import_core.buildSwapWithGasDeduction)({
145
+ userAddress,
146
+ aaNonce: request.aaNonce,
147
+ pointTokenAddress,
148
+ outputTokenAddress: usdt,
149
+ universalRouterAddress: universalRouter,
150
+ amountIn: request.amount,
151
+ minAmountOut,
152
+ swapPath: quoteResult.bestRoute.path,
153
+ deadline,
154
+ gasFeePt: 0n,
155
+ feeRecipient: userAddress
156
+ }) : void 0;
157
+ return { userOp, userOpFallback, estimatedUsdtOut, minAmountOut, deadline };
145
158
  }
146
159
  // =========================================================================
147
160
  // POST /perp-deposit
@@ -254,8 +267,15 @@ var TradingHandlers = class {
254
267
  gasFeePt: request.gasFeePt,
255
268
  gasFeePtRecipient: request.gasFeePtRecipient
256
269
  });
270
+ const userOpFallback = request.gasFeePt && request.gasFeePt > 0n ? (0, import_core.buildPerpDepositViaRelay)({
271
+ userAddress,
272
+ aaNonce: request.aaNonce,
273
+ relayAddress,
274
+ request: relayRequest
275
+ }) : void 0;
257
276
  return {
258
277
  userOp: userOp2,
278
+ userOpFallback,
259
279
  path: "relay",
260
280
  layerZeroFee,
261
281
  relayTokenFee,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/api/handlers.ts","../src/pools.ts"],"sourcesContent":["export { TradingHandlers } from \"./api/handlers\";\nexport type { TradingHandlersConfig } from \"./api/handlers\";\n\nexport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiQuoteError,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./api/types\";\n\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"./pools\";\n","import { getAddress } from \"viem\";\nimport type { Address, PublicClient } from \"viem\";\nimport {\n findBestQuote,\n buildSwapWithGasDeduction,\n buildPerpDepositWithGasDeduction,\n buildPerpDepositViaRelay,\n ORDERLY_RELAY_ABI,\n getContractAddresses,\n UNIVERSAL_ROUTER_ADDRESSES,\n ORDERLY_VAULT_ABI,\n ORDERLY_VAULT_ADDRESSES,\n BROKER_HASHES,\n TOKEN_HASHES,\n computeAccountId,\n} from \"@pafi-dev/core\";\nimport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./types\";\n\nexport interface TradingHandlersConfig {\n provider: PublicClient;\n chainId: number;\n}\n\n/**\n * Framework-agnostic handlers for on-chain trading actions.\n *\n * All handlers are stateless — they need only a PublicClient for RPC\n * calls. No ledger, no signer, no DB. Issuers wrap these in their own\n * HTTP controllers (Express / NestJS / Hono / etc.) the same way they\n * wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.\n *\n * Example (NestJS):\n *\n * const trading = new TradingHandlers({ provider, chainId });\n *\n * // GET /quote\n * const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });\n *\n * // POST /swap\n * const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });\n *\n * // POST /perp-deposit\n * const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });\n */\nexport class TradingHandlers {\n private readonly provider: PublicClient;\n private readonly chainId: number;\n\n constructor(config: TradingHandlersConfig) {\n this.provider = config.provider;\n this.chainId = config.chainId;\n }\n\n // =========================================================================\n // GET /quote\n // =========================================================================\n\n /**\n * Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.\n *\n * Uses multicall to batch all candidate routes into a single RPC call.\n * Returns `quoteError: \"QUOTE_UNAVAILABLE\"` when no pool/path exists\n * rather than throwing, so callers can show a soft \"unavailable\" UI\n * state without 500-ing.\n */\n async handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);\n }\n if (request.amount === 0n) {\n return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const pools = request.pools ?? [];\n\n try {\n const best = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: best.bestRoute.amountOut,\n gasEstimate: best.bestRoute.gasEstimate,\n };\n } catch {\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: 0n,\n gasEstimate: 0n,\n quoteError: \"QUOTE_UNAVAILABLE\",\n };\n }\n }\n\n // =========================================================================\n // POST /swap\n // =========================================================================\n\n /**\n * Build a PT → USDT swap UserOp.\n *\n * Quotes the best route, applies slippage, then encodes a 4-step\n * batch: PT.approve → Permit2.approve → UniversalRouter.execute →\n * PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned\n * `PartialUserOperation`; caller attaches paymaster data + user\n * signature and submits to the Bundler.\n */\n async handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handleSwap: amount must be positive\");\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];\n if (!universalRouter) {\n throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);\n }\n\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const userAddress = getAddress(request.userAddress);\n const pools = request.pools ?? [];\n const slippageBps = request.slippageBps ?? 50;\n const gasFeePt = request.gasFeePt ?? 0n;\n\n if (gasFeePt > 0n && !request.feeRecipient) {\n throw new Error(\"handleSwap: feeRecipient required when gasFeePt > 0\");\n }\n\n let quoteResult: Awaited<ReturnType<typeof findBestQuote>>;\n try {\n quoteResult = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n } catch {\n throw new Error(\"handleSwap: no swap path found for this point token\");\n }\n\n const estimatedUsdtOut = quoteResult.bestRoute.amountOut;\n const minAmountOut = (estimatedUsdtOut * BigInt(10000 - slippageBps)) / 10000n;\n const deadline = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);\n\n const userOp = buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt,\n feeRecipient: request.feeRecipient ?? userAddress,\n });\n\n return { userOp, estimatedUsdtOut, minAmountOut, deadline };\n }\n\n // =========================================================================\n // POST /perp-deposit\n // =========================================================================\n\n /**\n * Build an Orderly perp deposit UserOp.\n *\n * Default path is the **PAFI Orderly Relay** (`viaRelay: true`):\n * USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH\n * reserve and pays Orderly's LayerZero `msg.value` out of it; the\n * user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.\n * No native ETH on the user wallet is required, so paymaster\n * sponsorship of the ERC-4337 gas is sufficient end-to-end.\n *\n * Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.\n * Reserved for chains where no Relay is deployed — the user wallet\n * **must** hold `layerZeroFee` as native ETH.\n *\n * The Relay path automatically falls back to Vault when\n * `getContractAddresses(chainId).orderlyRelay` is the placeholder\n * sentinel (Relay not deployed for that chain).\n */\n async handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handlePerpDeposit: amount must be positive\");\n }\n\n const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];\n if (!vault) {\n throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);\n }\n\n const brokerHash = BROKER_HASHES[request.brokerId as keyof typeof BROKER_HASHES];\n if (!brokerHash) {\n throw new Error(`handlePerpDeposit: unknown brokerId \"${request.brokerId}\"`);\n }\n const tokenHash = TOKEN_HASHES.USDC;\n const userAddress = getAddress(request.userAddress);\n\n const [usdcAddress, brokerAllowed] = await Promise.all([\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedToken\",\n args: [tokenHash],\n }) as Promise<Address>,\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedBroker\",\n args: [brokerHash],\n }) as Promise<boolean>,\n ]);\n\n if (!brokerAllowed) {\n throw new Error(\n `handlePerpDeposit: broker \"${request.brokerId}\" is not whitelisted on Orderly Vault`,\n );\n }\n\n const accountId = computeAccountId(userAddress, brokerHash);\n const depositData = {\n accountId,\n brokerHash,\n tokenHash,\n tokenAmount: request.amount,\n };\n\n // Always read layerZeroFee for response — even on the Relay path\n // it's useful informational output (lets the FE show \"Relay\n // covers ~X ETH for you\").\n const layerZeroFee = (await this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getDepositFee\",\n args: [userAddress, depositData],\n })) as bigint;\n\n const useRelay = request.viaRelay !== false;\n const relayAddress = getContractAddresses(request.chainId).orderlyRelay;\n const relayDeployed = !isPlaceholderAddress(relayAddress);\n\n if (useRelay && relayDeployed) {\n // Cap = max(amount * 5%, 2 USDC). The Relay fee is a flat USDC\n // amount derived from the LayerZero ETH cost at oracle price —\n // it does NOT scale with deposit size. A pure percentage cap\n // breaks for small deposits (e.g. 0.01 USDC test deposit, 5%\n // cap = 500 wei < 14k wei real fee). The 2 USDC floor covers\n // normal LayerZero pricing on Base; the 5% slope still guards\n // against oracle spikes on large deposits.\n const RELAY_FEE_FLOOR_USDC = 2_000_000n; // 2 USDC (6 decimals)\n const percentCap = (request.amount * 500n) / 10_000n;\n const maxRelayFee =\n request.maxRelayFee ??\n (percentCap > RELAY_FEE_FLOOR_USDC ? percentCap : RELAY_FEE_FLOOR_USDC);\n\n const relayRequest = {\n token: usdcAddress,\n receiver: userAddress,\n brokerHash,\n totalAmount: request.amount,\n maxFee: maxRelayFee,\n };\n\n const relayTokenFee = (await this.provider.readContract({\n address: relayAddress,\n abi: ORDERLY_RELAY_ABI,\n functionName: \"quoteTokenFee\",\n args: [relayRequest],\n })) as bigint;\n\n if (relayTokenFee > maxRelayFee) {\n throw new Error(\n `handlePerpDeposit: Relay tokenFee ${relayTokenFee} (≈ ${\n Number(relayTokenFee) / 1e6\n } USDC) exceeds maxRelayFee ${maxRelayFee} — pass a larger ` +\n `\\`maxRelayFee\\` or increase the deposit \\`amount\\` so the fee ` +\n `becomes a smaller share of the total.`,\n );\n }\n\n // Sanity-check: Relay forwards `(totalAmount − tokenFee)` to\n // Orderly Vault. When `tokenFee >= totalAmount` the forwarded\n // amount is zero / negative and the Relay reverts on-chain with\n // `FeeExceedsAmount(fee, totalAmount)` (selector 0x536766bf),\n // which propagates as an opaque `BatchExecutor.CallFailed(1, …)`\n // revert at simulation time. Catch this early on the client so\n // the UX shows an actionable message instead of an AA21/AA34\n // bundler error wrapped around the raw selector.\n if (relayTokenFee >= request.amount) {\n const feeUsdc = Number(relayTokenFee) / 1e6;\n const amountUsdc = Number(request.amount) / 1e6;\n throw new Error(\n `handlePerpDeposit: deposit amount ${amountUsdc} USDC is below the ` +\n `Relay fee ${feeUsdc} USDC — increase \\`amount\\` to at least ` +\n `${(feeUsdc * 2).toFixed(6)} USDC so a meaningful balance reaches ` +\n `your Orderly account after the Relay charge.`,\n );\n }\n\n const userOp = buildPerpDepositViaRelay({\n userAddress,\n aaNonce: request.aaNonce,\n relayAddress,\n request: relayRequest,\n pointTokenAddress: request.pointTokenAddress,\n gasFeePt: request.gasFeePt,\n gasFeePtRecipient: request.gasFeePtRecipient,\n });\n\n return {\n userOp,\n path: \"relay\",\n layerZeroFee,\n relayTokenFee,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress,\n };\n }\n\n // Fallback: direct Vault.deposit{value} — user wallet MUST hold\n // `layerZeroFee` as native ETH (paymaster does not sponsor msg.value).\n const userOp = buildPerpDepositWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n chainId: request.chainId,\n usdcAddress,\n amount: request.amount,\n depositData,\n layerZeroFee,\n });\n\n return {\n userOp,\n path: \"vault\",\n layerZeroFee,\n relayTokenFee: 0n,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress: vault,\n };\n }\n}\n\n/**\n * `addresses.ts` uses `0x000…<suffix>` sentinels for chains where a\n * given contract is not yet deployed. Detect them by upper-160-bits =\n * 0 so we route to the Vault fallback automatically.\n */\nfunction isPlaceholderAddress(addr: Address): boolean {\n return /^0x0{36}[0-9a-fA-F]{4}$/i.test(addr);\n}\n","// Re-export from @pafi-dev/core — fetchPafiPools lives in core so all\n// SDK packages share one implementation.\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"@pafi-dev/core\";\n"],"mappings":"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","names":["userOp","import_core"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/api/handlers.ts","../src/pools.ts"],"sourcesContent":["export { TradingHandlers } from \"./api/handlers\";\nexport type { TradingHandlersConfig } from \"./api/handlers\";\n\nexport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiQuoteError,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./api/types\";\n\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"./pools\";\n","import { getAddress } from \"viem\";\nimport type { Address, PublicClient } from \"viem\";\nimport {\n findBestQuote,\n buildSwapWithGasDeduction,\n buildPerpDepositWithGasDeduction,\n buildPerpDepositViaRelay,\n ORDERLY_RELAY_ABI,\n getContractAddresses,\n UNIVERSAL_ROUTER_ADDRESSES,\n ORDERLY_VAULT_ABI,\n ORDERLY_VAULT_ADDRESSES,\n BROKER_HASHES,\n TOKEN_HASHES,\n computeAccountId,\n} from \"@pafi-dev/core\";\nimport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./types\";\n\nexport interface TradingHandlersConfig {\n provider: PublicClient;\n chainId: number;\n}\n\n/**\n * Framework-agnostic handlers for on-chain trading actions.\n *\n * All handlers are stateless — they need only a PublicClient for RPC\n * calls. No ledger, no signer, no DB. Issuers wrap these in their own\n * HTTP controllers (Express / NestJS / Hono / etc.) the same way they\n * wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.\n *\n * Example (NestJS):\n *\n * const trading = new TradingHandlers({ provider, chainId });\n *\n * // GET /quote\n * const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });\n *\n * // POST /swap\n * const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });\n *\n * // POST /perp-deposit\n * const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });\n */\nexport class TradingHandlers {\n private readonly provider: PublicClient;\n private readonly chainId: number;\n\n constructor(config: TradingHandlersConfig) {\n this.provider = config.provider;\n this.chainId = config.chainId;\n }\n\n // =========================================================================\n // GET /quote\n // =========================================================================\n\n /**\n * Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.\n *\n * Uses multicall to batch all candidate routes into a single RPC call.\n * Returns `quoteError: \"QUOTE_UNAVAILABLE\"` when no pool/path exists\n * rather than throwing, so callers can show a soft \"unavailable\" UI\n * state without 500-ing.\n */\n async handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);\n }\n if (request.amount === 0n) {\n return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const pools = request.pools ?? [];\n\n try {\n const best = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: best.bestRoute.amountOut,\n gasEstimate: best.bestRoute.gasEstimate,\n };\n } catch {\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: 0n,\n gasEstimate: 0n,\n quoteError: \"QUOTE_UNAVAILABLE\",\n };\n }\n }\n\n // =========================================================================\n // POST /swap\n // =========================================================================\n\n /**\n * Build a PT → USDT swap UserOp.\n *\n * Quotes the best route, applies slippage, then encodes a 4-step\n * batch: PT.approve → Permit2.approve → UniversalRouter.execute →\n * PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned\n * `PartialUserOperation`; caller attaches paymaster data + user\n * signature and submits to the Bundler.\n */\n async handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handleSwap: amount must be positive\");\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];\n if (!universalRouter) {\n throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);\n }\n\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const userAddress = getAddress(request.userAddress);\n const pools = request.pools ?? [];\n const slippageBps = request.slippageBps ?? 50;\n const gasFeePt = request.gasFeePt ?? 0n;\n\n if (gasFeePt > 0n && !request.feeRecipient) {\n throw new Error(\"handleSwap: feeRecipient required when gasFeePt > 0\");\n }\n\n let quoteResult: Awaited<ReturnType<typeof findBestQuote>>;\n try {\n quoteResult = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n } catch {\n throw new Error(\"handleSwap: no swap path found for this point token\");\n }\n\n const estimatedUsdtOut = quoteResult.bestRoute.amountOut;\n const minAmountOut = (estimatedUsdtOut * BigInt(10000 - slippageBps)) / 10000n;\n const deadline = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);\n\n const userOp = buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt,\n feeRecipient: request.feeRecipient ?? userAddress,\n });\n\n // Fee-stripped variant for the paymaster-refused fallback path.\n // Building it is essentially free (same quote, same path, just\n // skip the PT.transfer call) so always emit it when fee > 0; the\n // FE feeds it to `sendWithPaymasterFallback({ txParamsFallback })`.\n const userOpFallback =\n gasFeePt > 0n\n ? buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt: 0n,\n feeRecipient: userAddress,\n })\n : undefined;\n\n return { userOp, userOpFallback, estimatedUsdtOut, minAmountOut, deadline };\n }\n\n // =========================================================================\n // POST /perp-deposit\n // =========================================================================\n\n /**\n * Build an Orderly perp deposit UserOp.\n *\n * Default path is the **PAFI Orderly Relay** (`viaRelay: true`):\n * USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH\n * reserve and pays Orderly's LayerZero `msg.value` out of it; the\n * user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.\n * No native ETH on the user wallet is required, so paymaster\n * sponsorship of the ERC-4337 gas is sufficient end-to-end.\n *\n * Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.\n * Reserved for chains where no Relay is deployed — the user wallet\n * **must** hold `layerZeroFee` as native ETH.\n *\n * The Relay path automatically falls back to Vault when\n * `getContractAddresses(chainId).orderlyRelay` is the placeholder\n * sentinel (Relay not deployed for that chain).\n */\n async handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handlePerpDeposit: amount must be positive\");\n }\n\n const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];\n if (!vault) {\n throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);\n }\n\n const brokerHash = BROKER_HASHES[request.brokerId as keyof typeof BROKER_HASHES];\n if (!brokerHash) {\n throw new Error(`handlePerpDeposit: unknown brokerId \"${request.brokerId}\"`);\n }\n const tokenHash = TOKEN_HASHES.USDC;\n const userAddress = getAddress(request.userAddress);\n\n const [usdcAddress, brokerAllowed] = await Promise.all([\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedToken\",\n args: [tokenHash],\n }) as Promise<Address>,\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedBroker\",\n args: [brokerHash],\n }) as Promise<boolean>,\n ]);\n\n if (!brokerAllowed) {\n throw new Error(\n `handlePerpDeposit: broker \"${request.brokerId}\" is not whitelisted on Orderly Vault`,\n );\n }\n\n const accountId = computeAccountId(userAddress, brokerHash);\n const depositData = {\n accountId,\n brokerHash,\n tokenHash,\n tokenAmount: request.amount,\n };\n\n // Always read layerZeroFee for response — even on the Relay path\n // it's useful informational output (lets the FE show \"Relay\n // covers ~X ETH for you\").\n const layerZeroFee = (await this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getDepositFee\",\n args: [userAddress, depositData],\n })) as bigint;\n\n const useRelay = request.viaRelay !== false;\n const relayAddress = getContractAddresses(request.chainId).orderlyRelay;\n const relayDeployed = !isPlaceholderAddress(relayAddress);\n\n if (useRelay && relayDeployed) {\n // Cap = max(amount * 5%, 2 USDC). The Relay fee is a flat USDC\n // amount derived from the LayerZero ETH cost at oracle price —\n // it does NOT scale with deposit size. A pure percentage cap\n // breaks for small deposits (e.g. 0.01 USDC test deposit, 5%\n // cap = 500 wei < 14k wei real fee). The 2 USDC floor covers\n // normal LayerZero pricing on Base; the 5% slope still guards\n // against oracle spikes on large deposits.\n const RELAY_FEE_FLOOR_USDC = 2_000_000n; // 2 USDC (6 decimals)\n const percentCap = (request.amount * 500n) / 10_000n;\n const maxRelayFee =\n request.maxRelayFee ??\n (percentCap > RELAY_FEE_FLOOR_USDC ? percentCap : RELAY_FEE_FLOOR_USDC);\n\n const relayRequest = {\n token: usdcAddress,\n receiver: userAddress,\n brokerHash,\n totalAmount: request.amount,\n maxFee: maxRelayFee,\n };\n\n const relayTokenFee = (await this.provider.readContract({\n address: relayAddress,\n abi: ORDERLY_RELAY_ABI,\n functionName: \"quoteTokenFee\",\n args: [relayRequest],\n })) as bigint;\n\n if (relayTokenFee > maxRelayFee) {\n throw new Error(\n `handlePerpDeposit: Relay tokenFee ${relayTokenFee} (≈ ${\n Number(relayTokenFee) / 1e6\n } USDC) exceeds maxRelayFee ${maxRelayFee} — pass a larger ` +\n `\\`maxRelayFee\\` or increase the deposit \\`amount\\` so the fee ` +\n `becomes a smaller share of the total.`,\n );\n }\n\n // Sanity-check: Relay forwards `(totalAmount − tokenFee)` to\n // Orderly Vault. When `tokenFee >= totalAmount` the forwarded\n // amount is zero / negative and the Relay reverts on-chain with\n // `FeeExceedsAmount(fee, totalAmount)` (selector 0x536766bf),\n // which propagates as an opaque `BatchExecutor.CallFailed(1, …)`\n // revert at simulation time. Catch this early on the client so\n // the UX shows an actionable message instead of an AA21/AA34\n // bundler error wrapped around the raw selector.\n if (relayTokenFee >= request.amount) {\n const feeUsdc = Number(relayTokenFee) / 1e6;\n const amountUsdc = Number(request.amount) / 1e6;\n throw new Error(\n `handlePerpDeposit: deposit amount ${amountUsdc} USDC is below the ` +\n `Relay fee ${feeUsdc} USDC — increase \\`amount\\` to at least ` +\n `${(feeUsdc * 2).toFixed(6)} USDC so a meaningful balance reaches ` +\n `your Orderly account after the Relay charge.`,\n );\n }\n\n const userOp = buildPerpDepositViaRelay({\n userAddress,\n aaNonce: request.aaNonce,\n relayAddress,\n request: relayRequest,\n pointTokenAddress: request.pointTokenAddress,\n gasFeePt: request.gasFeePt,\n gasFeePtRecipient: request.gasFeePtRecipient,\n });\n\n // Same shape, no PT fee transfer — for the paymaster-refused\n // fallback path. The Relay still charges its USDC token-fee\n // (that compensates LayerZero ETH spend, NOT PAFI's gas).\n const userOpFallback =\n request.gasFeePt && request.gasFeePt > 0n\n ? buildPerpDepositViaRelay({\n userAddress,\n aaNonce: request.aaNonce,\n relayAddress,\n request: relayRequest,\n })\n : undefined;\n\n return {\n userOp,\n userOpFallback,\n path: \"relay\",\n layerZeroFee,\n relayTokenFee,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress,\n };\n }\n\n // Fallback: direct Vault.deposit{value} — user wallet MUST hold\n // `layerZeroFee` as native ETH (paymaster does not sponsor msg.value).\n const userOp = buildPerpDepositWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n chainId: request.chainId,\n usdcAddress,\n amount: request.amount,\n depositData,\n layerZeroFee,\n });\n\n return {\n userOp,\n path: \"vault\",\n layerZeroFee,\n relayTokenFee: 0n,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress: vault,\n };\n }\n}\n\n/**\n * `addresses.ts` uses `0x000…<suffix>` sentinels for chains where a\n * given contract is not yet deployed. Detect them by upper-160-bits =\n * 0 so we route to the Vault fallback automatically.\n */\nfunction isPlaceholderAddress(addr: Address): boolean {\n return /^0x0{36}[0-9a-fA-F]{4}$/i.test(addr);\n}\n","// Re-export from @pafi-dev/core — fetchPafiPools lives in core so all\n// SDK packages share one implementation.\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"@pafi-dev/core\";\n"],"mappings":"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","names":["userOp","import_core"]}
package/dist/index.d.cts CHANGED
@@ -43,6 +43,17 @@ interface ApiSwapRequest {
43
43
  interface ApiSwapResponse {
44
44
  /** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
45
45
  userOp: PartialUserOperation;
46
+ /**
47
+ * Fee-stripped fallback variant. Emitted only when `gasFeePt > 0n` —
48
+ * otherwise it would equal `userOp` exactly and we omit it to avoid
49
+ * encouraging callers to redundantly resubmit.
50
+ *
51
+ * Submit this when the paymaster refuses sponsorship (the user pays
52
+ * ERC-4337 gas in ETH directly): there's no point charging the
53
+ * operator fee in PT for sponsorship that didn't happen. Pair with
54
+ * `sendWithPaymasterFallback({ txParams, txParamsFallback })`.
55
+ */
56
+ userOpFallback?: PartialUserOperation;
46
57
  /** Raw USDT out before slippage (6 decimals). For display. */
47
58
  estimatedUsdtOut: bigint;
48
59
  /** Minimum USDT accepted — encoded in the UserOp calldata. */
@@ -107,6 +118,12 @@ interface ApiPerpDepositRequest {
107
118
  interface ApiPerpDepositResponse {
108
119
  /** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
109
120
  userOp: PartialUserOperation;
121
+ /**
122
+ * Fee-stripped fallback variant. Emitted only when `gasFeePt > 0n` and
123
+ * `path === "relay"` (Vault-direct path has no operator fee to strip).
124
+ * Submit when the paymaster refuses — see `ApiSwapResponse.userOpFallback`.
125
+ */
126
+ userOpFallback?: PartialUserOperation;
110
127
  /**
111
128
  * Which execution path the handler chose.
112
129
  * - `"relay"`: zero-ETH path — paymaster sponsorship alone is sufficient.
package/dist/index.d.ts CHANGED
@@ -43,6 +43,17 @@ interface ApiSwapRequest {
43
43
  interface ApiSwapResponse {
44
44
  /** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
45
45
  userOp: PartialUserOperation;
46
+ /**
47
+ * Fee-stripped fallback variant. Emitted only when `gasFeePt > 0n` —
48
+ * otherwise it would equal `userOp` exactly and we omit it to avoid
49
+ * encouraging callers to redundantly resubmit.
50
+ *
51
+ * Submit this when the paymaster refuses sponsorship (the user pays
52
+ * ERC-4337 gas in ETH directly): there's no point charging the
53
+ * operator fee in PT for sponsorship that didn't happen. Pair with
54
+ * `sendWithPaymasterFallback({ txParams, txParamsFallback })`.
55
+ */
56
+ userOpFallback?: PartialUserOperation;
46
57
  /** Raw USDT out before slippage (6 decimals). For display. */
47
58
  estimatedUsdtOut: bigint;
48
59
  /** Minimum USDT accepted — encoded in the UserOp calldata. */
@@ -107,6 +118,12 @@ interface ApiPerpDepositRequest {
107
118
  interface ApiPerpDepositResponse {
108
119
  /** Unsigned UserOp — attach paymaster data + user signature, then submit to Bundler. */
109
120
  userOp: PartialUserOperation;
121
+ /**
122
+ * Fee-stripped fallback variant. Emitted only when `gasFeePt > 0n` and
123
+ * `path === "relay"` (Vault-direct path has no operator fee to strip).
124
+ * Submit when the paymaster refuses — see `ApiSwapResponse.userOpFallback`.
125
+ */
126
+ userOpFallback?: PartialUserOperation;
110
127
  /**
111
128
  * Which execution path the handler chose.
112
129
  * - `"relay"`: zero-ETH path — paymaster sponsorship alone is sufficient.
package/dist/index.js CHANGED
@@ -126,7 +126,20 @@ var TradingHandlers = class {
126
126
  gasFeePt,
127
127
  feeRecipient: request.feeRecipient ?? userAddress
128
128
  });
129
- return { userOp, estimatedUsdtOut, minAmountOut, deadline };
129
+ const userOpFallback = gasFeePt > 0n ? buildSwapWithGasDeduction({
130
+ userAddress,
131
+ aaNonce: request.aaNonce,
132
+ pointTokenAddress,
133
+ outputTokenAddress: usdt,
134
+ universalRouterAddress: universalRouter,
135
+ amountIn: request.amount,
136
+ minAmountOut,
137
+ swapPath: quoteResult.bestRoute.path,
138
+ deadline,
139
+ gasFeePt: 0n,
140
+ feeRecipient: userAddress
141
+ }) : void 0;
142
+ return { userOp, userOpFallback, estimatedUsdtOut, minAmountOut, deadline };
130
143
  }
131
144
  // =========================================================================
132
145
  // POST /perp-deposit
@@ -239,8 +252,15 @@ var TradingHandlers = class {
239
252
  gasFeePt: request.gasFeePt,
240
253
  gasFeePtRecipient: request.gasFeePtRecipient
241
254
  });
255
+ const userOpFallback = request.gasFeePt && request.gasFeePt > 0n ? buildPerpDepositViaRelay({
256
+ userAddress,
257
+ aaNonce: request.aaNonce,
258
+ relayAddress,
259
+ request: relayRequest
260
+ }) : void 0;
242
261
  return {
243
262
  userOp: userOp2,
263
+ userOpFallback,
244
264
  path: "relay",
245
265
  layerZeroFee,
246
266
  relayTokenFee,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api/handlers.ts","../src/pools.ts"],"sourcesContent":["import { getAddress } from \"viem\";\nimport type { Address, PublicClient } from \"viem\";\nimport {\n findBestQuote,\n buildSwapWithGasDeduction,\n buildPerpDepositWithGasDeduction,\n buildPerpDepositViaRelay,\n ORDERLY_RELAY_ABI,\n getContractAddresses,\n UNIVERSAL_ROUTER_ADDRESSES,\n ORDERLY_VAULT_ABI,\n ORDERLY_VAULT_ADDRESSES,\n BROKER_HASHES,\n TOKEN_HASHES,\n computeAccountId,\n} from \"@pafi-dev/core\";\nimport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./types\";\n\nexport interface TradingHandlersConfig {\n provider: PublicClient;\n chainId: number;\n}\n\n/**\n * Framework-agnostic handlers for on-chain trading actions.\n *\n * All handlers are stateless — they need only a PublicClient for RPC\n * calls. No ledger, no signer, no DB. Issuers wrap these in their own\n * HTTP controllers (Express / NestJS / Hono / etc.) the same way they\n * wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.\n *\n * Example (NestJS):\n *\n * const trading = new TradingHandlers({ provider, chainId });\n *\n * // GET /quote\n * const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });\n *\n * // POST /swap\n * const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });\n *\n * // POST /perp-deposit\n * const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });\n */\nexport class TradingHandlers {\n private readonly provider: PublicClient;\n private readonly chainId: number;\n\n constructor(config: TradingHandlersConfig) {\n this.provider = config.provider;\n this.chainId = config.chainId;\n }\n\n // =========================================================================\n // GET /quote\n // =========================================================================\n\n /**\n * Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.\n *\n * Uses multicall to batch all candidate routes into a single RPC call.\n * Returns `quoteError: \"QUOTE_UNAVAILABLE\"` when no pool/path exists\n * rather than throwing, so callers can show a soft \"unavailable\" UI\n * state without 500-ing.\n */\n async handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);\n }\n if (request.amount === 0n) {\n return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const pools = request.pools ?? [];\n\n try {\n const best = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: best.bestRoute.amountOut,\n gasEstimate: best.bestRoute.gasEstimate,\n };\n } catch {\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: 0n,\n gasEstimate: 0n,\n quoteError: \"QUOTE_UNAVAILABLE\",\n };\n }\n }\n\n // =========================================================================\n // POST /swap\n // =========================================================================\n\n /**\n * Build a PT → USDT swap UserOp.\n *\n * Quotes the best route, applies slippage, then encodes a 4-step\n * batch: PT.approve → Permit2.approve → UniversalRouter.execute →\n * PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned\n * `PartialUserOperation`; caller attaches paymaster data + user\n * signature and submits to the Bundler.\n */\n async handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handleSwap: amount must be positive\");\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];\n if (!universalRouter) {\n throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);\n }\n\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const userAddress = getAddress(request.userAddress);\n const pools = request.pools ?? [];\n const slippageBps = request.slippageBps ?? 50;\n const gasFeePt = request.gasFeePt ?? 0n;\n\n if (gasFeePt > 0n && !request.feeRecipient) {\n throw new Error(\"handleSwap: feeRecipient required when gasFeePt > 0\");\n }\n\n let quoteResult: Awaited<ReturnType<typeof findBestQuote>>;\n try {\n quoteResult = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n } catch {\n throw new Error(\"handleSwap: no swap path found for this point token\");\n }\n\n const estimatedUsdtOut = quoteResult.bestRoute.amountOut;\n const minAmountOut = (estimatedUsdtOut * BigInt(10000 - slippageBps)) / 10000n;\n const deadline = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);\n\n const userOp = buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt,\n feeRecipient: request.feeRecipient ?? userAddress,\n });\n\n return { userOp, estimatedUsdtOut, minAmountOut, deadline };\n }\n\n // =========================================================================\n // POST /perp-deposit\n // =========================================================================\n\n /**\n * Build an Orderly perp deposit UserOp.\n *\n * Default path is the **PAFI Orderly Relay** (`viaRelay: true`):\n * USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH\n * reserve and pays Orderly's LayerZero `msg.value` out of it; the\n * user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.\n * No native ETH on the user wallet is required, so paymaster\n * sponsorship of the ERC-4337 gas is sufficient end-to-end.\n *\n * Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.\n * Reserved for chains where no Relay is deployed — the user wallet\n * **must** hold `layerZeroFee` as native ETH.\n *\n * The Relay path automatically falls back to Vault when\n * `getContractAddresses(chainId).orderlyRelay` is the placeholder\n * sentinel (Relay not deployed for that chain).\n */\n async handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handlePerpDeposit: amount must be positive\");\n }\n\n const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];\n if (!vault) {\n throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);\n }\n\n const brokerHash = BROKER_HASHES[request.brokerId as keyof typeof BROKER_HASHES];\n if (!brokerHash) {\n throw new Error(`handlePerpDeposit: unknown brokerId \"${request.brokerId}\"`);\n }\n const tokenHash = TOKEN_HASHES.USDC;\n const userAddress = getAddress(request.userAddress);\n\n const [usdcAddress, brokerAllowed] = await Promise.all([\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedToken\",\n args: [tokenHash],\n }) as Promise<Address>,\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedBroker\",\n args: [brokerHash],\n }) as Promise<boolean>,\n ]);\n\n if (!brokerAllowed) {\n throw new Error(\n `handlePerpDeposit: broker \"${request.brokerId}\" is not whitelisted on Orderly Vault`,\n );\n }\n\n const accountId = computeAccountId(userAddress, brokerHash);\n const depositData = {\n accountId,\n brokerHash,\n tokenHash,\n tokenAmount: request.amount,\n };\n\n // Always read layerZeroFee for response — even on the Relay path\n // it's useful informational output (lets the FE show \"Relay\n // covers ~X ETH for you\").\n const layerZeroFee = (await this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getDepositFee\",\n args: [userAddress, depositData],\n })) as bigint;\n\n const useRelay = request.viaRelay !== false;\n const relayAddress = getContractAddresses(request.chainId).orderlyRelay;\n const relayDeployed = !isPlaceholderAddress(relayAddress);\n\n if (useRelay && relayDeployed) {\n // Cap = max(amount * 5%, 2 USDC). The Relay fee is a flat USDC\n // amount derived from the LayerZero ETH cost at oracle price —\n // it does NOT scale with deposit size. A pure percentage cap\n // breaks for small deposits (e.g. 0.01 USDC test deposit, 5%\n // cap = 500 wei < 14k wei real fee). The 2 USDC floor covers\n // normal LayerZero pricing on Base; the 5% slope still guards\n // against oracle spikes on large deposits.\n const RELAY_FEE_FLOOR_USDC = 2_000_000n; // 2 USDC (6 decimals)\n const percentCap = (request.amount * 500n) / 10_000n;\n const maxRelayFee =\n request.maxRelayFee ??\n (percentCap > RELAY_FEE_FLOOR_USDC ? percentCap : RELAY_FEE_FLOOR_USDC);\n\n const relayRequest = {\n token: usdcAddress,\n receiver: userAddress,\n brokerHash,\n totalAmount: request.amount,\n maxFee: maxRelayFee,\n };\n\n const relayTokenFee = (await this.provider.readContract({\n address: relayAddress,\n abi: ORDERLY_RELAY_ABI,\n functionName: \"quoteTokenFee\",\n args: [relayRequest],\n })) as bigint;\n\n if (relayTokenFee > maxRelayFee) {\n throw new Error(\n `handlePerpDeposit: Relay tokenFee ${relayTokenFee} (≈ ${\n Number(relayTokenFee) / 1e6\n } USDC) exceeds maxRelayFee ${maxRelayFee} — pass a larger ` +\n `\\`maxRelayFee\\` or increase the deposit \\`amount\\` so the fee ` +\n `becomes a smaller share of the total.`,\n );\n }\n\n // Sanity-check: Relay forwards `(totalAmount − tokenFee)` to\n // Orderly Vault. When `tokenFee >= totalAmount` the forwarded\n // amount is zero / negative and the Relay reverts on-chain with\n // `FeeExceedsAmount(fee, totalAmount)` (selector 0x536766bf),\n // which propagates as an opaque `BatchExecutor.CallFailed(1, …)`\n // revert at simulation time. Catch this early on the client so\n // the UX shows an actionable message instead of an AA21/AA34\n // bundler error wrapped around the raw selector.\n if (relayTokenFee >= request.amount) {\n const feeUsdc = Number(relayTokenFee) / 1e6;\n const amountUsdc = Number(request.amount) / 1e6;\n throw new Error(\n `handlePerpDeposit: deposit amount ${amountUsdc} USDC is below the ` +\n `Relay fee ${feeUsdc} USDC — increase \\`amount\\` to at least ` +\n `${(feeUsdc * 2).toFixed(6)} USDC so a meaningful balance reaches ` +\n `your Orderly account after the Relay charge.`,\n );\n }\n\n const userOp = buildPerpDepositViaRelay({\n userAddress,\n aaNonce: request.aaNonce,\n relayAddress,\n request: relayRequest,\n pointTokenAddress: request.pointTokenAddress,\n gasFeePt: request.gasFeePt,\n gasFeePtRecipient: request.gasFeePtRecipient,\n });\n\n return {\n userOp,\n path: \"relay\",\n layerZeroFee,\n relayTokenFee,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress,\n };\n }\n\n // Fallback: direct Vault.deposit{value} — user wallet MUST hold\n // `layerZeroFee` as native ETH (paymaster does not sponsor msg.value).\n const userOp = buildPerpDepositWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n chainId: request.chainId,\n usdcAddress,\n amount: request.amount,\n depositData,\n layerZeroFee,\n });\n\n return {\n userOp,\n path: \"vault\",\n layerZeroFee,\n relayTokenFee: 0n,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress: vault,\n };\n }\n}\n\n/**\n * `addresses.ts` uses `0x000…<suffix>` sentinels for chains where a\n * given contract is not yet deployed. Detect them by upper-160-bits =\n * 0 so we route to the Vault fallback automatically.\n */\nfunction isPlaceholderAddress(addr: Address): boolean {\n return /^0x0{36}[0-9a-fA-F]{4}$/i.test(addr);\n}\n","// Re-export from @pafi-dev/core — fetchPafiPools lives in core so all\n// SDK packages share one implementation.\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"@pafi-dev/core\";\n"],"mappings":"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","names":["userOp"]}
1
+ {"version":3,"sources":["../src/api/handlers.ts","../src/pools.ts"],"sourcesContent":["import { getAddress } from \"viem\";\nimport type { Address, PublicClient } from \"viem\";\nimport {\n findBestQuote,\n buildSwapWithGasDeduction,\n buildPerpDepositWithGasDeduction,\n buildPerpDepositViaRelay,\n ORDERLY_RELAY_ABI,\n getContractAddresses,\n UNIVERSAL_ROUTER_ADDRESSES,\n ORDERLY_VAULT_ABI,\n ORDERLY_VAULT_ADDRESSES,\n BROKER_HASHES,\n TOKEN_HASHES,\n computeAccountId,\n} from \"@pafi-dev/core\";\nimport type {\n ApiQuoteRequest,\n ApiQuoteResponse,\n ApiSwapRequest,\n ApiSwapResponse,\n ApiPerpDepositRequest,\n ApiPerpDepositResponse,\n} from \"./types\";\n\nexport interface TradingHandlersConfig {\n provider: PublicClient;\n chainId: number;\n}\n\n/**\n * Framework-agnostic handlers for on-chain trading actions.\n *\n * All handlers are stateless — they need only a PublicClient for RPC\n * calls. No ledger, no signer, no DB. Issuers wrap these in their own\n * HTTP controllers (Express / NestJS / Hono / etc.) the same way they\n * wrap `IssuerApiHandlers` from `@pafi-dev/issuer`.\n *\n * Example (NestJS):\n *\n * const trading = new TradingHandlers({ provider, chainId });\n *\n * // GET /quote\n * const quote = await trading.handleQuote({ chainId, pointTokenAddress, amount, pools });\n *\n * // POST /swap\n * const swap = await trading.handleSwap({ chainId, userAddress, pointTokenAddress, amount, aaNonce });\n *\n * // POST /perp-deposit\n * const deposit = await trading.handlePerpDeposit({ chainId, userAddress, amount, aaNonce, brokerId });\n */\nexport class TradingHandlers {\n private readonly provider: PublicClient;\n private readonly chainId: number;\n\n constructor(config: TradingHandlersConfig) {\n this.provider = config.provider;\n this.chainId = config.chainId;\n }\n\n // =========================================================================\n // GET /quote\n // =========================================================================\n\n /**\n * Quote exact-input PT → USDT via Uniswap V4 on-chain Quoter.\n *\n * Uses multicall to batch all candidate routes into a single RPC call.\n * Returns `quoteError: \"QUOTE_UNAVAILABLE\"` when no pool/path exists\n * rather than throwing, so callers can show a soft \"unavailable\" UI\n * state without 500-ing.\n */\n async handleQuote(request: ApiQuoteRequest): Promise<ApiQuoteResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);\n }\n if (request.amount === 0n) {\n return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const pools = request.pools ?? [];\n\n try {\n const best = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: best.bestRoute.amountOut,\n gasEstimate: best.bestRoute.gasEstimate,\n };\n } catch {\n return {\n pointAmount: request.amount,\n estimatedUsdtOut: 0n,\n gasEstimate: 0n,\n quoteError: \"QUOTE_UNAVAILABLE\",\n };\n }\n }\n\n // =========================================================================\n // POST /swap\n // =========================================================================\n\n /**\n * Build a PT → USDT swap UserOp.\n *\n * Quotes the best route, applies slippage, then encodes a 4-step\n * batch: PT.approve → Permit2.approve → UniversalRouter.execute →\n * PT.transfer (fee, omitted when gasFeePt = 0). Returns an unsigned\n * `PartialUserOperation`; caller attaches paymaster data + user\n * signature and submits to the Bundler.\n */\n async handleSwap(request: ApiSwapRequest): Promise<ApiSwapResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handleSwap: amount must be positive\");\n }\n\n const { usdt } = getContractAddresses(request.chainId);\n const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];\n if (!universalRouter) {\n throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);\n }\n\n const pointTokenAddress = getAddress(request.pointTokenAddress);\n const userAddress = getAddress(request.userAddress);\n const pools = request.pools ?? [];\n const slippageBps = request.slippageBps ?? 50;\n const gasFeePt = request.gasFeePt ?? 0n;\n\n if (gasFeePt > 0n && !request.feeRecipient) {\n throw new Error(\"handleSwap: feeRecipient required when gasFeePt > 0\");\n }\n\n let quoteResult: Awaited<ReturnType<typeof findBestQuote>>;\n try {\n quoteResult = await findBestQuote(\n this.provider,\n request.chainId,\n pointTokenAddress,\n usdt,\n request.amount,\n pools,\n );\n } catch {\n throw new Error(\"handleSwap: no swap path found for this point token\");\n }\n\n const estimatedUsdtOut = quoteResult.bestRoute.amountOut;\n const minAmountOut = (estimatedUsdtOut * BigInt(10000 - slippageBps)) / 10000n;\n const deadline = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);\n\n const userOp = buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt,\n feeRecipient: request.feeRecipient ?? userAddress,\n });\n\n // Fee-stripped variant for the paymaster-refused fallback path.\n // Building it is essentially free (same quote, same path, just\n // skip the PT.transfer call) so always emit it when fee > 0; the\n // FE feeds it to `sendWithPaymasterFallback({ txParamsFallback })`.\n const userOpFallback =\n gasFeePt > 0n\n ? buildSwapWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n pointTokenAddress,\n outputTokenAddress: usdt,\n universalRouterAddress: universalRouter,\n amountIn: request.amount,\n minAmountOut,\n swapPath: quoteResult.bestRoute.path,\n deadline,\n gasFeePt: 0n,\n feeRecipient: userAddress,\n })\n : undefined;\n\n return { userOp, userOpFallback, estimatedUsdtOut, minAmountOut, deadline };\n }\n\n // =========================================================================\n // POST /perp-deposit\n // =========================================================================\n\n /**\n * Build an Orderly perp deposit UserOp.\n *\n * Default path is the **PAFI Orderly Relay** (`viaRelay: true`):\n * USDC.approve(relay) + relay.deposit(req). The Relay holds an ETH\n * reserve and pays Orderly's LayerZero `msg.value` out of it; the\n * user pays a USDC fee (quoted via `Relay.quoteTokenFee`) instead.\n * No native ETH on the user wallet is required, so paymaster\n * sponsorship of the ERC-4337 gas is sufficient end-to-end.\n *\n * Fallback path (`viaRelay: false`): direct `Vault.deposit{value}`.\n * Reserved for chains where no Relay is deployed — the user wallet\n * **must** hold `layerZeroFee` as native ETH.\n *\n * The Relay path automatically falls back to Vault when\n * `getContractAddresses(chainId).orderlyRelay` is the placeholder\n * sentinel (Relay not deployed for that chain).\n */\n async handlePerpDeposit(request: ApiPerpDepositRequest): Promise<ApiPerpDepositResponse> {\n if (request.chainId !== this.chainId) {\n throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);\n }\n if (request.amount <= 0n) {\n throw new Error(\"handlePerpDeposit: amount must be positive\");\n }\n\n const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];\n if (!vault) {\n throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);\n }\n\n const brokerHash = BROKER_HASHES[request.brokerId as keyof typeof BROKER_HASHES];\n if (!brokerHash) {\n throw new Error(`handlePerpDeposit: unknown brokerId \"${request.brokerId}\"`);\n }\n const tokenHash = TOKEN_HASHES.USDC;\n const userAddress = getAddress(request.userAddress);\n\n const [usdcAddress, brokerAllowed] = await Promise.all([\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedToken\",\n args: [tokenHash],\n }) as Promise<Address>,\n this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getAllowedBroker\",\n args: [brokerHash],\n }) as Promise<boolean>,\n ]);\n\n if (!brokerAllowed) {\n throw new Error(\n `handlePerpDeposit: broker \"${request.brokerId}\" is not whitelisted on Orderly Vault`,\n );\n }\n\n const accountId = computeAccountId(userAddress, brokerHash);\n const depositData = {\n accountId,\n brokerHash,\n tokenHash,\n tokenAmount: request.amount,\n };\n\n // Always read layerZeroFee for response — even on the Relay path\n // it's useful informational output (lets the FE show \"Relay\n // covers ~X ETH for you\").\n const layerZeroFee = (await this.provider.readContract({\n address: vault,\n abi: ORDERLY_VAULT_ABI,\n functionName: \"getDepositFee\",\n args: [userAddress, depositData],\n })) as bigint;\n\n const useRelay = request.viaRelay !== false;\n const relayAddress = getContractAddresses(request.chainId).orderlyRelay;\n const relayDeployed = !isPlaceholderAddress(relayAddress);\n\n if (useRelay && relayDeployed) {\n // Cap = max(amount * 5%, 2 USDC). The Relay fee is a flat USDC\n // amount derived from the LayerZero ETH cost at oracle price —\n // it does NOT scale with deposit size. A pure percentage cap\n // breaks for small deposits (e.g. 0.01 USDC test deposit, 5%\n // cap = 500 wei < 14k wei real fee). The 2 USDC floor covers\n // normal LayerZero pricing on Base; the 5% slope still guards\n // against oracle spikes on large deposits.\n const RELAY_FEE_FLOOR_USDC = 2_000_000n; // 2 USDC (6 decimals)\n const percentCap = (request.amount * 500n) / 10_000n;\n const maxRelayFee =\n request.maxRelayFee ??\n (percentCap > RELAY_FEE_FLOOR_USDC ? percentCap : RELAY_FEE_FLOOR_USDC);\n\n const relayRequest = {\n token: usdcAddress,\n receiver: userAddress,\n brokerHash,\n totalAmount: request.amount,\n maxFee: maxRelayFee,\n };\n\n const relayTokenFee = (await this.provider.readContract({\n address: relayAddress,\n abi: ORDERLY_RELAY_ABI,\n functionName: \"quoteTokenFee\",\n args: [relayRequest],\n })) as bigint;\n\n if (relayTokenFee > maxRelayFee) {\n throw new Error(\n `handlePerpDeposit: Relay tokenFee ${relayTokenFee} (≈ ${\n Number(relayTokenFee) / 1e6\n } USDC) exceeds maxRelayFee ${maxRelayFee} — pass a larger ` +\n `\\`maxRelayFee\\` or increase the deposit \\`amount\\` so the fee ` +\n `becomes a smaller share of the total.`,\n );\n }\n\n // Sanity-check: Relay forwards `(totalAmount − tokenFee)` to\n // Orderly Vault. When `tokenFee >= totalAmount` the forwarded\n // amount is zero / negative and the Relay reverts on-chain with\n // `FeeExceedsAmount(fee, totalAmount)` (selector 0x536766bf),\n // which propagates as an opaque `BatchExecutor.CallFailed(1, …)`\n // revert at simulation time. Catch this early on the client so\n // the UX shows an actionable message instead of an AA21/AA34\n // bundler error wrapped around the raw selector.\n if (relayTokenFee >= request.amount) {\n const feeUsdc = Number(relayTokenFee) / 1e6;\n const amountUsdc = Number(request.amount) / 1e6;\n throw new Error(\n `handlePerpDeposit: deposit amount ${amountUsdc} USDC is below the ` +\n `Relay fee ${feeUsdc} USDC — increase \\`amount\\` to at least ` +\n `${(feeUsdc * 2).toFixed(6)} USDC so a meaningful balance reaches ` +\n `your Orderly account after the Relay charge.`,\n );\n }\n\n const userOp = buildPerpDepositViaRelay({\n userAddress,\n aaNonce: request.aaNonce,\n relayAddress,\n request: relayRequest,\n pointTokenAddress: request.pointTokenAddress,\n gasFeePt: request.gasFeePt,\n gasFeePtRecipient: request.gasFeePtRecipient,\n });\n\n // Same shape, no PT fee transfer — for the paymaster-refused\n // fallback path. The Relay still charges its USDC token-fee\n // (that compensates LayerZero ETH spend, NOT PAFI's gas).\n const userOpFallback =\n request.gasFeePt && request.gasFeePt > 0n\n ? buildPerpDepositViaRelay({\n userAddress,\n aaNonce: request.aaNonce,\n relayAddress,\n request: relayRequest,\n })\n : undefined;\n\n return {\n userOp,\n userOpFallback,\n path: \"relay\",\n layerZeroFee,\n relayTokenFee,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress,\n };\n }\n\n // Fallback: direct Vault.deposit{value} — user wallet MUST hold\n // `layerZeroFee` as native ETH (paymaster does not sponsor msg.value).\n const userOp = buildPerpDepositWithGasDeduction({\n userAddress,\n aaNonce: request.aaNonce,\n chainId: request.chainId,\n usdcAddress,\n amount: request.amount,\n depositData,\n layerZeroFee,\n });\n\n return {\n userOp,\n path: \"vault\",\n layerZeroFee,\n relayTokenFee: 0n,\n accountId,\n brokerHash,\n usdcAddress,\n relayAddress: vault,\n };\n }\n}\n\n/**\n * `addresses.ts` uses `0x000…<suffix>` sentinels for chains where a\n * given contract is not yet deployed. Detect them by upper-160-bits =\n * 0 so we route to the Vault fallback automatically.\n */\nfunction isPlaceholderAddress(addr: Address): boolean {\n return /^0x0{36}[0-9a-fA-F]{4}$/i.test(addr);\n}\n","// Re-export from @pafi-dev/core — fetchPafiPools lives in core so all\n// SDK packages share one implementation.\nexport { fetchPafiPools, PAFI_SUBGRAPH_URL } from \"@pafi-dev/core\";\n"],"mappings":"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","names":["userOp"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pafi-dev/trading",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Stateless on-chain trading handlers for PAFI — swap, quote, perp deposit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,7 +22,7 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "@pafi-dev/core": "0.5.17"
25
+ "@pafi-dev/core": "0.5.18"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "viem": "^2.0.0"