@pafi-dev/trading 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,22 +21,34 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  PAFI_SUBGRAPH_URL: () => import_core10.PAFI_SUBGRAPH_URL,
24
+ SWAP_EXACT_OUT: () => SWAP_EXACT_OUT,
25
+ SWAP_EXACT_OUT_SINGLE: () => SWAP_EXACT_OUT_SINGLE,
24
26
  TradingHandlers: () => TradingHandlers,
25
27
  buildAllPaths: () => buildAllPaths,
26
28
  buildErc20ApprovalCalldata: () => buildErc20ApprovalCalldata,
27
29
  buildPermit2ApprovalCalldata: () => buildPermit2ApprovalCalldata,
28
30
  buildSwapFromQuote: () => buildSwapFromQuote,
29
31
  buildSwapUserOp: () => buildSwapUserOp,
32
+ buildSwapUserOpExactOut: () => buildSwapUserOpExactOut,
30
33
  buildUniversalRouterExecuteArgs: () => buildUniversalRouterExecuteArgs,
34
+ buildUniversalRouterExecuteArgsExactOut: () => buildUniversalRouterExecuteArgsExactOut,
31
35
  buildV4SwapInput: () => buildV4SwapInput,
36
+ buildV4SwapInputExactOut: () => buildV4SwapInputExactOut,
32
37
  checkAllowance: () => checkAllowance,
33
38
  combineRoutes: () => combineRoutes,
34
39
  fetchPafiPools: () => import_core10.fetchPafiPools,
35
40
  findBestQuote: () => findBestQuote,
41
+ findBestQuoteExactOut: () => findBestQuoteExactOut,
42
+ perpDepositDirect: () => perpDepositDirect,
36
43
  quoteBestRoute: () => quoteBestRoute,
44
+ quoteBestRouteExactOut: () => quoteBestRouteExactOut,
37
45
  quoteExactInput: () => quoteExactInput,
38
46
  quoteExactInputSingle: () => quoteExactInputSingle,
39
- simulateSwap: () => simulateSwap
47
+ quoteExactOutput: () => quoteExactOutput,
48
+ quoteExactOutputSingle: () => quoteExactOutputSingle,
49
+ simulateSwap: () => simulateSwap,
50
+ swapDirect: () => swapDirect,
51
+ swapDirectExactOut: () => swapDirectExactOut
40
52
  });
41
53
  module.exports = __toCommonJS(index_exports);
42
54
 
@@ -99,7 +111,7 @@ async function quoteExactInput(client, quoterAddress, exactCurrency, path, exact
99
111
  address: quoterAddress,
100
112
  abi: import_core2.v4QuoterAbi,
101
113
  functionName: "quoteExactInput",
102
- args: [{ exactCurrency, path, exactAmount: BigInt(exactAmount) }]
114
+ args: [{ exactCurrency, path, exactAmount }]
103
115
  });
104
116
  return { amountOut, gasEstimate, path };
105
117
  }
@@ -112,7 +124,7 @@ async function quoteExactInputSingle(client, quoterAddress, poolKey, zeroForOne,
112
124
  {
113
125
  poolKey,
114
126
  zeroForOne,
115
- exactAmount: BigInt(exactAmount),
127
+ exactAmount,
116
128
  hookData
117
129
  }
118
130
  ]
@@ -129,13 +141,7 @@ async function quoteBestRoute(client, quoterAddress, exactCurrency, routes, exac
129
141
  {
130
142
  exactCurrency,
131
143
  path,
132
- // `as unknown as number` cast — the V4 quoter's solidity
133
- // signature takes `uint256` (a bigint at the wire level), but
134
- // our pinned ABI types it as `number`. The runtime is fine
135
- // (bigint serializes correctly) — this cast tells TS to skip
136
- // the structural check until we re-generate the ABI with the
137
- // correct uint256 type. Tracked: SDK_CORE_TRADING_AUDIT.md H11.
138
- exactAmount: BigInt(exactAmount)
144
+ exactAmount
139
145
  }
140
146
  ]
141
147
  })),
@@ -176,6 +182,84 @@ async function findBestQuote(client, chainId, tokenIn, tokenOut, exactAmount, po
176
182
  }
177
183
  return quoteBestRoute(client, quoter, tokenIn, paths, exactAmount);
178
184
  }
