@pafi-dev/trading 0.4.0 → 0.4.2

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,24 +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,
36
42
  perpDepositDirect: () => perpDepositDirect,
37
43
  quoteBestRoute: () => quoteBestRoute,
44
+ quoteBestRouteExactOut: () => quoteBestRouteExactOut,
38
45
  quoteExactInput: () => quoteExactInput,
39
46
  quoteExactInputSingle: () => quoteExactInputSingle,
47
+ quoteExactOutput: () => quoteExactOutput,
48
+ quoteExactOutputSingle: () => quoteExactOutputSingle,
40
49
  simulateSwap: () => simulateSwap,
41
- swapDirect: () => swapDirect
50
+ swapDirect: () => swapDirect,
51
+ swapDirectExactOut: () => swapDirectExactOut
42
52
  });
43
53
  module.exports = __toCommonJS(index_exports);
44
54
 
@@ -101,7 +111,7 @@ async function quoteExactInput(client, quoterAddress, exactCurrency, path, exact
101
111
  address: quoterAddress,
102
112
  abi: import_core2.v4QuoterAbi,
103
113
  functionName: "quoteExactInput",
104
- args: [{ exactCurrency, path, exactAmount: BigInt(exactAmount) }]
114
+ args: [{ exactCurrency, path, exactAmount }]
105
115
  });
106
116
  return { amountOut, gasEstimate, path };
107
117
  }
@@ -114,7 +124,7 @@ async function quoteExactInputSingle(client, quoterAddress, poolKey, zeroForOne,
114
124
  {
115
125
  poolKey,
116
126
  zeroForOne,
117
- exactAmount: BigInt(exactAmount),
127
+ exactAmount,
118
128
  hookData
119
129
  }
120
130
  ]
@@ -131,13 +141,7 @@ async function quoteBestRoute(client, quoterAddress, exactCurrency, routes, exac
131
141
  {
132
142
  exactCurrency,
133
143
  path,
134
- // `as unknown as number` cast — the V4 quoter's solidity
135
- // signature takes `uint256` (a bigint at the wire level), but
136
- // our pinned ABI types it as `number`. The runtime is fine
137
- // (bigint serializes correctly) — this cast tells TS to skip
138
- // the structural check until we re-generate the ABI with the
139
- // correct uint256 type. Tracked: SDK_CORE_TRADING_AUDIT.md H11.
140
- exactAmount: BigInt(exactAmount)
144
+ exactAmount
141
145
  }
142
146
  ]
143
147
  })),
@@ -178,6 +182,84 @@ async function findBestQuote(client, chainId, tokenIn, tokenOut, exactAmount, po
178
182
  }
179
183
  return quoteBestRoute(client, quoter, tokenIn, paths, exactAmount);
180
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
+ }
181
263
 
182
264
  // src/swap/approval.ts
183
265
  var import_viem = require("viem");
