@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.js CHANGED
@@ -71,7 +71,7 @@ async function quoteExactInput(client, quoterAddress, exactCurrency, path, exact
71
71
  address: quoterAddress,
72
72
  abi: v4QuoterAbi,
73
73
  functionName: "quoteExactInput",
74
- args: [{ exactCurrency, path, exactAmount: BigInt(exactAmount) }]
74
+ args: [{ exactCurrency, path, exactAmount }]
75
75
  });
76
76
  return { amountOut, gasEstimate, path };
77
77
  }
@@ -84,7 +84,7 @@ async function quoteExactInputSingle(client, quoterAddress, poolKey, zeroForOne,
84
84
  {
85
85
  poolKey,
86
86
  zeroForOne,
87
- exactAmount: BigInt(exactAmount),
87
+ exactAmount,
88
88
  hookData
89
89
  }
90
90
  ]
@@ -101,13 +101,7 @@ async function quoteBestRoute(client, quoterAddress, exactCurrency, routes, exac
101
101
  {
102
102
  exactCurrency,
103
103
  path,
104
- // `as unknown as number` cast — the V4 quoter's solidity
105
- // signature takes `uint256` (a bigint at the wire level), but
106
- // our pinned ABI types it as `number`. The runtime is fine
107
- // (bigint serializes correctly) — this cast tells TS to skip
108
- // the structural check until we re-generate the ABI with the
109
- // correct uint256 type. Tracked: SDK_CORE_TRADING_AUDIT.md H11.
110
- exactAmount: BigInt(exactAmount)
104
+ exactAmount
111
105
  }
112
106
  ]
113
107
  })),
@@ -148,6 +142,84 @@ async function findBestQuote(client, chainId, tokenIn, tokenOut, exactAmount, po
148
142
  }
149
143
  return quoteBestRoute(client, quoter, tokenIn, paths, exactAmount);
150
144
  }
145
+ async function quoteExactOutput(client, quoterAddress, exactCurrency, path, exactAmount) {
146
+ const [amountIn, gasEstimate] = await client.readContract({
147
+ address: quoterAddress,
148
+ abi: v4QuoterAbi,
149
+ functionName: "quoteExactOutput",
150
+ args: [{ exactCurrency, path, exactAmount }]
151
+ });
152
+ return { amountIn, gasEstimate, path };
153
+ }
154
+ async function quoteExactOutputSingle(client, quoterAddress, poolKey, zeroForOne, exactAmount, hookData) {
155
+ const [amountIn, gasEstimate] = await client.readContract({
156
+ address: quoterAddress,
157
+ abi: v4QuoterAbi,
158
+ functionName: "quoteExactOutputSingle",
159
+ args: [
160
+ {
161
+ poolKey,
162
+ zeroForOne,
163
+ exactAmount,
164
+ hookData
165
+ }
166
+ ]
167
+ });
168
+ return { amountIn, gasEstimate };
169
+ }
170
+ async function quoteBestRouteExactOut(client, quoterAddress, exactCurrency, routes, exactAmount) {
171
+ const results = await client.multicall({
172
+ contracts: routes.map((path) => ({
173
+ address: quoterAddress,
174
+ abi: v4QuoterAbi,
175
+ functionName: "quoteExactOutput",
176
+ args: [
177
+ {
178
+ exactCurrency,
179
+ path,
180
+ exactAmount
181
+ }
182
+ ]
183
+ })),
184
+ allowFailure: true
185
+ });
186
+ const allRoutes = [];
187
+ let firstFailure;
188
+ for (let i = 0; i < results.length; i++) {
189
+ const r = results[i];
190
+ if (r.status === "success") {
191
+ const [amountIn, gasEstimate] = r.result;
192
+ allRoutes.push({ amountIn, gasEstimate, path: routes[i] });
193
+ } else if (firstFailure === void 0) {
194
+ const errMsg = r.error instanceof Error ? r.error.message : String(r.error ?? "unknown");
195
+ firstFailure = errMsg;
196
+ }
197
+ }
198
+ if (allRoutes.length === 0) {
199
+ throw new Error(
200
+ `No valid exact-output routes found to ${exactCurrency} (${routes.length} candidates probed)` + (firstFailure ? `; first failure: ${firstFailure}` : "")
201
+ );
202
+ }
203
+ const bestRoute = allRoutes.reduce(
204
+ (best, current) => current.amountIn < best.amountIn ? current : best
205
+ );
206
+ return { bestRoute, allRoutes };
207
+ }
208
+ async function findBestQuoteExactOut(client, chainId, tokenIn, tokenOut, exactAmount, pools = [], quoterAddress, maxHops = 3) {
209
+ const quoter = quoterAddress ?? V4_QUOTER_ADDRESSES[chainId];
210
+ if (!quoter) {
211
+ throw new Error(`No V4 Quoter address configured for chain ${chainId}`);
212
+ }
213
+ const commonPools = COMMON_POOLS2[chainId] ?? [];
214
+ const allPools = [...pools, ...commonPools];
215
+ const paths = buildAllPaths(allPools, tokenOut, tokenIn, maxHops);
216
+ if (paths.length === 0) {
217
+ throw new Error(
218
+ `No exact-output paths found to ${tokenOut} from ${tokenIn}`
219
+ );
220
+ }
221
+ return quoteBestRouteExactOut(client, quoter, tokenOut, paths, exactAmount);
222
+ }
151
223
 