185
+ async function quoteExactOutput(client, quoterAddress, exactCurrency, path, exactAmount) {
186
+ const [amountIn, gasEstimate] = await client.readContract({
187
+ address: quoterAddress,
188
+ abi: import_core2.v4QuoterAbi,
189
+ functionName: "quoteExactOutput",
190
+ args: [{ exactCurrency, path, exactAmount }]
191
+ });
192
+ return { amountIn, gasEstimate, path };
193
+ }
194
+ async function quoteExactOutputSingle(client, quoterAddress, poolKey, zeroForOne, exactAmount, hookData) {
195
+ const [amountIn, gasEstimate] = await client.readContract({
196
+ address: quoterAddress,
197
+ abi: import_core2.v4QuoterAbi,
198
+ functionName: "quoteExactOutputSingle",
199
+ args: [
200
+ {
201
+ poolKey,
202
+ zeroForOne,
203
+ exactAmount,
204
+ hookData
205
+ }
206
+ ]
207
+ });
208
+ return { amountIn, gasEstimate };
209
+ }
210
+ async function quoteBestRouteExactOut(client, quoterAddress, exactCurrency, routes, exactAmount) {
211
+ const results = await client.multicall({
212
+ contracts: routes.map((path) => ({
213
+ address: quoterAddress,
214
+ abi: import_core2.v4QuoterAbi,
215
+ functionName: "quoteExactOutput",
216
+ args: [
217
+ {
218
+ exactCurrency,
219
+ path,
220
+ exactAmount
221
+ }
222
+ ]
223
+ })),
224
+ allowFailure: true
225
+ });
226
+ const allRoutes = [];
227
+ let firstFailure;
228
+ for (let i = 0; i < results.length; i++) {
229
+ const r = results[i];
230
+ if (r.status === "success") {
231
+ const [amountIn, gasEstimate] = r.result;
232
+ allRoutes.push({ amountIn, gasEstimate, path: routes[i] });
233
+ } else if (firstFailure === void 0) {
234
+ const errMsg = r.error instanceof Error ? r.error.message : String(r.error ?? "unknown");
235
+ firstFailure = errMsg;
236
+ }
237
+ }
238
+ if (allRoutes.length === 0) {
239
+ throw new Error(
240
+ `No valid exact-output routes found to ${exactCurrency} (${routes.length} candidates probed)` + (firstFailure ? `; first failure: ${firstFailure}` : "")
241
+ );
242
+ }
243
+ const bestRoute = allRoutes.reduce(
244
+ (best, current) => current.amountIn < best.amountIn ? current : best
245
+ );
246
+ return { bestRoute, allRoutes };
247
+ }
248
+ async function findBestQuoteExactOut(client, chainId, tokenIn, tokenOut, exactAmount, pools = [], quoterAddress, maxHops = 3) {
249
+ const quoter = quoterAddress ?? import_core3.V4_QUOTER_ADDRESSES[chainId];
250
+ if (!quoter) {
251
+ throw new Error(`No V4 Quoter address configured for chain ${chainId}`);
252
+ }
253
+ const commonPools = import_core3.COMMON_POOLS[chainId] ?? [];
254
+ const allPools = [...pools, ...commonPools];
255
+ const paths = buildAllPaths(allPools, tokenOut, tokenIn, maxHops);
256
+ if (paths.length === 0) {
257
+ throw new Error(
258
+ `No exact-output paths found to ${tokenOut} from ${tokenIn}`
259
+ );
260
+ }
261
+ return quoteBestRouteExactOut(client, quoter, tokenOut, paths, exactAmount);
262
+ }
179
263
 
180
264
  // src/swap/approval.ts
181
265
  var import_viem = require("viem");