@@ -210,8 +292,11 @@ function buildPermit2ApprovalCalldata(token, spender, amount, expiration) {
210
292
  var import_viem2 = require("viem");
211
293
  var V4_SWAP = 16;
212
294
  var SWAP_EXACT_IN = 7;
295
+ var SWAP_EXACT_OUT_SINGLE = 8;
296
+ var SWAP_EXACT_OUT = 9;
213
297
  var SETTLE_ALL = 12;
214
298
  var TAKE_ALL = 15;
299
+ var UINT128_MAX = 2n ** 128n - 1n;
215
300
  var PATH_KEY_ABI_COMPONENTS = [
216
301
  { name: "intermediateCurrency", type: "address" },
217
302
  { name: "fee", type: "uint256" },
@@ -229,6 +314,16 @@ var EXACT_INPUT_PARAMS_ABI = [
229
314
  { name: "amountIn", type: "uint128" },
230
315
  { name: "amountOutMinimum", type: "uint128" }
231
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
+ ];
232
327
  function buildV4SwapInput(currencyIn, path, amountIn, minAmountOut, outputCurrency) {
233
328
  const actions = (0, import_viem2.encodePacked)(
234
329
  ["uint8", "uint8", "uint8"],
@@ -280,7 +375,81 @@ function buildUniversalRouterExecuteArgs(currencyIn, path, amountIn, minAmountOu
280
375
  ];
281
376
  return { commands, inputs };
282
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;
283
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
+ }
284
453
  return buildUniversalRouterExecuteArgs(
285
454
  params.currencyIn,
286
455
  params.quote.path,
@@ -381,6 +550,82 @@ function buildSwapUserOp(params) {
381
550
  }
382
551
  });
383
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
+ }
384
629
 
385
630
  // src/api/handlers.ts
386
631
  var TradingHandlers = class {
@@ -469,10 +714,9 @@ var TradingHandlers = class {
469
714
  * net. Quote response surfaces both `estimatedOutputAmount` (gross)
470
715
  * and `outputNet` so the FE can display reality.
471
716
  *
472
- * v0.3.1 — `authenticatedAddress` first param. Caller (issuer
473
- * controller / FE proxy) MUST pass the address extracted from the
474
- * verified session/JWT. Handler asserts it equals
475
- * `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`.
476
720
  */
477
721
  async handleSwap(authenticatedAddress, request) {
478
722
  if ((0, import_viem4.getAddress)(authenticatedAddress) !== (0, import_viem4.getAddress)(request.userAddress)) {
@@ -494,6 +738,13 @@ var TradingHandlers = class {
494
738
  "handleSwap: amount must be positive"
495
739
  );
496
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
+ }
497
748
  const { pafiFeeRecipient } = (0, import_core9.getContractAddresses)(request.chainId);
498
749
  const universalRouter = import_core9.UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
499
750
  if (!universalRouter) {
@@ -581,6 +832,198 @@ var TradingHandlers = class {
581
832
  };
582
833
  }
583
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
+ // =========================================================================
584
1027
  // POST /perp-deposit
585
1028
  // =========================================================================
586
1029
  /**
@@ -787,6 +1230,17 @@ async function quoteOperatorFeeOutput(provider, chainId, outputTokenAddress) {
787
1230
  pointTokenAddress: outputTokenAddress
788
1231
  });
789
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
+ }
790
1244
 
791
1245
  // src/pools.ts
792
1246
  var import_core10 = require("@pafi-dev/core");
@@ -794,6 +1248,29 @@ var import_core10 = require("@pafi-dev/core");
794
1248
  // src/direct/swapDirect.ts
795
1249
  var import_core11 = require("@pafi-dev/core");
796
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
+ }
797
1274
  const code = await params.publicClient.getCode({
798
1275
  address: params.userAddress
799
1276
  });
@@ -809,13 +1286,6 @@ async function swapDirect(params) {
809
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.`
810
1287
  );
811
1288
  }
812
- const universalRouter = import_core11.UNIVERSAL_ROUTER_ADDRESSES[params.chainId];
813
- if (!universalRouter) {
814
- throw new Error(`swapDirect: no UniversalRouter for chainId ${params.chainId}`);
815
- }
816
- if (params.amount <= 0n) {
817
- throw new Error("swapDirect: amount must be positive");
818
- }
819
1289
  let quoteResult;
820
1290
  try {
821
1291
  quoteResult = await findBestQuote(
@@ -858,12 +1328,117 @@ async function swapDirect(params) {
858
1328
  gasFeeAmountOutput,
859
1329
  feeRecipient: pafiFeeRecipient
860
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
+ }
861
1378
  const account = params.walletClient.account;
862
1379
  if (!account) {
863
1380
  throw new Error(
864
- "swapDirect: walletClient has no account attached \u2014 cannot send tx"
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.`
865
1402
  );
866
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
+ });
867
1442
  const txHash = await params.walletClient.sendTransaction({
868
1443
  account,
869
1444
  chain: params.walletClient.chain,
@@ -879,23 +1454,24 @@ async function swapDirect(params) {
879
1454
  });
880
1455
  } catch (err) {
881
1456
  params.onWarning?.(
882
- `swapDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
1457
+ `swapDirectExactOut: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
883
1458
  );
884
1459
  }
885
1460
  }
886
1461
  return {
887
1462
  txHash,
888
1463
  receipt,
889
- estimatedOutputAmount,
890
- minAmountOut,
1464
+ outputAmount: params.amount,
1465
+ estimatedInputAmount,
1466
+ maxAmountIn,
891
1467
  hops,
892
1468
  deadline,
893
- feeAmountUsed: gasFeeAmountOutput
1469
+ feeAmountUsed: gasFeeAmountInput
894
1470
  };
895
1471
  }
896
1472
 
897
1473
  // src/direct/perpDepositDirect.ts
898
- var import_core12 = require("@pafi-dev/core");
1474
+ var import_core13 = require("@pafi-dev/core");
899
1475
  async function perpDepositDirect(params) {
900
1476
  if (params.amount <= 0n) {
901
1477
  throw new Error("perpDepositDirect: amount must be positive");
@@ -903,41 +1479,41 @@ async function perpDepositDirect(params) {
903
1479
  const code = await params.publicClient.getCode({
904
1480
  address: params.userAddress
905
1481
  });
906
- const delegate = (0, import_core12.parseEip7702DelegatedAddress)(code);
1482
+ const delegate = (0, import_core13.parseEip7702DelegatedAddress)(code);
907
1483
  if (!delegate) {
908
1484
  throw new Error(
909
1485
  `perpDepositDirect: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first or use the AA path via \`TradingHandlers.handlePerpDeposit()\` + sponsor-relayer.`
910
1486
  );
911
1487
  }
912
- const impl = (0, import_core12.detectDelegateImpl)(delegate);
1488
+ const impl = (0, import_core13.detectDelegateImpl)(delegate);
913
1489
  if (impl === "unknown") {
914
1490
  params.onWarning?.(
915
- `perpDepositDirect: user delegated to ${delegate} (not a PAFI-recognised impl ${import_core12.SIMPLE_7702_IMPL_BASE_MAINNET} / ${import_core12.BATCH_EXECUTOR_7702_IMPL}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
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.`
916
1492
  );
917
1493
  }
918
- const vault = import_core12.ORDERLY_VAULT_ADDRESSES[params.chainId];
1494
+ const vault = import_core13.ORDERLY_VAULT_ADDRESSES[params.chainId];
919
1495
  if (!vault) {
920
1496
  throw new Error(
921
1497
  `perpDepositDirect: no Orderly Vault for chainId ${params.chainId}`
922
1498
  );
923
1499
  }
924
- const brokerHash = import_core12.BROKER_HASHES[params.brokerId];
1500
+ const brokerHash = import_core13.BROKER_HASHES[params.brokerId];
925
1501
  if (!brokerHash) {
926
1502
  throw new Error(
927
1503
  `perpDepositDirect: unknown brokerId "${params.brokerId}"`
928
1504
  );
929
1505
  }
930
- const tokenHash = import_core12.TOKEN_HASHES.USDC;
1506
+ const tokenHash = import_core13.TOKEN_HASHES.USDC;
931
1507
  const [usdcAddress, brokerAllowed] = await Promise.all([
932
1508
  params.publicClient.readContract({
933
1509
  address: vault,
934
- abi: import_core12.ORDERLY_VAULT_ABI,
1510
+ abi: import_core13.ORDERLY_VAULT_ABI,
935
1511
  functionName: "getAllowedToken",
936
1512
  args: [tokenHash]
937
1513
  }),
938
1514
  params.publicClient.readContract({
939
1515
  address: vault,
940
- abi: import_core12.ORDERLY_VAULT_ABI,
1516
+ abi: import_core13.ORDERLY_VAULT_ABI,
941
1517
  functionName: "getAllowedBroker",
942
1518
  args: [brokerHash]
943
1519
  })
@@ -947,7 +1523,7 @@ async function perpDepositDirect(params) {
947
1523
  `perpDepositDirect: broker "${params.brokerId}" is not whitelisted on Orderly Vault`
948
1524
  );
949
1525
  }
950
- const { orderlyRelay: relayAddress, pafiFeeRecipient } = (0, import_core12.getContractAddresses)(
1526
+ const { orderlyRelay: relayAddress, pafiFeeRecipient } = (0, import_core13.getContractAddresses)(
951
1527
  params.chainId
952
1528
  );
953
1529
  const RELAY_FEE_FLOOR_USDC = 2000000n;
@@ -962,7 +1538,7 @@ async function perpDepositDirect(params) {
962
1538
  };
963
1539
  const relayTokenFee = await params.publicClient.readContract({
964
1540
  address: relayAddress,
965
- abi: import_core12.ORDERLY_RELAY_ABI,
1541
+ abi: import_core13.ORDERLY_RELAY_ABI,
966
1542
  functionName: "quoteTokenFee",
967
1543
  args: [relayRequest]
968
1544
  });
@@ -977,7 +1553,7 @@ async function perpDepositDirect(params) {
977
1553
  );
978
1554
  }
979
1555
  const gasFeeUsdc = params.gasFeeUsdc ?? 0n;
980
- const partial = (0, import_core12.buildPerpDepositViaRelay)({
1556
+ const partial = (0, import_core13.buildPerpDepositViaRelay)({
981
1557
  userAddress: params.userAddress,
982
1558
  aaNonce: 0n,
983
1559
  // ignored on the native-tx path
@@ -1011,7 +1587,7 @@ async function perpDepositDirect(params) {
1011
1587
  );
1012
1588
  }
1013
1589
  }
1014
- const accountId = (0, import_core12.computeAccountId)(params.userAddress, brokerHash);
1590
+ const accountId = (0, import_core13.computeAccountId)(params.userAddress, brokerHash);
1015
1591
  return {
1016
1592
  txHash,
1017
1593
  receipt,
@@ -1028,23 +1604,33 @@ async function perpDepositDirect(params) {
1028
1604
  // Annotate the CommonJS export names for ESM import in node:
1029
1605
  0 && (module.exports = {
1030
1606
  PAFI_SUBGRAPH_URL,
1607
+ SWAP_EXACT_OUT,
1608
+ SWAP_EXACT_OUT_SINGLE,
1031
1609
  TradingHandlers,
1032
1610
  buildAllPaths,
1033
1611
  buildErc20ApprovalCalldata,
1034
1612
  buildPermit2ApprovalCalldata,
1035
1613
  buildSwapFromQuote,
1036
1614
  buildSwapUserOp,
1615
+ buildSwapUserOpExactOut,
1037
1616
  buildUniversalRouterExecuteArgs,
1617
+ buildUniversalRouterExecuteArgsExactOut,
1038
1618
  buildV4SwapInput,
1619
+ buildV4SwapInputExactOut,
1039
1620
  checkAllowance,
1040
1621
  combineRoutes,
1041
1622
  fetchPafiPools,
1042
1623
  findBestQuote,
1624
+ findBestQuoteExactOut,
1043
1625
  perpDepositDirect,
1044
1626
  quoteBestRoute,
1627
+ quoteBestRouteExactOut,
1045
1628
  quoteExactInput,
1046
1629
  quoteExactInputSingle,
1630
+ quoteExactOutput,
1631
+ quoteExactOutputSingle,
1047
1632
  simulateSwap,
1048
- swapDirect
1633
+ swapDirect,
1634
+ swapDirectExactOut
1049
1635
  });
1050
1636
  //# sourceMappingURL=index.cjs.map