152
224
  // src/swap/approval.ts
153
225
  import { encodeFunctionData } from "viem";
@@ -180,8 +252,11 @@ function buildPermit2ApprovalCalldata(token, spender, amount, expiration) {
180
252
  import { encodeAbiParameters, encodePacked } from "viem";
181
253
  var V4_SWAP = 16;
182
254
  var SWAP_EXACT_IN = 7;
255
+ var SWAP_EXACT_OUT_SINGLE = 8;
256
+ var SWAP_EXACT_OUT = 9;
183
257
  var SETTLE_ALL = 12;
184
258
  var TAKE_ALL = 15;
259
+ var UINT128_MAX = 2n ** 128n - 1n;
185
260
  var PATH_KEY_ABI_COMPONENTS = [
186
261
  { name: "intermediateCurrency", type: "address" },
187
262
  { name: "fee", type: "uint256" },
@@ -199,6 +274,16 @@ var EXACT_INPUT_PARAMS_ABI = [
199
274
  { name: "amountIn", type: "uint128" },
200
275
  { name: "amountOutMinimum", type: "uint128" }
201
276
  ];
277
+ var EXACT_OUTPUT_PARAMS_ABI = [
278
+ { name: "currencyOut", type: "address" },
279
+ {
280
+ name: "path",
281
+ type: "tuple[]",
282
+ components: PATH_KEY_ABI_COMPONENTS
283
+ },
284
+ { name: "amountOut", type: "uint128" },
285
+ { name: "amountInMaximum", type: "uint128" }
286
+ ];
202
287
  function buildV4SwapInput(currencyIn, path, amountIn, minAmountOut, outputCurrency) {
203
288
  const actions = encodePacked(
204
289
  ["uint8", "uint8", "uint8"],
@@ -250,7 +335,81 @@ function buildUniversalRouterExecuteArgs(currencyIn, path, amountIn, minAmountOu
250
335
  ];
251
336
  return { commands, inputs };
252
337
  }
338
+ function buildV4SwapInputExactOut(currencyOut, path, amountOut, maxAmountIn, inputCurrency) {
339
+ if (amountOut <= 0n || amountOut > UINT128_MAX) {
340
+ throw new Error(
341
+ `buildV4SwapInputExactOut: amountOut (${amountOut}) must be in (0, 2^128-1]`
342
+ );
343
+ }
344
+ if (maxAmountIn <= 0n || maxAmountIn > UINT128_MAX) {
345
+ throw new Error(
346
+ `buildV4SwapInputExactOut: maxAmountIn (${maxAmountIn}) must be in (0, 2^128-1]`
347
+ );
348
+ }
349
+ const actions = encodePacked(
350
+ ["uint8", "uint8", "uint8"],
351
+ [SWAP_EXACT_OUT, SETTLE_ALL, TAKE_ALL]
352
+ );
353
+ const swapParam = encodeAbiParameters(
354
+ [{ name: "swap", type: "tuple", components: EXACT_OUTPUT_PARAMS_ABI }],
355
+ [
356
+ {
357
+ currencyOut,
358
+ path: path.map((p) => ({
359
+ intermediateCurrency: p.intermediateCurrency,
360
+ fee: BigInt(p.fee),
361
+ tickSpacing: p.tickSpacing,
362
+ hooks: p.hooks,
363
+ hookData: p.hookData
364
+ })),
365
+ amountOut,
366
+ amountInMaximum: maxAmountIn
367
+ }
368
+ ]
369
+ );
370
+ const settleParam = encodeAbiParameters(
371
+ [
372
+ { name: "currency", type: "address" },
373
+ { name: "maxAmount", type: "uint256" }
374
+ ],
375
+ [inputCurrency, maxAmountIn]
376
+ );
377
+ const takeParam = encodeAbiParameters(
378
+ [
379
+ { name: "currency", type: "address" },
380
+ { name: "minAmount", type: "uint256" }
381
+ ],
382
+ [currencyOut, amountOut]
383
+ );
384
+ return encodeAbiParameters(
385
+ [
386
+ { name: "actions", type: "bytes" },
387
+ { name: "params", type: "bytes[]" }
388
+ ],
389
+ [actions, [swapParam, settleParam, takeParam]]
390
+ );
391
+ }
392
+ function buildUniversalRouterExecuteArgsExactOut(currencyOut, path, amountOut, maxAmountIn, inputCurrency) {
393
+ const commands = encodePacked(["uint8"], [V4_SWAP]);
394
+ const inputs = [
395
+ buildV4SwapInputExactOut(
396
+ currencyOut,
397
+ path,
398
+ amountOut,
399
+ maxAmountIn,
400
+ inputCurrency
401
+ )
402
+ ];
403
+ return { commands, inputs };
404
+ }
405
+ var _buildSwapFromQuoteWarned = false;
253
406
  function buildSwapFromQuote(params) {
407
+ if (!_buildSwapFromQuoteWarned) {
408
+ _buildSwapFromQuoteWarned = true;
409
+ console.warn(
410
+ "[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`."
411
+ );
412
+ }
254
413
  return buildUniversalRouterExecuteArgs(
255
414
  params.currencyIn,
256
415
  params.quote.path,
@@ -358,6 +517,82 @@ function buildSwapUserOp(params) {
358
517
  }
359
518
  });
360
519
  }
520
+ function buildSwapUserOpExactOut(params) {
521
+ if (params.amountOut <= 0n) {
522
+ throw new Error("buildSwapUserOpExactOut: amountOut must be positive");
523
+ }
524
+ if (params.maxAmountIn <= 0n) {
525
+ throw new Error("buildSwapUserOpExactOut: maxAmountIn must be positive");
526
+ }
527
+ if (params.gasFeeAmountInput < 0n) {
528
+ throw new Error(
529
+ "buildSwapUserOpExactOut: gasFeeAmountInput must be non-negative"
530
+ );
531
+ }
532
+ if (params.swapPath.length === 0) {
533
+ throw new Error(
534
+ "buildSwapUserOpExactOut: swapPath must contain at least one PathKey"
535
+ );
536
+ }
537
+ const PERMIT2_EXPIRATION_MAX = 2n ** 48n - 1n;
538
+ if (params.deadline <= 0n || params.deadline > PERMIT2_EXPIRATION_MAX) {
539
+ throw new Error(
540
+ `buildSwapUserOpExactOut: deadline (${params.deadline}) must be unix seconds in (0, 2^48-1]. Did you accidentally pass milliseconds?`
541
+ );
542
+ }
543
+ const { commands, inputs } = buildUniversalRouterExecuteArgsExactOut(
544
+ params.outputTokenAddress,
545
+ params.swapPath,
546
+ params.amountOut,
547
+ params.maxAmountIn,
548
+ params.inputTokenAddress
549
+ );
550
+ const swapCallData = encodeFunctionData2({
551
+ abi: universalRouterAbi2,
552
+ functionName: "execute",
553
+ args: [commands, inputs, params.deadline]
554
+ });
555
+ const permit2ApproveData = buildPermit2ApprovalCalldata(
556
+ params.inputTokenAddress,
557
+ params.universalRouterAddress,
558
+ params.maxAmountIn,
559
+ Number(params.deadline)
560
+ );
561
+ const operations = [];
562
+ if (params.gasFeeAmountInput > 0n) {
563
+ operations.push(
564
+ erc20TransferOp(
565
+ params.inputTokenAddress,
566
+ params.feeRecipient,
567
+ params.gasFeeAmountInput
568
+ )
569
+ );
570
+ }
571
+ operations.push(
572
+ erc20ApproveOp(params.inputTokenAddress, PERMIT2_ADDRESS, params.maxAmountIn),
573
+ rawCallOp(PERMIT2_ADDRESS, permit2ApproveData),
574
+ rawCallOp(params.universalRouterAddress, swapCallData)
575
+ );
576
+ return buildPartialUserOperation({
577
+ sender: params.userAddress,
578
+ nonce: params.aaNonce,
579
+ operations,
580
+ gasLimits: {
581
+ // +50k headroom for the additional pre-swap ERC-20 transfer when
582
+ // the input-side fee is non-zero. Keeps margin even when fee=0n.
583
+ callGasLimit: params.gasLimits?.callGasLimit ?? 750000n,
584
+ verificationGasLimit: params.gasLimits?.verificationGasLimit ?? 150000n,
585
+ preVerificationGas: params.gasLimits?.preVerificationGas ?? 50000n
586
+ }
587
+ });
588
+ }
589
+
590
+ // src/swap/slippage.ts
591
+ var MAX_SLIPPAGE_BPS = 5e3;
592
+ function isValidSlippageBps(value) {
593
+ if (value === void 0) return true;
594
+ return Number.isInteger(value) && value >= 0 && value <= MAX_SLIPPAGE_BPS;
595
+ }
361
596
 
362
597
  // src/api/handlers.ts
363
598
  var TradingHandlers = class {
@@ -446,10 +681,9 @@ var TradingHandlers = class {
446
681
  * net. Quote response surfaces both `estimatedOutputAmount` (gross)
447
682
  * and `outputNet` so the FE can display reality.
448
683
  *
449
- * v0.3.1 — `authenticatedAddress` first param. Caller (issuer
450
- * controller / FE proxy) MUST pass the address extracted from the
451
- * verified session/JWT. Handler asserts it equals
452
- * `request.userAddress`. See SDK_CORE_TRADING_AUDIT.md C6.
684
+ * `authenticatedAddress` first param: caller (issuer controller / FE
685
+ * proxy) MUST pass the address extracted from the verified
686
+ * session/JWT. Handler asserts it equals `request.userAddress`.
453
687
  */
454
688
  async handleSwap(authenticatedAddress, request) {
455
689
  if (getAddress(authenticatedAddress) !== getAddress(request.userAddress)) {
@@ -471,6 +705,13 @@ var TradingHandlers = class {
471
705
  "handleSwap: amount must be positive"
472
706
  );
473
707
  }
708
+ if (!isValidSlippageBps(request.slippageBps)) {
709
+ throw new ValidationError(
710
+ "INVALID_SLIPPAGE",
711
+ `handleSwap: slippageBps (${request.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`,
712
+ { received: request.slippageBps, max: MAX_SLIPPAGE_BPS }
713
+ );
714
+ }
474
715
  const { pafiFeeRecipient } = getContractAddresses(request.chainId);
475
716
  const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
476
717
  if (!universalRouter) {
@@ -558,6 +799,198 @@ var TradingHandlers = class {
558
799
  };
559
800
  }
560
801
  // =========================================================================
802
+ // GET /quote/exact-out — V4 exact-output quote
803
+ // =========================================================================
804
+ /**
805
+ * Quote the input required to receive `request.amount` of the output
806
+ * token via Uniswap V4. Input-side operator fee is auto-quoted, so
807
+ * `inputGross = estimatedInputAmount + feeAmountInput` is the total
808
+ * the user must hold.
809
+ *
810
+ * Returns `quoteError: "QUOTE_UNAVAILABLE"` rather than throwing when
811
+ * no path exists.
812
+ */
813
+ async handleQuoteExactOut(request) {
814
+ if (request.chainId !== this.chainId) {
815
+ throw new ValidationError(
816
+ "UNSUPPORTED_CHAIN_ID",
817
+ `handleQuoteExactOut: unsupported chainId ${request.chainId}`,
818
+ { requested: request.chainId, supported: this.chainId }
819
+ );
820
+ }
821
+ if (request.amount === 0n) {
822
+ return {
823
+ outputAmount: 0n,
824
+ estimatedInputAmount: 0n,
825
+ inputGross: 0n,
826
+ feeAmountInput: 0n,
827
+ gasEstimate: 0n
828
+ };
829
+ }
830
+ const inputTokenAddress = getAddress(request.inputTokenAddress);
831
+ const outputTokenAddress = getAddress(request.outputTokenAddress);
832
+ const pools = request.pools ?? [];
833
+ try {
834
+ const best = await findBestQuoteExactOut(
835
+ this.provider,
836
+ request.chainId,
837
+ inputTokenAddress,
838
+ outputTokenAddress,
839
+ request.amount,
840
+ pools
841
+ );
842
+ const feeAmountInput = await quoteOperatorFeeInput(
843
+ this.provider,
844
+ request.chainId,
845
+ inputTokenAddress
846
+ ).catch(() => 0n);
847
+ const estimatedInputAmount = best.bestRoute.amountIn;
848
+ return {
849
+ outputAmount: request.amount,
850
+ estimatedInputAmount,
851
+ inputGross: estimatedInputAmount + feeAmountInput,
852
+ feeAmountInput,
853
+ gasEstimate: best.bestRoute.gasEstimate
854
+ };
855
+ } catch {
856
+ return {
857
+ outputAmount: request.amount,
858
+ estimatedInputAmount: 0n,
859
+ inputGross: 0n,
860
+ feeAmountInput: 0n,
861
+ gasEstimate: 0n,
862
+ quoteError: "QUOTE_UNAVAILABLE"
863
+ };
864
+ }
865
+ }
866
+ // =========================================================================
867
+ // POST /swap/exact-out — V4 exact-output swap UserOp
868
+ // =========================================================================
869
+ /**
870
+ * Build a V4 exact-output swap UserOp.
871
+ *
872
+ * Quotes the best exact-output route, applies slippage as a CEILING on
873
+ * `maxAmountIn` (so the cap is never silently tightened by floor
874
+ * division), then encodes a 4-step batch:
875
+ *
876
+ * inputToken.transfer(feeRecipient, gasFeeAmountInput) [if > 0]
877
+ * → input.approve(Permit2, maxAmountIn)
878
+ * → Permit2.approve(router, maxAmountIn)
879
+ * → UniversalRouter.execute (V4 SWAP_EXACT_OUT)
880
+ *
881
+ * Operator fee is INPUT-token-side (charged before swap) so the user
882
+ * receives exactly `request.amount` of output.
883
+ *
884
+ * `authenticatedAddress` first param: caller MUST pass the address
885
+ * extracted from the verified session/JWT. Handler asserts equality
886
+ * with `request.userAddress`.
887
+ */
888
+ async handleSwapExactOut(authenticatedAddress, request) {
889
+ if (getAddress(authenticatedAddress) !== getAddress(request.userAddress)) {
890
+ throw new ValidationError(
891
+ "USER_ADDRESS_MISMATCH",
892
+ `handleSwapExactOut: authenticatedAddress (${authenticatedAddress}) does not match request.userAddress (${request.userAddress})`
893
+ );
894
+ }
895
+ if (request.chainId !== this.chainId) {
896
+ throw new ValidationError(
897
+ "UNSUPPORTED_CHAIN_ID",
898
+ `handleSwapExactOut: unsupported chainId ${request.chainId}`,
899
+ { requested: request.chainId, supported: this.chainId }
900
+ );
901
+ }
902
+ if (request.amount <= 0n) {
903
+ throw new ValidationError(
904
+ "INVALID_AMOUNT",
905
+ "handleSwapExactOut: amount must be positive"
906
+ );
907
+ }
908
+ if (!isValidSlippageBps(request.slippageBps)) {
909
+ throw new ValidationError(
910
+ "INVALID_SLIPPAGE",
911
+ `handleSwapExactOut: slippageBps (${request.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`,
912
+ { received: request.slippageBps, max: MAX_SLIPPAGE_BPS }
913
+ );
914
+ }
915
+ const { pafiFeeRecipient } = getContractAddresses(request.chainId);
916
+ const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
917
+ if (!universalRouter) {
918
+ throw new ValidationError(
919
+ "ROUTER_NOT_DEPLOYED",
920
+ `handleSwapExactOut: no UniversalRouter for chainId ${request.chainId}`
921
+ );
922
+ }
923
+ const inputTokenAddress = getAddress(request.inputTokenAddress);
924
+ const outputTokenAddress = getAddress(request.outputTokenAddress);
925
+ const userAddress = getAddress(request.userAddress);
926
+ const pools = request.pools ?? [];
927
+ let quoteResult;
928
+ try {
929
+ quoteResult = await findBestQuoteExactOut(
930
+ this.provider,
931
+ request.chainId,
932
+ inputTokenAddress,
933
+ outputTokenAddress,
934
+ request.amount,
935
+ pools
936
+ );
937
+ } catch (err) {
938
+ const cause = err instanceof Error ? err.message : String(err);
939
+ throw new ValidationError(
940
+ "NO_SWAP_PATH",
941
+ `handleSwapExactOut: no swap path found from ${inputTokenAddress} to ${outputTokenAddress} (cause: ${cause})`
942
+ );
943
+ }
944
+ const gasFeeAmountInput = request.gasFeeAmountInput !== void 0 ? request.gasFeeAmountInput : await quoteOperatorFeeInput(
945
+ this.provider,
946
+ request.chainId,
947
+ inputTokenAddress
948
+ ).catch(() => 0n);
949
+ const hops = quoteResult.bestRoute.path.length;
950
+ const slippageBps = request.slippageBps ?? (hops > 1 ? 100 : 50);
951
+ const estimatedInputAmount = quoteResult.bestRoute.amountIn;
952
+ const slippageNumerator = estimatedInputAmount * BigInt(1e4 + slippageBps);
953
+ const maxAmountIn = (slippageNumerator + 9999n) / 10000n;
954
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
955
+ const userOp = buildSwapUserOpExactOut({
956
+ userAddress,
957
+ aaNonce: request.aaNonce,
958
+ inputTokenAddress,
959
+ outputTokenAddress,
960
+ universalRouterAddress: universalRouter,
961
+ amountOut: request.amount,
962
+ maxAmountIn,
963
+ swapPath: quoteResult.bestRoute.path,
964
+ deadline,
965
+ gasFeeAmountInput,
966
+ feeRecipient: pafiFeeRecipient
967
+ });
968
+ const userOpFallback = gasFeeAmountInput > 0n ? buildSwapUserOpExactOut({
969
+ userAddress,
970
+ aaNonce: request.aaNonce,
971
+ inputTokenAddress,
972
+ outputTokenAddress,
973
+ universalRouterAddress: universalRouter,
974
+ amountOut: request.amount,
975
+ maxAmountIn,
976
+ swapPath: quoteResult.bestRoute.path,
977
+ deadline,
978
+ gasFeeAmountInput: 0n,
979
+ feeRecipient: pafiFeeRecipient
980
+ }) : void 0;
981
+ return {
982
+ userOp,
983
+ userOpFallback,
984
+ outputAmount: request.amount,
985
+ estimatedInputAmount,
986
+ maxAmountIn,
987
+ hops,
988
+ deadline,
989
+ feeAmountUsed: gasFeeAmountInput,
990
+ feeRecipient: pafiFeeRecipient
991
+ };
992
+ }
993
+ // =========================================================================
561
994
  // POST /perp-deposit
562
995
  // =========================================================================
563
996
  /**
@@ -764,6 +1197,17 @@ async function quoteOperatorFeeOutput(provider, chainId, outputTokenAddress) {
764
1197
  pointTokenAddress: outputTokenAddress
765
1198
  });
766
1199
  }
1200
+ async function quoteOperatorFeeInput(provider, chainId, inputTokenAddress) {
1201
+ const { usdt } = getContractAddresses(chainId);
1202
+ if (usdt && getAddress(inputTokenAddress) === getAddress(usdt)) {
1203
+ return quoteOperatorFeeUsdt({ provider, chainId });
1204
+ }
1205
+ return quoteOperatorFeePt({
1206
+ provider,
1207
+ chainId,
1208
+ pointTokenAddress: inputTokenAddress
1209
+ });
1210
+ }
767
1211
 
768
1212
  // src/pools.ts
769
1213
  import { fetchPafiPools, PAFI_SUBGRAPH_URL } from "@pafi-dev/core";
@@ -778,6 +1222,29 @@ import {
778
1222
  BATCH_EXECUTOR_7702_IMPL
779
1223
  } from "@pafi-dev/core";
780
1224
  async function swapDirect(params) {
1225
+ const universalRouter = UNIVERSAL_ROUTER_ADDRESSES2[params.chainId];
1226
+ if (!universalRouter) {
1227
+ throw new Error(`swapDirect: no UniversalRouter for chainId ${params.chainId}`);
1228
+ }
1229
+ if (params.amount <= 0n) {
1230
+ throw new Error("swapDirect: amount must be positive");
1231
+ }
1232
+ if (!isValidSlippageBps(params.slippageBps)) {
1233
+ throw new Error(
1234
+ `swapDirect: slippageBps (${params.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`
1235
+ );
1236
+ }
1237
+ const account = params.walletClient.account;
1238
+ if (!account) {
1239
+ throw new Error(
1240
+ "swapDirect: walletClient has no account attached \u2014 cannot send tx"
1241
+ );
1242
+ }
1243
+ if (account.address.toLowerCase() !== params.userAddress.toLowerCase()) {
1244
+ throw new Error(
1245
+ `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`
1246
+ );
1247
+ }
781
1248
  const code = await params.publicClient.getCode({
782
1249
  address: params.userAddress
783
1250
  });
@@ -793,13 +1260,6 @@ async function swapDirect(params) {
793
1260
  `swapDirect: user delegated to ${delegate} which is not a PAFI-recognised impl (expected ${SIMPLE_7702_IMPL_BASE_MAINNET} or ${BATCH_EXECUTOR_7702_IMPL}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
794
1261
  );
795
1262
  }
796
- const universalRouter = UNIVERSAL_ROUTER_ADDRESSES2[params.chainId];
797
- if (!universalRouter) {
798
- throw new Error(`swapDirect: no UniversalRouter for chainId ${params.chainId}`);
799
- }
800
- if (params.amount <= 0n) {
801
- throw new Error("swapDirect: amount must be positive");
802
- }
803
1263
  let quoteResult;
804
1264
  try {
805
1265
  quoteResult = await findBestQuote(
@@ -842,12 +1302,124 @@ async function swapDirect(params) {
842
1302
  gasFeeAmountOutput,
843
1303
  feeRecipient: pafiFeeRecipient
844
1304
  });
1305
+ const txHash = await params.walletClient.sendTransaction({
1306
+ account,
1307
+ chain: params.walletClient.chain,
1308
+ to: params.userAddress,
1309
+ value: 0n,
1310
+ data: userOp.callData
1311
+ });
1312
+ let receipt;
1313
+ if (params.waitForReceipt !== false) {
1314
+ try {
1315
+ receipt = await params.publicClient.waitForTransactionReceipt({
1316
+ hash: txHash
1317
+ });
1318
+ } catch (err) {
1319
+ params.onWarning?.(
1320
+ `swapDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
1321
+ );
1322
+ }
1323
+ }
1324
+ return {
1325
+ txHash,
1326
+ receipt,
1327
+ estimatedOutputAmount,
1328
+ minAmountOut,
1329
+ hops,
1330
+ deadline,
1331
+ feeAmountUsed: gasFeeAmountOutput
1332
+ };
1333
+ }
1334
+
1335
+ // src/direct/swapDirectExactOut.ts
1336
+ import {
1337
+ UNIVERSAL_ROUTER_ADDRESSES as UNIVERSAL_ROUTER_ADDRESSES3,
1338
+ getContractAddresses as getContractAddresses3,
1339
+ parseEip7702DelegatedAddress as parseEip7702DelegatedAddress2,
1340
+ detectDelegateImpl as detectDelegateImpl2,
1341
+ SIMPLE_7702_IMPL_BASE_MAINNET as SIMPLE_7702_IMPL_BASE_MAINNET2,
1342
+ BATCH_EXECUTOR_7702_IMPL as BATCH_EXECUTOR_7702_IMPL2
1343
+ } from "@pafi-dev/core";
1344
+ async function swapDirectExactOut(params) {
1345
+ const universalRouter = UNIVERSAL_ROUTER_ADDRESSES3[params.chainId];
1346
+ if (!universalRouter) {
1347
+ throw new Error(
1348
+ `swapDirectExactOut: no UniversalRouter for chainId ${params.chainId}`
1349
+ );
1350
+ }
1351
+ if (params.amount <= 0n) {
1352
+ throw new Error("swapDirectExactOut: amount must be positive");
1353
+ }
1354
+ if (!isValidSlippageBps(params.slippageBps)) {
1355
+ throw new Error(
1356
+ `swapDirectExactOut: slippageBps (${params.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`
1357
+ );
1358
+ }
845
1359
  const account = params.walletClient.account;
846
1360
  if (!account) {
847
1361
  throw new Error(
848
- "swapDirect: walletClient has no account attached \u2014 cannot send tx"
1362
+ "swapDirectExactOut: walletClient has no account attached \u2014 cannot send tx"
849
1363
  );
850
1364
  }
1365
+ if (account.address.toLowerCase() !== params.userAddress.toLowerCase()) {
1366
+ throw new Error(
1367
+ `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`
1368
+ );
1369
+ }
1370
+ const code = await params.publicClient.getCode({
1371
+ address: params.userAddress
1372
+ });
1373
+ const delegate = parseEip7702DelegatedAddress2(code);
1374
+ if (!delegate) {
1375
+ throw new Error(
1376
+ `swapDirectExactOut: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first or use the AA path via \`TradingHandlers.handleSwapExactOut()\` + sponsor-relayer.`
1377
+ );
1378
+ }
1379
+ const impl = detectDelegateImpl2(delegate);
1380
+ if (impl === "unknown") {
1381
+ params.onWarning?.(
1382
+ `swapDirectExactOut: user delegated to ${delegate} which is not a PAFI-recognised impl (expected ${SIMPLE_7702_IMPL_BASE_MAINNET2} or ${BATCH_EXECUTOR_7702_IMPL2}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
1383
+ );
1384
+ }
1385
+ let quoteResult;
1386
+ try {
1387
+ quoteResult = await findBestQuoteExactOut(
1388
+ params.publicClient,
1389
+ params.chainId,
1390
+ params.inputTokenAddress,
1391
+ params.outputTokenAddress,
1392
+ params.amount,
1393
+ params.pools ?? []
1394
+ );
1395
+ } catch (err) {
1396
+ const cause = err instanceof Error ? err.message : String(err);
1397
+ throw new Error(
1398
+ `swapDirectExactOut: no swap path found from ${params.inputTokenAddress} to ${params.outputTokenAddress} (cause: ${cause})`
1399
+ );
1400
+ }
1401
+ const hops = quoteResult.bestRoute.path.length;
1402
+ const slippageBps = params.slippageBps ?? (hops > 1 ? 100 : 50);
1403
+ const estimatedInputAmount = quoteResult.bestRoute.amountIn;
1404
+ const slippageNumerator = estimatedInputAmount * BigInt(1e4 + slippageBps);
1405
+ const maxAmountIn = (slippageNumerator + 9999n) / 10000n;
1406
+ const gasFeeAmountInput = params.gasFeeAmountInput ?? 0n;
1407
+ const deadline = params.deadline ?? BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
1408
+ const { pafiFeeRecipient } = getContractAddresses3(params.chainId);
1409
+ const userOp = buildSwapUserOpExactOut({
1410
+ userAddress: params.userAddress,
1411
+ aaNonce: 0n,
1412
+ // ignored on the native-tx path; nonce comes from EOA tx count
1413
+ inputTokenAddress: params.inputTokenAddress,
1414
+ outputTokenAddress: params.outputTokenAddress,
1415
+ universalRouterAddress: universalRouter,
1416
+ amountOut: params.amount,
1417
+ maxAmountIn,
1418
+ swapPath: quoteResult.bestRoute.path,
1419
+ deadline,
1420
+ gasFeeAmountInput,
1421
+ feeRecipient: pafiFeeRecipient
1422
+ });
851
1423
  const txHash = await params.walletClient.sendTransaction({
852
1424
  account,
853
1425
  chain: params.walletClient.chain,
@@ -863,18 +1435,19 @@ async function swapDirect(params) {
863
1435
  });
864
1436
  } catch (err) {
865
1437
  params.onWarning?.(
866
- `swapDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
1438
+ `swapDirectExactOut: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
867
1439
  );
868
1440
  }
869
1441
  }
870
1442
  return {
871
1443
  txHash,
872
1444
  receipt,
873
- estimatedOutputAmount,
874
- minAmountOut,
1445
+ outputAmount: params.amount,
1446
+ estimatedInputAmount,
1447
+ maxAmountIn,
875
1448
  hops,
876
1449
  deadline,
877
- feeAmountUsed: gasFeeAmountOutput
1450
+ feeAmountUsed: gasFeeAmountInput
878
1451
  };
879
1452
  }
880
1453
 
@@ -887,11 +1460,11 @@ import {
887
1460
  TOKEN_HASHES as TOKEN_HASHES2,
888
1461
  buildPerpDepositViaRelay as buildPerpDepositViaRelay2,
889
1462
  computeAccountId as computeAccountId2,
890
- detectDelegateImpl as detectDelegateImpl2,
891
- getContractAddresses as getContractAddresses3,
892
- parseEip7702DelegatedAddress as parseEip7702DelegatedAddress2,
893
- BATCH_EXECUTOR_7702_IMPL as BATCH_EXECUTOR_7702_IMPL2,
894
- SIMPLE_7702_IMPL_BASE_MAINNET as SIMPLE_7702_IMPL_BASE_MAINNET2
1463
+ detectDelegateImpl as detectDelegateImpl3,
1464
+ getContractAddresses as getContractAddresses4,
1465
+ parseEip7702DelegatedAddress as parseEip7702DelegatedAddress3,
1466
+ BATCH_EXECUTOR_7702_IMPL as BATCH_EXECUTOR_7702_IMPL3,
1467
+ SIMPLE_7702_IMPL_BASE_MAINNET as SIMPLE_7702_IMPL_BASE_MAINNET3
895
1468
  } from "@pafi-dev/core";
896
1469
  async function perpDepositDirect(params) {
897
1470
  if (params.amount <= 0n) {
@@ -900,16 +1473,16 @@ async function perpDepositDirect(params) {
900
1473
  const code = await params.publicClient.getCode({
901
1474
  address: params.userAddress
902
1475
  });
903
- const delegate = parseEip7702DelegatedAddress2(code);
1476
+ const delegate = parseEip7702DelegatedAddress3(code);
904
1477
  if (!delegate) {
905
1478
  throw new Error(
906
1479
  `perpDepositDirect: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first or use the AA path via \`TradingHandlers.handlePerpDeposit()\` + sponsor-relayer.`
907
1480
  );
908
1481
  }
909
- const impl = detectDelegateImpl2(delegate);
1482
+ const impl = detectDelegateImpl3(delegate);
910
1483
  if (impl === "unknown") {
911
1484
  params.onWarning?.(
912
- `perpDepositDirect: user delegated to ${delegate} (not a PAFI-recognised impl ${SIMPLE_7702_IMPL_BASE_MAINNET2} / ${BATCH_EXECUTOR_7702_IMPL2}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
1485
+ `perpDepositDirect: user delegated to ${delegate} (not a PAFI-recognised impl ${SIMPLE_7702_IMPL_BASE_MAINNET3} / ${BATCH_EXECUTOR_7702_IMPL3}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
913
1486
  );
914
1487
  }
915
1488
  const vault = ORDERLY_VAULT_ADDRESSES2[params.chainId];
@@ -944,7 +1517,7 @@ async function perpDepositDirect(params) {
944
1517
  `perpDepositDirect: broker "${params.brokerId}" is not whitelisted on Orderly Vault`
945
1518
  );
946
1519
  }
947
- const { orderlyRelay: relayAddress, pafiFeeRecipient } = getContractAddresses3(
1520
+ const { orderlyRelay: relayAddress, pafiFeeRecipient } = getContractAddresses4(
948
1521
  params.chainId
949
1522
  );
950
1523
  const RELAY_FEE_FLOOR_USDC = 2000000n;
@@ -1024,23 +1597,33 @@ async function perpDepositDirect(params) {
1024
1597
  }
1025
1598
  export {
1026
1599
  PAFI_SUBGRAPH_URL,
1600
+ SWAP_EXACT_OUT,
1601
+ SWAP_EXACT_OUT_SINGLE,
1027
1602
  TradingHandlers,
1028
1603
  buildAllPaths,
1029
1604
  buildErc20ApprovalCalldata,
1030
1605
  buildPermit2ApprovalCalldata,
1031
1606
  buildSwapFromQuote,
1032
1607
  buildSwapUserOp,
1608
+ buildSwapUserOpExactOut,
1033
1609
  buildUniversalRouterExecuteArgs,
1610
+ buildUniversalRouterExecuteArgsExactOut,
1034
1611
  buildV4SwapInput,
1612
+ buildV4SwapInputExactOut,
1035
1613
  checkAllowance,
1036
1614
  combineRoutes,
1037
1615
  fetchPafiPools,
1038
1616
  findBestQuote,
1617
+ findBestQuoteExactOut,
1039
1618
  perpDepositDirect,
1040
1619
  quoteBestRoute,
1620
+ quoteBestRouteExactOut,
1041
1621
  quoteExactInput,
1042
1622
  quoteExactInputSingle,
1623
+ quoteExactOutput,
1624
+ quoteExactOutputSingle,
1043
1625
  simulateSwap,
1044
- swapDirect
1626
+ swapDirect,
1627
+ swapDirectExactOut
1045
1628
  };
1046
1629
  //# sourceMappingURL=index.js.map