@@ -208,8 +292,11 @@ function buildPermit2ApprovalCalldata(token, spender, amount, expiration) {
208
292
  var import_viem2 = require("viem");
209
293
  var V4_SWAP = 16;
210
294
  var SWAP_EXACT_IN = 7;
295
+ var SWAP_EXACT_OUT_SINGLE = 8;
296
+ var SWAP_EXACT_OUT = 9;
211
297
  var SETTLE_ALL = 12;
212
298
  var TAKE_ALL = 15;
299
+ var UINT128_MAX = 2n ** 128n - 1n;
213
300
  var PATH_KEY_ABI_COMPONENTS = [
214
301
  { name: "intermediateCurrency", type: "address" },
215
302
  { name: "fee", type: "uint256" },
@@ -227,6 +314,16 @@ var EXACT_INPUT_PARAMS_ABI = [
227
314
  { name: "amountIn", type: "uint128" },
228
315
  { name: "amountOutMinimum", type: "uint128" }
229
316
  ];
317
+ var EXACT_OUTPUT_PARAMS_ABI = [
318
+ { name: "currencyOut", type: "address" },
319
+ {
320
+ name: "path",
321
+ type: "tuple[]",
322
+ components: PATH_KEY_ABI_COMPONENTS
323
+ },
324
+ { name: "amountOut", type: "uint128" },
325
+ { name: "amountInMaximum", type: "uint128" }
326
+ ];
230
327
  function buildV4SwapInput(currencyIn, path, amountIn, minAmountOut, outputCurrency) {
231
328
  const actions = (0, import_viem2.encodePacked)(
232
329
  ["uint8", "uint8", "uint8"],
@@ -278,7 +375,81 @@ function buildUniversalRouterExecuteArgs(currencyIn, path, amountIn, minAmountOu
278
375
  ];
279
376
  return { commands, inputs };
280
377
  }
378
+ function buildV4SwapInputExactOut(currencyOut, path, amountOut, maxAmountIn, inputCurrency) {
379
+ if (amountOut <= 0n || amountOut > UINT128_MAX) {
380
+ throw new Error(
381
+ `buildV4SwapInputExactOut: amountOut (${amountOut}) must be in (0, 2^128-1]`
382
+ );
383
+ }
384
+ if (maxAmountIn <= 0n || maxAmountIn > UINT128_MAX) {
385
+ throw new Error(
386
+ `buildV4SwapInputExactOut: maxAmountIn (${maxAmountIn}) must be in (0, 2^128-1]`
387
+ );
388
+ }
389
+ const actions = (0, import_viem2.encodePacked)(
390
+ ["uint8", "uint8", "uint8"],
391
+ [SWAP_EXACT_OUT, SETTLE_ALL, TAKE_ALL]
392
+ );
393
+ const swapParam = (0, import_viem2.encodeAbiParameters)(
394
+ [{ name: "swap", type: "tuple", components: EXACT_OUTPUT_PARAMS_ABI }],
395
+ [
396
+ {
397
+ currencyOut,
398
+ path: path.map((p) => ({
399
+ intermediateCurrency: p.intermediateCurrency,
400
+ fee: BigInt(p.fee),
401
+ tickSpacing: p.tickSpacing,
402
+ hooks: p.hooks,
403
+ hookData: p.hookData
404
+ })),
405
+ amountOut,
406
+ amountInMaximum: maxAmountIn
407
+ }
408
+ ]
409
+ );
410
+ const settleParam = (0, import_viem2.encodeAbiParameters)(
411
+ [
412
+ { name: "currency", type: "address" },
413
+ { name: "maxAmount", type: "uint256" }
414
+ ],
415
+ [inputCurrency, maxAmountIn]
416
+ );
417
+ const takeParam = (0, import_viem2.encodeAbiParameters)(
418
+ [
419
+ { name: "currency", type: "address" },
420
+ { name: "minAmount", type: "uint256" }
421
+ ],
422
+ [currencyOut, amountOut]
423
+ );
424
+ return (0, import_viem2.encodeAbiParameters)(
425
+ [
426
+ { name: "actions", type: "bytes" },
427
+ { name: "params", type: "bytes[]" }
428
+ ],
429
+ [actions, [swapParam, settleParam, takeParam]]
430
+ );
431
+ }
432
+ function buildUniversalRouterExecuteArgsExactOut(currencyOut, path, amountOut, maxAmountIn, inputCurrency) {
433
+ const commands = (0, import_viem2.encodePacked)(["uint8"], [V4_SWAP]);
434
+ const inputs = [
435
+ buildV4SwapInputExactOut(
436
+ currencyOut,
437
+ path,
438
+ amountOut,
439
+ maxAmountIn,
440
+ inputCurrency
441
+ )
442
+ ];
443
+ return { commands, inputs };
444
+ }
445
+ var _buildSwapFromQuoteWarned = false;
281
446
  function buildSwapFromQuote(params) {
447
+ if (!_buildSwapFromQuoteWarned) {
448
+ _buildSwapFromQuoteWarned = true;
449
+ console.warn(
450
+ "[PAFI] DEPRECATION (v1.4+): `buildSwapFromQuote` from @pafi-dev/trading is deprecated and will be removed in v2.0. Use `buildUniversalRouterExecuteArgs` directly with `quote.bestRoute.path`."
451
+ );
452
+ }
282
453
  return buildUniversalRouterExecuteArgs(
283
454
  params.currencyIn,
284
455
  params.quote.path,
@@ -379,6 +550,82 @@ function buildSwapUserOp(params) {
379
550
  }
380
551
  });
381
552
  }
553
+ function buildSwapUserOpExactOut(params) {
554
+ if (params.amountOut <= 0n) {
555
+ throw new Error("buildSwapUserOpExactOut: amountOut must be positive");
556
+ }
557
+ if (params.maxAmountIn <= 0n) {
558
+ throw new Error("buildSwapUserOpExactOut: maxAmountIn must be positive");
559
+ }
560
+ if (params.gasFeeAmountInput < 0n) {
561
+ throw new Error(
562
+ "buildSwapUserOpExactOut: gasFeeAmountInput must be non-negative"
563
+ );
564
+ }
565
+ if (params.swapPath.length === 0) {
566
+ throw new Error(
567
+ "buildSwapUserOpExactOut: swapPath must contain at least one PathKey"
568
+ );
569
+ }
570
+ const PERMIT2_EXPIRATION_MAX = 2n ** 48n - 1n;
571
+ if (params.deadline <= 0n || params.deadline > PERMIT2_EXPIRATION_MAX) {
572
+ throw new Error(
573
+ `buildSwapUserOpExactOut: deadline (${params.deadline}) must be unix seconds in (0, 2^48-1]. Did you accidentally pass milliseconds?`
574
+ );
575
+ }
576
+ const { commands, inputs } = buildUniversalRouterExecuteArgsExactOut(
577
+ params.outputTokenAddress,
578
+ params.swapPath,
579
+ params.amountOut,
580
+ params.maxAmountIn,
581
+ params.inputTokenAddress
582
+ );
583
+ const swapCallData = (0, import_viem3.encodeFunctionData)({
584
+ abi: import_core8.universalRouterAbi,
585
+ functionName: "execute",
586
+ args: [commands, inputs, params.deadline]
587
+ });
588
+ const permit2ApproveData = buildPermit2ApprovalCalldata(
589
+ params.inputTokenAddress,
590
+ params.universalRouterAddress,
591
+ params.maxAmountIn,
592
+ Number(params.deadline)
593
+ );
594
+ const operations = [];
595
+ if (params.gasFeeAmountInput > 0n) {
596
+ operations.push(
597
+ (0, import_core8.erc20TransferOp)(
598
+ params.inputTokenAddress,
599
+ params.feeRecipient,
600
+ params.gasFeeAmountInput
601
+ )
602
+ );
603
+ }
604
+ operations.push(
605
+ (0, import_core8.erc20ApproveOp)(params.inputTokenAddress, import_core8.PERMIT2_ADDRESS, params.maxAmountIn),
606
+ (0, import_core8.rawCallOp)(import_core8.PERMIT2_ADDRESS, permit2ApproveData),
607
+ (0, import_core8.rawCallOp)(params.universalRouterAddress, swapCallData)
608
+ );
609
+ return (0, import_core8.buildPartialUserOperation)({
610
+ sender: params.userAddress,
611
+ nonce: params.aaNonce,
612
+ operations,
613
+ gasLimits: {
614
+ // +50k headroom for the additional pre-swap ERC-20 transfer when
615
+ // the input-side fee is non-zero. Keeps margin even when fee=0n.
616
+ callGasLimit: params.gasLimits?.callGasLimit ?? 750000n,
617
+ verificationGasLimit: params.gasLimits?.verificationGasLimit ?? 150000n,
618
+ preVerificationGas: params.gasLimits?.preVerificationGas ?? 50000n
619
+ }
620
+ });
621
+ }
622
+
623
+ // src/swap/slippage.ts
624
+ var MAX_SLIPPAGE_BPS = 5e3;
625
+ function isValidSlippageBps(value) {
626
+ if (value === void 0) return true;
627
+ return Number.isInteger(value) && value >= 0 && value <= MAX_SLIPPAGE_BPS;
628
+ }
382
629
 
383
630
  // src/api/handlers.ts
384
631
  var TradingHandlers = class {
@@ -467,10 +714,9 @@ var TradingHandlers = class {
467
714
  * net. Quote response surfaces both `estimatedOutputAmount` (gross)
468
715
  * and `outputNet` so the FE can display reality.
469
716
  *
470
- * v0.3.1 — `authenticatedAddress` first param. Caller (issuer
471
- * controller / FE proxy) MUST pass the address extracted from the
472
- * verified session/JWT. Handler asserts it equals
473
- * `request.userAddress`. See SDK_CORE_TRADING_AUDIT.md C6.
717
+ * `authenticatedAddress` first param: caller (issuer controller / FE
718
+ * proxy) MUST pass the address extracted from the verified
719
+ * session/JWT. Handler asserts it equals `request.userAddress`.
474
720
  */
475
721
  async handleSwap(authenticatedAddress, request) {
476
722
  if ((0, import_viem4.getAddress)(authenticatedAddress) !== (0, import_viem4.getAddress)(request.userAddress)) {
@@ -492,6 +738,13 @@ var TradingHandlers = class {
492
738
  "handleSwap: amount must be positive"
493
739
  );
494
740
  }
741
+ if (!isValidSlippageBps(request.slippageBps)) {
742
+ throw new import_core9.ValidationError(
743
+ "INVALID_SLIPPAGE",
744
+ `handleSwap: slippageBps (${request.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`,
745
+ { received: request.slippageBps, max: MAX_SLIPPAGE_BPS }
746
+ );
747
+ }
495
748
  const { pafiFeeRecipient } = (0, import_core9.getContractAddresses)(request.chainId);
496
749
  const universalRouter = import_core9.UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
497
750
  if (!universalRouter) {
@@ -579,6 +832,198 @@ var TradingHandlers = class {
579
832
  };
580
833
  }
581
834
  // =========================================================================
835
+ // GET /quote/exact-out — V4 exact-output quote
836
+ // =========================================================================
837
+ /**
838
+ * Quote the input required to receive `request.amount` of the output
839
+ * token via Uniswap V4. Input-side operator fee is auto-quoted, so
840
+ * `inputGross = estimatedInputAmount + feeAmountInput` is the total
841
+ * the user must hold.
842
+ *
843
+ * Returns `quoteError: "QUOTE_UNAVAILABLE"` rather than throwing when
844
+ * no path exists.
845
+ */
846
+ async handleQuoteExactOut(request) {
847
+ if (request.chainId !== this.chainId) {
848
+ throw new import_core9.ValidationError(
849
+ "UNSUPPORTED_CHAIN_ID",
850
+ `handleQuoteExactOut: unsupported chainId ${request.chainId}`,
851
+ { requested: request.chainId, supported: this.chainId }
852
+ );
853
+ }
854
+ if (request.amount === 0n) {
855
+ return {
856
+ outputAmount: 0n,
857
+ estimatedInputAmount: 0n,
858
+ inputGross: 0n,
859
+ feeAmountInput: 0n,
860
+ gasEstimate: 0n
861
+ };
862
+ }
863
+ const inputTokenAddress = (0, import_viem4.getAddress)(request.inputTokenAddress);
864
+ const outputTokenAddress = (0, import_viem4.getAddress)(request.outputTokenAddress);
865
+ const pools = request.pools ?? [];
866
+ try {
867
+ const best = await findBestQuoteExactOut(
868
+ this.provider,
869
+ request.chainId,
870
+ inputTokenAddress,
871
+ outputTokenAddress,
872
+ request.amount,
873
+ pools
874
+ );
875
+ const feeAmountInput = await quoteOperatorFeeInput(
876
+ this.provider,
877
+ request.chainId,
878
+ inputTokenAddress
879
+ ).catch(() => 0n);
880
+ const estimatedInputAmount = best.bestRoute.amountIn;
881
+ return {
882
+ outputAmount: request.amount,
883
+ estimatedInputAmount,
884
+ inputGross: estimatedInputAmount + feeAmountInput,
885
+ feeAmountInput,
886
+ gasEstimate: best.bestRoute.gasEstimate
887
+ };
888
+ } catch {
889
+ return {
890
+ outputAmount: request.amount,
891
+ estimatedInputAmount: 0n,
892
+ inputGross: 0n,
893
+ feeAmountInput: 0n,
894
+ gasEstimate: 0n,
895
+ quoteError: "QUOTE_UNAVAILABLE"
896
+ };
897
+ }
898
+ }
899
+ // =========================================================================
900
+ // POST /swap/exact-out — V4 exact-output swap UserOp
901
+ // =========================================================================
902
+ /**
903
+ * Build a V4 exact-output swap UserOp.
904
+ *
905
+ * Quotes the best exact-output route, applies slippage as a CEILING on
906
+ * `maxAmountIn` (so the cap is never silently tightened by floor
907
+ * division), then encodes a 4-step batch:
908
+ *
909
+ * inputToken.transfer(feeRecipient, gasFeeAmountInput) [if > 0]
910
+ * → input.approve(Permit2, maxAmountIn)
911
+ * → Permit2.approve(router, maxAmountIn)
912
+ * → UniversalRouter.execute (V4 SWAP_EXACT_OUT)
913
+ *
914
+ * Operator fee is INPUT-token-side (charged before swap) so the user
915
+ * receives exactly `request.amount` of output.
916
+ *
917
+ * `authenticatedAddress` first param: caller MUST pass the address
918
+ * extracted from the verified session/JWT. Handler asserts equality
919
+ * with `request.userAddress`.
920
+ */
921
+ async handleSwapExactOut(authenticatedAddress, request) {
922
+ if ((0, import_viem4.getAddress)(authenticatedAddress) !== (0, import_viem4.getAddress)(request.userAddress)) {
923
+ throw new import_core9.ValidationError(
924
+ "USER_ADDRESS_MISMATCH",
925
+ `handleSwapExactOut: authenticatedAddress (${authenticatedAddress}) does not match request.userAddress (${request.userAddress})`
926
+ );
927
+ }
928
+ if (request.chainId !== this.chainId) {
929
+ throw new import_core9.ValidationError(
930
+ "UNSUPPORTED_CHAIN_ID",
931
+ `handleSwapExactOut: unsupported chainId ${request.chainId}`,
932
+ { requested: request.chainId, supported: this.chainId }
933
+ );
934
+ }
935
+ if (request.amount <= 0n) {
936
+ throw new import_core9.ValidationError(
937
+ "INVALID_AMOUNT",
938
+ "handleSwapExactOut: amount must be positive"
939
+ );
940
+ }
941
+ if (!isValidSlippageBps(request.slippageBps)) {
942
+ throw new import_core9.ValidationError(
943
+ "INVALID_SLIPPAGE",
944
+ `handleSwapExactOut: slippageBps (${request.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`,
945
+ { received: request.slippageBps, max: MAX_SLIPPAGE_BPS }
946
+ );
947
+ }
948
+ const { pafiFeeRecipient } = (0, import_core9.getContractAddresses)(request.chainId);
949
+ const universalRouter = import_core9.UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
950
+ if (!universalRouter) {
951
+ throw new import_core9.ValidationError(
952
+ "ROUTER_NOT_DEPLOYED",
953
+ `handleSwapExactOut: no UniversalRouter for chainId ${request.chainId}`
954
+ );
955
+ }
956
+ const inputTokenAddress = (0, import_viem4.getAddress)(request.inputTokenAddress);
957
+ const outputTokenAddress = (0, import_viem4.getAddress)(request.outputTokenAddress);
958
+ const userAddress = (0, import_viem4.getAddress)(request.userAddress);
959
+ const pools = request.pools ?? [];
960
+ let quoteResult;
961
+ try {
962
+ quoteResult = await findBestQuoteExactOut(
963
+ this.provider,
964
+ request.chainId,
965
+ inputTokenAddress,
966
+ outputTokenAddress,
967
+ request.amount,
968
+ pools
969
+ );
970
+ } catch (err) {
971
+ const cause = err instanceof Error ? err.message : String(err);
972
+ throw new import_core9.ValidationError(
973
+ "NO_SWAP_PATH",
974
+ `handleSwapExactOut: no swap path found from ${inputTokenAddress} to ${outputTokenAddress} (cause: ${cause})`
975
+ );
976
+ }
977
+ const gasFeeAmountInput = request.gasFeeAmountInput !== void 0 ? request.gasFeeAmountInput : await quoteOperatorFeeInput(
978
+ this.provider,
979
+ request.chainId,
980
+ inputTokenAddress
981
+ ).catch(() => 0n);
982
+ const hops = quoteResult.bestRoute.path.length;
983
+ const slippageBps = request.slippageBps ?? (hops > 1 ? 100 : 50);
984
+ const estimatedInputAmount = quoteResult.bestRoute.amountIn;
985
+ const slippageNumerator = estimatedInputAmount * BigInt(1e4 + slippageBps);
986
+ const maxAmountIn = (slippageNumerator + 9999n) / 10000n;
987
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
988
+ const userOp = buildSwapUserOpExactOut({
989
+ userAddress,
990
+ aaNonce: request.aaNonce,
991
+ inputTokenAddress,
992
+ outputTokenAddress,
993
+ universalRouterAddress: universalRouter,
994
+ amountOut: request.amount,
995
+ maxAmountIn,
996
+ swapPath: quoteResult.bestRoute.path,
997
+ deadline,
998
+ gasFeeAmountInput,
999
+ feeRecipient: pafiFeeRecipient
1000
+ });
1001
+ const userOpFallback = gasFeeAmountInput > 0n ? buildSwapUserOpExactOut({
1002
+ userAddress,
1003
+ aaNonce: request.aaNonce,
1004
+ inputTokenAddress,
1005
+ outputTokenAddress,
1006
+ universalRouterAddress: universalRouter,
1007
+ amountOut: request.amount,
1008
+ maxAmountIn,
1009
+ swapPath: quoteResult.bestRoute.path,
1010
+ deadline,
1011
+ gasFeeAmountInput: 0n,
1012
+ feeRecipient: pafiFeeRecipient
1013
+ }) : void 0;
1014
+ return {
1015
+ userOp,
1016
+ userOpFallback,
1017
+ outputAmount: request.amount,
1018
+ estimatedInputAmount,
1019
+ maxAmountIn,
1020
+ hops,
1021
+ deadline,
1022
+ feeAmountUsed: gasFeeAmountInput,
1023
+ feeRecipient: pafiFeeRecipient
1024
+ };
1025
+ }
1026
+ // =========================================================================
582
1027
  // POST /perp-deposit
583
1028
  // =========================================================================
584
1029
  /**
@@ -785,27 +1230,407 @@ async function quoteOperatorFeeOutput(provider, chainId, outputTokenAddress) {
785
1230
  pointTokenAddress: outputTokenAddress
786
1231
  });
787
1232
  }
1233
+ async function quoteOperatorFeeInput(provider, chainId, inputTokenAddress) {
1234
+ const { usdt } = (0, import_core9.getContractAddresses)(chainId);
1235
+ if (usdt && (0, import_viem4.getAddress)(inputTokenAddress) === (0, import_viem4.getAddress)(usdt)) {
1236
+ return (0, import_core9.quoteOperatorFeeUsdt)({ provider, chainId });
1237
+ }
1238
+ return (0, import_core9.quoteOperatorFeePt)({
1239
+ provider,
1240
+ chainId,
1241
+ pointTokenAddress: inputTokenAddress
1242
+ });
1243
+ }
788
1244
 
789
1245
  // src/pools.ts
790
1246
  var import_core10 = require("@pafi-dev/core");
1247
+
1248
+ // src/direct/swapDirect.ts
1249
+ var import_core11 = require("@pafi-dev/core");
1250
+ async function swapDirect(params) {
1251
+ const universalRouter = import_core11.UNIVERSAL_ROUTER_ADDRESSES[params.chainId];
1252
+ if (!universalRouter) {
1253
+ throw new Error(`swapDirect: no UniversalRouter for chainId ${params.chainId}`);
1254
+ }
1255
+ if (params.amount <= 0n) {
1256
+ throw new Error("swapDirect: amount must be positive");
1257
+ }
1258
+ if (!isValidSlippageBps(params.slippageBps)) {
1259
+ throw new Error(
1260
+ `swapDirect: slippageBps (${params.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`
1261
+ );
1262
+ }
1263
+ const account = params.walletClient.account;
1264
+ if (!account) {
1265
+ throw new Error(
1266
+ "swapDirect: walletClient has no account attached \u2014 cannot send tx"
1267
+ );
1268
+ }
1269
+ if (account.address.toLowerCase() !== params.userAddress.toLowerCase()) {
1270
+ throw new Error(
1271
+ `swapDirect: walletClient.account.address (${account.address}) must equal userAddress (${params.userAddress}) \u2014 the native tx must be sent from the same EOA whose 7702-delegated bytecode is being executed`
1272
+ );
1273
+ }
1274
+ const code = await params.publicClient.getCode({
1275
+ address: params.userAddress
1276
+ });
1277
+ const delegate = (0, import_core11.parseEip7702DelegatedAddress)(code);
1278
+ if (!delegate) {
1279
+ throw new Error(
1280
+ `swapDirect: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first (user pays a one-time delegation tx) or use the AA path via \`TradingHandlers.handleSwap()\` + sponsor-relayer.`
1281
+ );
1282
+ }
1283
+ const impl = (0, import_core11.detectDelegateImpl)(delegate);
1284
+ if (impl === "unknown") {
1285
+ params.onWarning?.(
1286
+ `swapDirect: user delegated to ${delegate} which is not a PAFI-recognised impl (expected ${import_core11.SIMPLE_7702_IMPL_BASE_MAINNET} or ${import_core11.BATCH_EXECUTOR_7702_IMPL}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
1287
+ );
1288
+ }
1289
+ let quoteResult;
1290
+ try {
1291
+ quoteResult = await findBestQuote(
1292
+ params.publicClient,
1293
+ params.chainId,
1294
+ params.inputTokenAddress,
1295
+ params.outputTokenAddress,
1296
+ params.amount,
1297
+ params.pools ?? []
1298
+ );
1299
+ } catch (err) {
1300
+ const cause = err instanceof Error ? err.message : String(err);
1301
+ throw new Error(
1302
+ `swapDirect: no swap path found from ${params.inputTokenAddress} to ${params.outputTokenAddress} (cause: ${cause})`
1303
+ );
1304
+ }
1305
+ const hops = quoteResult.bestRoute.path.length;
1306
+ const slippageBps = params.slippageBps ?? (hops > 1 ? 100 : 50);
1307
+ const estimatedOutputAmount = quoteResult.bestRoute.amountOut;
1308
+ const minAmountOut = estimatedOutputAmount * BigInt(1e4 - slippageBps) / 10000n;
1309
+ const gasFeeAmountOutput = params.gasFeeAmountOutput ?? 0n;
1310
+ if (gasFeeAmountOutput > 0n && minAmountOut < gasFeeAmountOutput) {
1311
+ throw new Error(
1312
+ `swapDirect: minAmountOut (${minAmountOut}) below operator fee (${gasFeeAmountOutput})`
1313
+ );
1314
+ }
1315
+ const deadline = params.deadline ?? BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
1316
+ const { pafiFeeRecipient } = (0, import_core11.getContractAddresses)(params.chainId);
1317
+ const userOp = buildSwapUserOp({
1318
+ userAddress: params.userAddress,
1319
+ aaNonce: 0n,
1320
+ // ignored on the native-tx path; nonce comes from EOA tx count
1321
+ inputTokenAddress: params.inputTokenAddress,
1322
+ outputTokenAddress: params.outputTokenAddress,
1323
+ universalRouterAddress: universalRouter,
1324
+ amountIn: params.amount,
1325
+ minAmountOut,
1326
+ swapPath: quoteResult.bestRoute.path,
1327
+ deadline,
1328
+ gasFeeAmountOutput,
1329
+ feeRecipient: pafiFeeRecipient
1330
+ });
1331
+ const txHash = await params.walletClient.sendTransaction({
1332
+ account,
1333
+ chain: params.walletClient.chain,
1334
+ to: params.userAddress,
1335
+ value: 0n,
1336
+ data: userOp.callData
1337
+ });
1338
+ let receipt;
1339
+ if (params.waitForReceipt !== false) {
1340
+ try {
1341
+ receipt = await params.publicClient.waitForTransactionReceipt({
1342
+ hash: txHash
1343
+ });
1344
+ } catch (err) {
1345
+ params.onWarning?.(
1346
+ `swapDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
1347
+ );
1348
+ }
1349
+ }
1350
+ return {
1351
+ txHash,
1352
+ receipt,
1353
+ estimatedOutputAmount,
1354
+ minAmountOut,
1355
+ hops,
1356
+ deadline,
1357
+ feeAmountUsed: gasFeeAmountOutput
1358
+ };
1359
+ }
1360
+
1361
+ // src/direct/swapDirectExactOut.ts
1362
+ var import_core12 = require("@pafi-dev/core");
1363
+ async function swapDirectExactOut(params) {
1364
+ const universalRouter = import_core12.UNIVERSAL_ROUTER_ADDRESSES[params.chainId];
1365
+ if (!universalRouter) {
1366
+ throw new Error(
1367
+ `swapDirectExactOut: no UniversalRouter for chainId ${params.chainId}`
1368
+ );
1369
+ }
1370
+ if (params.amount <= 0n) {
1371
+ throw new Error("swapDirectExactOut: amount must be positive");
1372
+ }
1373
+ if (!isValidSlippageBps(params.slippageBps)) {
1374
+ throw new Error(
1375
+ `swapDirectExactOut: slippageBps (${params.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`
1376
+ );
1377
+ }
1378
+ const account = params.walletClient.account;
1379
+ if (!account) {
1380
+ throw new Error(
1381
+ "swapDirectExactOut: walletClient has no account attached \u2014 cannot send tx"
1382
+ );
1383
+ }
1384
+ if (account.address.toLowerCase() !== params.userAddress.toLowerCase()) {
1385
+ throw new Error(
1386
+ `swapDirectExactOut: walletClient.account.address (${account.address}) must equal userAddress (${params.userAddress}) \u2014 the native tx must be sent from the same EOA whose 7702-delegated bytecode is being executed`
1387
+ );
1388
+ }
1389
+ const code = await params.publicClient.getCode({
1390
+ address: params.userAddress
1391
+ });
1392
+ const delegate = (0, import_core12.parseEip7702DelegatedAddress)(code);
1393
+ if (!delegate) {
1394
+ throw new Error(
1395
+ `swapDirectExactOut: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first or use the AA path via \`TradingHandlers.handleSwapExactOut()\` + sponsor-relayer.`
1396
+ );
1397
+ }
1398
+ const impl = (0, import_core12.detectDelegateImpl)(delegate);
1399
+ if (impl === "unknown") {
1400
+ params.onWarning?.(
1401
+ `swapDirectExactOut: user delegated to ${delegate} which is not a PAFI-recognised impl (expected ${import_core12.SIMPLE_7702_IMPL_BASE_MAINNET} or ${import_core12.BATCH_EXECUTOR_7702_IMPL}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
1402
+ );
1403
+ }
1404
+ let quoteResult;
1405
+ try {
1406
+ quoteResult = await findBestQuoteExactOut(
1407
+ params.publicClient,
1408
+ params.chainId,
1409
+ params.inputTokenAddress,
1410
+ params.outputTokenAddress,
1411
+ params.amount,
1412
+ params.pools ?? []
1413
+ );
1414
+ } catch (err) {
1415
+ const cause = err instanceof Error ? err.message : String(err);
1416
+ throw new Error(
1417
+ `swapDirectExactOut: no swap path found from ${params.inputTokenAddress} to ${params.outputTokenAddress} (cause: ${cause})`
1418
+ );
1419
+ }
1420
+ const hops = quoteResult.bestRoute.path.length;
1421
+ const slippageBps = params.slippageBps ?? (hops > 1 ? 100 : 50);
1422
+ const estimatedInputAmount = quoteResult.bestRoute.amountIn;
1423
+ const slippageNumerator = estimatedInputAmount * BigInt(1e4 + slippageBps);
1424
+ const maxAmountIn = (slippageNumerator + 9999n) / 10000n;
1425
+ const gasFeeAmountInput = params.gasFeeAmountInput ?? 0n;
1426
+ const deadline = params.deadline ?? BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
1427
+ const { pafiFeeRecipient } = (0, import_core12.getContractAddresses)(params.chainId);
1428
+ const userOp = buildSwapUserOpExactOut({
1429
+ userAddress: params.userAddress,
1430
+ aaNonce: 0n,
1431
+ // ignored on the native-tx path; nonce comes from EOA tx count
1432
+ inputTokenAddress: params.inputTokenAddress,
1433
+ outputTokenAddress: params.outputTokenAddress,
1434
+ universalRouterAddress: universalRouter,
1435
+ amountOut: params.amount,
1436
+ maxAmountIn,
1437
+ swapPath: quoteResult.bestRoute.path,
1438
+ deadline,
1439
+ gasFeeAmountInput,
1440
+ feeRecipient: pafiFeeRecipient
1441
+ });
1442
+ const txHash = await params.walletClient.sendTransaction({
1443
+ account,
1444
+ chain: params.walletClient.chain,
1445
+ to: params.userAddress,
1446
+ value: 0n,
1447
+ data: userOp.callData
1448
+ });
1449
+ let receipt;
1450
+ if (params.waitForReceipt !== false) {
1451
+ try {
1452
+ receipt = await params.publicClient.waitForTransactionReceipt({
1453
+ hash: txHash
1454
+ });
1455
+ } catch (err) {
1456
+ params.onWarning?.(
1457
+ `swapDirectExactOut: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
1458
+ );
1459
+ }
1460
+ }
1461
+ return {
1462
+ txHash,
1463
+ receipt,
1464
+ outputAmount: params.amount,
1465
+ estimatedInputAmount,
1466
+ maxAmountIn,
1467
+ hops,
1468
+ deadline,
1469
+ feeAmountUsed: gasFeeAmountInput
1470
+ };
1471
+ }
1472
+
1473
+ // src/direct/perpDepositDirect.ts
1474
+ var import_core13 = require("@pafi-dev/core");
1475
+ async function perpDepositDirect(params) {
1476
+ if (params.amount <= 0n) {
1477
+ throw new Error("perpDepositDirect: amount must be positive");
1478
+ }
1479
+ const code = await params.publicClient.getCode({
1480
+ address: params.userAddress
1481
+ });
1482
+ const delegate = (0, import_core13.parseEip7702DelegatedAddress)(code);
1483
+ if (!delegate) {
1484
+ throw new Error(
1485
+ `perpDepositDirect: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first or use the AA path via \`TradingHandlers.handlePerpDeposit()\` + sponsor-relayer.`
1486
+ );
1487
+ }
1488
+ const impl = (0, import_core13.detectDelegateImpl)(delegate);
1489
+ if (impl === "unknown") {
1490
+ params.onWarning?.(
1491
+ `perpDepositDirect: user delegated to ${delegate} (not a PAFI-recognised impl ${import_core13.SIMPLE_7702_IMPL_BASE_MAINNET} / ${import_core13.BATCH_EXECUTOR_7702_IMPL}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
1492
+ );
1493
+ }
1494
+ const vault = import_core13.ORDERLY_VAULT_ADDRESSES[params.chainId];
1495
+ if (!vault) {
1496
+ throw new Error(
1497
+ `perpDepositDirect: no Orderly Vault for chainId ${params.chainId}`
1498
+ );
1499
+ }
1500
+ const brokerHash = import_core13.BROKER_HASHES[params.brokerId];
1501
+ if (!brokerHash) {
1502
+ throw new Error(
1503
+ `perpDepositDirect: unknown brokerId "${params.brokerId}"`
1504
+ );
1505
+ }
1506
+ const tokenHash = import_core13.TOKEN_HASHES.USDC;
1507
+ const [usdcAddress, brokerAllowed] = await Promise.all([
1508
+ params.publicClient.readContract({
1509
+ address: vault,
1510
+ abi: import_core13.ORDERLY_VAULT_ABI,
1511
+ functionName: "getAllowedToken",
1512
+ args: [tokenHash]
1513
+ }),
1514
+ params.publicClient.readContract({
1515
+ address: vault,
1516
+ abi: import_core13.ORDERLY_VAULT_ABI,
1517
+ functionName: "getAllowedBroker",
1518
+ args: [brokerHash]
1519
+ })
1520
+ ]);
1521
+ if (!brokerAllowed) {
1522
+ throw new Error(
1523
+ `perpDepositDirect: broker "${params.brokerId}" is not whitelisted on Orderly Vault`
1524
+ );
1525
+ }
1526
+ const { orderlyRelay: relayAddress, pafiFeeRecipient } = (0, import_core13.getContractAddresses)(
1527
+ params.chainId
1528
+ );
1529
+ const RELAY_FEE_FLOOR_USDC = 2000000n;
1530
+ const percentCap = params.amount * 500n / 10000n;
1531
+ const maxFee = params.maxRelayFee ?? (percentCap > RELAY_FEE_FLOOR_USDC ? percentCap : RELAY_FEE_FLOOR_USDC);
1532
+ const relayRequest = {
1533
+ token: usdcAddress,
1534
+ receiver: params.userAddress,
1535
+ brokerHash,
1536
+ totalAmount: params.amount,
1537
+ maxFee
1538
+ };
1539
+ const relayTokenFee = await params.publicClient.readContract({
1540
+ address: relayAddress,
1541
+ abi: import_core13.ORDERLY_RELAY_ABI,
1542
+ functionName: "quoteTokenFee",
1543
+ args: [relayRequest]
1544
+ });
1545
+ if (relayTokenFee > maxFee) {
1546
+ throw new Error(
1547
+ `perpDepositDirect: Relay tokenFee ${relayTokenFee} exceeds maxFee ${maxFee} \u2014 pass a larger \`maxRelayFee\` or increase \`amount\`.`
1548
+ );
1549
+ }
1550
+ if (relayTokenFee >= params.amount) {
1551
+ throw new Error(
1552
+ `perpDepositDirect: deposit amount ${params.amount} below Relay fee ${relayTokenFee} \u2014 increase \`amount\`.`
1553
+ );
1554
+ }
1555
+ const gasFeeUsdc = params.gasFeeUsdc ?? 0n;
1556
+ const partial = (0, import_core13.buildPerpDepositViaRelay)({
1557
+ userAddress: params.userAddress,
1558
+ aaNonce: 0n,
1559
+ // ignored on the native-tx path
1560
+ relayAddress,
1561
+ request: relayRequest,
1562
+ gasFeeUsdc: gasFeeUsdc > 0n ? gasFeeUsdc : void 0,
1563
+ gasFeeUsdcRecipient: gasFeeUsdc > 0n ? pafiFeeRecipient : void 0
1564
+ });
1565
+ const account = params.walletClient.account;
1566
+ if (!account) {
1567
+ throw new Error(
1568
+ "perpDepositDirect: walletClient has no account attached \u2014 cannot send tx"
1569
+ );
1570
+ }
1571
+ const txHash = await params.walletClient.sendTransaction({
1572
+ account,
1573
+ chain: params.walletClient.chain,
1574
+ to: params.userAddress,
1575
+ value: 0n,
1576
+ data: partial.callData
1577
+ });
1578
+ let receipt;
1579
+ if (params.waitForReceipt !== false) {
1580
+ try {
1581
+ receipt = await params.publicClient.waitForTransactionReceipt({
1582
+ hash: txHash
1583
+ });
1584
+ } catch (err) {
1585
+ params.onWarning?.(
1586
+ `perpDepositDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
1587
+ );
1588
+ }
1589
+ }
1590
+ const accountId = (0, import_core13.computeAccountId)(params.userAddress, brokerHash);
1591
+ return {
1592
+ txHash,
1593
+ receipt,
1594
+ relayTokenFee,
1595
+ maxFee,
1596
+ netDeposit: params.amount - relayTokenFee,
1597
+ feeAmountUsed: gasFeeUsdc,
1598
+ accountId,
1599
+ brokerHash,
1600
+ usdcAddress,
1601
+ relayAddress
1602
+ };
1603
+ }
791
1604
  // Annotate the CommonJS export names for ESM import in node:
792
1605
  0 && (module.exports = {
793
1606
  PAFI_SUBGRAPH_URL,
1607
+ SWAP_EXACT_OUT,
1608
+ SWAP_EXACT_OUT_SINGLE,
794
1609
  TradingHandlers,
795
1610
  buildAllPaths,
796
1611
  buildErc20ApprovalCalldata,
797
1612
  buildPermit2ApprovalCalldata,
798
1613
  buildSwapFromQuote,
799
1614
  buildSwapUserOp,
1615
+ buildSwapUserOpExactOut,
800
1616
  buildUniversalRouterExecuteArgs,
1617
+ buildUniversalRouterExecuteArgsExactOut,
801
1618
  buildV4SwapInput,
1619
+ buildV4SwapInputExactOut,
802
1620
  checkAllowance,
803
1621
  combineRoutes,
804
1622
  fetchPafiPools,
805
1623
  findBestQuote,
1624
+ findBestQuoteExactOut,
1625
+ perpDepositDirect,
806
1626
  quoteBestRoute,
1627
+ quoteBestRouteExactOut,
807
1628
  quoteExactInput,
808
1629
  quoteExactInputSingle,
809
- simulateSwap
1630
+ quoteExactOutput,
1631
+ quoteExactOutputSingle,
1632
+ simulateSwap,
1633
+ swapDirect,
1634
+ swapDirectExactOut
810
1635
  });
811
1636
  //# sourceMappingURL=index.cjs.map