@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.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,26 +1197,433 @@ 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";
1214
+
1215
+ // src/direct/swapDirect.ts
1216
+ import {
1217
+ UNIVERSAL_ROUTER_ADDRESSES as UNIVERSAL_ROUTER_ADDRESSES2,
1218
+ getContractAddresses as getContractAddresses2,
1219
+ parseEip7702DelegatedAddress,
1220
+ detectDelegateImpl,
1221
+ SIMPLE_7702_IMPL_BASE_MAINNET,
1222
+ BATCH_EXECUTOR_7702_IMPL
1223
+ } from "@pafi-dev/core";
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
+ }
1248
+ const code = await params.publicClient.getCode({
1249
+ address: params.userAddress
1250
+ });
1251
+ const delegate = parseEip7702DelegatedAddress(code);
1252
+ if (!delegate) {
1253
+ throw new Error(
1254
+ `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.`
1255
+ );
1256
+ }
1257
+ const impl = detectDelegateImpl(delegate);
1258
+ if (impl === "unknown") {
1259
+ params.onWarning?.(
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.`
1261
+ );
1262
+ }
1263
+ let quoteResult;
1264
+ try {
1265
+ quoteResult = await findBestQuote(
1266
+ params.publicClient,
1267
+ params.chainId,
1268
+ params.inputTokenAddress,
1269
+ params.outputTokenAddress,
1270
+ params.amount,
1271
+ params.pools ?? []
1272
+ );
1273
+ } catch (err) {
1274
+ const cause = err instanceof Error ? err.message : String(err);
1275
+ throw new Error(
1276
+ `swapDirect: no swap path found from ${params.inputTokenAddress} to ${params.outputTokenAddress} (cause: ${cause})`
1277
+ );
1278
+ }
1279
+ const hops = quoteResult.bestRoute.path.length;
1280
+ const slippageBps = params.slippageBps ?? (hops > 1 ? 100 : 50);
1281
+ const estimatedOutputAmount = quoteResult.bestRoute.amountOut;
1282
+ const minAmountOut = estimatedOutputAmount * BigInt(1e4 - slippageBps) / 10000n;
1283
+ const gasFeeAmountOutput = params.gasFeeAmountOutput ?? 0n;
1284
+ if (gasFeeAmountOutput > 0n && minAmountOut < gasFeeAmountOutput) {
1285
+ throw new Error(
1286
+ `swapDirect: minAmountOut (${minAmountOut}) below operator fee (${gasFeeAmountOutput})`
1287
+ );
1288
+ }
1289
+ const deadline = params.deadline ?? BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
1290
+ const { pafiFeeRecipient } = getContractAddresses2(params.chainId);
1291
+ const userOp = buildSwapUserOp({
1292
+ userAddress: params.userAddress,
1293
+ aaNonce: 0n,
1294
+ // ignored on the native-tx path; nonce comes from EOA tx count
1295
+ inputTokenAddress: params.inputTokenAddress,
1296
+ outputTokenAddress: params.outputTokenAddress,
1297
+ universalRouterAddress: universalRouter,
1298
+ amountIn: params.amount,
1299
+ minAmountOut,
1300
+ swapPath: quoteResult.bestRoute.path,
1301
+ deadline,
1302
+ gasFeeAmountOutput,
1303
+ feeRecipient: pafiFeeRecipient
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
+ }
1359
+ const account = params.walletClient.account;
1360
+ if (!account) {
1361
+ throw new Error(
1362
+ "swapDirectExactOut: walletClient has no account attached \u2014 cannot send tx"
1363
+ );
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
+ });
1423
+ const txHash = await params.walletClient.sendTransaction({
1424
+ account,
1425
+ chain: params.walletClient.chain,
1426
+ to: params.userAddress,
1427
+ value: 0n,
1428
+ data: userOp.callData
1429
+ });
1430
+ let receipt;
1431
+ if (params.waitForReceipt !== false) {
1432
+ try {
1433
+ receipt = await params.publicClient.waitForTransactionReceipt({
1434
+ hash: txHash
1435
+ });
1436
+ } catch (err) {
1437
+ params.onWarning?.(
1438
+ `swapDirectExactOut: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
1439
+ );
1440
+ }
1441
+ }
1442
+ return {
1443
+ txHash,
1444
+ receipt,
1445
+ outputAmount: params.amount,
1446
+ estimatedInputAmount,
1447
+ maxAmountIn,
1448
+ hops,
1449
+ deadline,
1450
+ feeAmountUsed: gasFeeAmountInput
1451
+ };
1452
+ }
1453
+
1454
+ // src/direct/perpDepositDirect.ts
1455
+ import {
1456
+ BROKER_HASHES as BROKER_HASHES2,
1457
+ ORDERLY_RELAY_ABI as ORDERLY_RELAY_ABI2,
1458
+ ORDERLY_VAULT_ABI as ORDERLY_VAULT_ABI2,
1459
+ ORDERLY_VAULT_ADDRESSES as ORDERLY_VAULT_ADDRESSES2,
1460
+ TOKEN_HASHES as TOKEN_HASHES2,
1461
+ buildPerpDepositViaRelay as buildPerpDepositViaRelay2,
1462
+ computeAccountId as computeAccountId2,
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
1468
+ } from "@pafi-dev/core";
1469
+ async function perpDepositDirect(params) {
1470
+ if (params.amount <= 0n) {
1471
+ throw new Error("perpDepositDirect: amount must be positive");
1472
+ }
1473
+ const code = await params.publicClient.getCode({
1474
+ address: params.userAddress
1475
+ });
1476
+ const delegate = parseEip7702DelegatedAddress3(code);
1477
+ if (!delegate) {
1478
+ throw new Error(
1479
+ `perpDepositDirect: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first or use the AA path via \`TradingHandlers.handlePerpDeposit()\` + sponsor-relayer.`
1480
+ );
1481
+ }
1482
+ const impl = detectDelegateImpl3(delegate);
1483
+ if (impl === "unknown") {
1484
+ params.onWarning?.(
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.`
1486
+ );
1487
+ }
1488
+ const vault = ORDERLY_VAULT_ADDRESSES2[params.chainId];
1489
+ if (!vault) {
1490
+ throw new Error(
1491
+ `perpDepositDirect: no Orderly Vault for chainId ${params.chainId}`
1492
+ );
1493
+ }
1494
+ const brokerHash = BROKER_HASHES2[params.brokerId];
1495
+ if (!brokerHash) {
1496
+ throw new Error(
1497
+ `perpDepositDirect: unknown brokerId "${params.brokerId}"`
1498
+ );
1499
+ }
1500
+ const tokenHash = TOKEN_HASHES2.USDC;
1501
+ const [usdcAddress, brokerAllowed] = await Promise.all([
1502
+ params.publicClient.readContract({
1503
+ address: vault,
1504
+ abi: ORDERLY_VAULT_ABI2,
1505
+ functionName: "getAllowedToken",
1506
+ args: [tokenHash]
1507
+ }),
1508
+ params.publicClient.readContract({
1509
+ address: vault,
1510
+ abi: ORDERLY_VAULT_ABI2,
1511
+ functionName: "getAllowedBroker",
1512
+ args: [brokerHash]
1513
+ })
1514
+ ]);
1515
+ if (!brokerAllowed) {
1516
+ throw new Error(
1517
+ `perpDepositDirect: broker "${params.brokerId}" is not whitelisted on Orderly Vault`
1518
+ );
1519
+ }
1520
+ const { orderlyRelay: relayAddress, pafiFeeRecipient } = getContractAddresses4(
1521
+ params.chainId
1522
+ );
1523
+ const RELAY_FEE_FLOOR_USDC = 2000000n;
1524
+ const percentCap = params.amount * 500n / 10000n;
1525
+ const maxFee = params.maxRelayFee ?? (percentCap > RELAY_FEE_FLOOR_USDC ? percentCap : RELAY_FEE_FLOOR_USDC);
1526
+ const relayRequest = {
1527
+ token: usdcAddress,
1528
+ receiver: params.userAddress,
1529
+ brokerHash,
1530
+ totalAmount: params.amount,
1531
+ maxFee
1532
+ };
1533
+ const relayTokenFee = await params.publicClient.readContract({
1534
+ address: relayAddress,
1535
+ abi: ORDERLY_RELAY_ABI2,
1536
+ functionName: "quoteTokenFee",
1537
+ args: [relayRequest]
1538
+ });
1539
+ if (relayTokenFee > maxFee) {
1540
+ throw new Error(
1541
+ `perpDepositDirect: Relay tokenFee ${relayTokenFee} exceeds maxFee ${maxFee} \u2014 pass a larger \`maxRelayFee\` or increase \`amount\`.`
1542
+ );
1543
+ }
1544
+ if (relayTokenFee >= params.amount) {
1545
+ throw new Error(
1546
+ `perpDepositDirect: deposit amount ${params.amount} below Relay fee ${relayTokenFee} \u2014 increase \`amount\`.`
1547
+ );
1548
+ }
1549
+ const gasFeeUsdc = params.gasFeeUsdc ?? 0n;
1550
+ const partial = buildPerpDepositViaRelay2({
1551
+ userAddress: params.userAddress,
1552
+ aaNonce: 0n,
1553
+ // ignored on the native-tx path
1554
+ relayAddress,
1555
+ request: relayRequest,
1556
+ gasFeeUsdc: gasFeeUsdc > 0n ? gasFeeUsdc : void 0,
1557
+ gasFeeUsdcRecipient: gasFeeUsdc > 0n ? pafiFeeRecipient : void 0
1558
+ });
1559
+ const account = params.walletClient.account;
1560
+ if (!account) {
1561
+ throw new Error(
1562
+ "perpDepositDirect: walletClient has no account attached \u2014 cannot send tx"
1563
+ );
1564
+ }
1565
+ const txHash = await params.walletClient.sendTransaction({
1566
+ account,
1567
+ chain: params.walletClient.chain,
1568
+ to: params.userAddress,
1569
+ value: 0n,
1570
+ data: partial.callData
1571
+ });
1572
+ let receipt;
1573
+ if (params.waitForReceipt !== false) {
1574
+ try {
1575
+ receipt = await params.publicClient.waitForTransactionReceipt({
1576
+ hash: txHash
1577
+ });
1578
+ } catch (err) {
1579
+ params.onWarning?.(
1580
+ `perpDepositDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
1581
+ );
1582
+ }
1583
+ }
1584
+ const accountId = computeAccountId2(params.userAddress, brokerHash);
1585
+ return {
1586
+ txHash,
1587
+ receipt,
1588
+ relayTokenFee,
1589
+ maxFee,
1590
+ netDeposit: params.amount - relayTokenFee,
1591
+ feeAmountUsed: gasFeeUsdc,
1592
+ accountId,
1593
+ brokerHash,
1594
+ usdcAddress,
1595
+ relayAddress
1596
+ };
1597
+ }
770
1598
  export {
771
1599
  PAFI_SUBGRAPH_URL,
1600
+ SWAP_EXACT_OUT,
1601
+ SWAP_EXACT_OUT_SINGLE,
772
1602
  TradingHandlers,
773
1603
  buildAllPaths,
774
1604
  buildErc20ApprovalCalldata,
775
1605
  buildPermit2ApprovalCalldata,
776
1606
  buildSwapFromQuote,
777
1607
  buildSwapUserOp,
1608
+ buildSwapUserOpExactOut,
778
1609
  buildUniversalRouterExecuteArgs,
1610
+ buildUniversalRouterExecuteArgsExactOut,
779
1611
  buildV4SwapInput,
1612
+ buildV4SwapInputExactOut,
780
1613
  checkAllowance,
781
1614
  combineRoutes,
782
1615
  fetchPafiPools,
783
1616
  findBestQuote,
1617
+ findBestQuoteExactOut,
1618
+ perpDepositDirect,
784
1619
  quoteBestRoute,
1620
+ quoteBestRouteExactOut,
785
1621
  quoteExactInput,
786
1622
  quoteExactInputSingle,
787
- simulateSwap
1623
+ quoteExactOutput,
1624
+ quoteExactOutputSingle,
1625
+ simulateSwap,
1626
+ swapDirect,
1627
+ swapDirectExactOut
788
1628
  };
789
1629
  //# sourceMappingURL=index.js.map