@pafi-dev/trading 0.3.3 → 0.4.0

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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Address, PublicClient, Hex } from 'viem';
2
- import { PartialUserOperation, PoolKey, QuoteResult, PathKey, BestQuote } from '@pafi-dev/core';
1
+ import { Address, PublicClient, Hex, WalletClient, TransactionReceipt } from 'viem';
2
+ import { PartialUserOperation, PoolKey, QuoteResult, PathKey, BestQuote, BROKER_HASHES } from '@pafi-dev/core';
3
3
  export { PAFI_SUBGRAPH_URL, fetchPafiPools } from '@pafi-dev/core';
4
4
 
5
5
  interface ApiQuoteRequest {
@@ -508,4 +508,179 @@ declare function quoteBestRoute(client: PublicClient, quoterAddress: Address, ex
508
508
  */
509
509
  declare function findBestQuote(client: PublicClient, chainId: number, tokenIn: Address, tokenOut: Address, exactAmount: bigint, pools?: PoolKey[], quoterAddress?: Address, maxHops?: number): Promise<BestQuote>;
510
510
 
511
- export { type ApiPerpDepositRequest, type ApiPerpDepositResponse, type ApiQuoteError, type ApiQuoteRequest, type ApiQuoteResponse, type ApiSwapRequest, type ApiSwapResponse, type BuildSwapUserOpParams, type SwapSimulationResult, TradingHandlers, type TradingHandlersConfig, buildAllPaths, buildErc20ApprovalCalldata, buildPermit2ApprovalCalldata, buildSwapFromQuote, buildSwapUserOp, buildUniversalRouterExecuteArgs, buildV4SwapInput, checkAllowance, combineRoutes, findBestQuote, quoteBestRoute, quoteExactInput, quoteExactInputSingle, simulateSwap };
511
+ interface SwapDirectParams {
512
+ /** User EOA — must be delegated via EIP-7702 to a PAFI-supported impl. */
513
+ userAddress: Address;
514
+ chainId: number;
515
+ inputTokenAddress: Address;
516
+ outputTokenAddress: Address;
517
+ /** Input amount (raw token decimals). */
518
+ amount: bigint;
519
+ /** Pools to route through. Caller pre-fetches via `fetchPafiPools`. */
520
+ pools?: PoolKey[];
521
+ publicClient: PublicClient;
522
+ walletClient: WalletClient;
523
+ /**
524
+ * Slippage tolerance in basis points. Default 50 single-hop / 100
525
+ * multi-hop (handler picks based on `bestRoute.path.length`).
526
+ */
527
+ slippageBps?: number;
528
+ /** Swap deadline (unix seconds). Default = now + 5 minutes. */
529
+ deadline?: bigint;
530
+ /**
531
+ * Operator fee in OUTPUT token, paid to PAFI fee recipient.
532
+ *
533
+ * - `undefined` (default for direct path): **skip the fee transfer
534
+ * entirely** — PAFI is not sponsoring this swap, no reimbursement
535
+ * owed. Most callers want this.
536
+ * - `0n` — same as above, explicit.
537
+ * - explicit `bigint` — include the fee transfer (rare; only when
538
+ * issuer dev/test wants to mirror sponsored behaviour).
539
+ */
540
+ gasFeeAmountOutput?: bigint;
541
+ /** Wait for receipt before returning. Default `true`. */
542
+ waitForReceipt?: boolean;
543
+ onWarning?: (msg: string) => void;
544
+ }
545
+ interface SwapDirectResult {
546
+ txHash: Hex;
547
+ receipt?: TransactionReceipt;
548
+ estimatedOutputAmount: bigint;
549
+ minAmountOut: bigint;
550
+ hops: number;
551
+ deadline: bigint;
552
+ feeAmountUsed: bigint;
553
+ }
554
+ /**
555
+ * One-shot helper for the **FE-direct swap** path — no AA, no bundler,
556
+ * no PAFI sponsor-relayer. The user EOA pays gas in ETH and broadcasts
557
+ * a single type-2 transaction calling its own EIP-7702 delegated
558
+ * bytecode (`Simple7702Account.executeBatch`).
559
+ *
560
+ * Flow:
561
+ * 1. Verify the EOA has EIP-7702 delegation to a PAFI-supported impl
562
+ * (Simple7702Account or Coinbase SW v2 BatchExecutor). Throw with
563
+ * a clear hint pointing at `delegateDirect()` if not delegated.
564
+ * 2. `findBestQuote` → best route + gross output.
565
+ * 3. `buildSwapUserOp` → encoded `executeBatch(calls)` calldata.
566
+ * 4. `walletClient.sendTransaction({ to: userAddress, data: callData })`
567
+ * — self-call into the delegated bytecode, which dispatches the
568
+ * batch (input.approve → Permit2.approve → UR.execute → optional
569
+ * fee transfer).
570
+ * 5. Wait for receipt (optional).
571
+ *
572
+ * Use when:
573
+ * - The FE doesn't have a Pimlico API key + doesn't want to depend on
574
+ * PAFI sponsor-relayer.
575
+ * - The user already has a small ETH balance.
576
+ * - You need a deterministic, single-tx swap (vs. AA UserOp's longer
577
+ * bundler round-trip).
578
+ *
579
+ * Throws when the user is NOT yet delegated — caller should run
580
+ * `delegateDirect` first or use the AA path (`TradingHandlers.handleSwap`
581
+ * + `permissionless`).
582
+ *
583
+ * @example
584
+ * ```ts
585
+ * import { swapDirect, fetchPafiPools } from "@pafi-dev/trading";
586
+ *
587
+ * const pools = await fetchPafiPools(8453, POINT_TOKEN);
588
+ * const result = await swapDirect({
589
+ * userAddress: wallet.address,
590
+ * chainId: 8453,
591
+ * inputTokenAddress: POINT_TOKEN,
592
+ * outputTokenAddress: USDT,
593
+ * amount: parseUnits("100", 18),
594
+ * pools,
595
+ * publicClient,
596
+ * walletClient,
597
+ * });
598
+ * console.log("Swap tx:", result.txHash);
599
+ * ```
600
+ */
601
+ declare function swapDirect(params: SwapDirectParams): Promise<SwapDirectResult>;
602
+
603
+ interface PerpDepositDirectParams {
604
+ /** User EOA — must be EIP-7702 delegated. */
605
+ userAddress: Address;
606
+ chainId: number;
607
+ /** USDC amount to deposit (6-decimal raw units). */
608
+ amount: bigint;
609
+ /** Orderly broker — `'orderly' | 'woofi_pro' | 'logx'`. */
610
+ brokerId: keyof typeof BROKER_HASHES;
611
+ publicClient: PublicClient;
612
+ walletClient: WalletClient;
613
+ /**
614
+ * Max acceptable USDC fee charged by the Relay (slippage cap on its
615
+ * USD-pricing of `msg.value`). Default `max(amount * 5%, 2 USDC)` —
616
+ * matches `TradingHandlers.handlePerpDeposit`.
617
+ */
618
+ maxRelayFee?: bigint;
619
+ /**
620
+ * Operator fee in USDC (input-token, BEFORE deposit). Same semantics
621
+ * as `swapDirect.gasFeeAmountOutput`:
622
+ *
623
+ * - `undefined` (default for direct path): **skip the fee transfer**
624
+ * — PAFI is not sponsoring this deposit.
625
+ * - explicit `bigint` — include the fee transfer.
626
+ */
627
+ gasFeeUsdc?: bigint;
628
+ /** Wait for receipt before returning. Default `true`. */
629
+ waitForReceipt?: boolean;
630
+ onWarning?: (msg: string) => void;
631
+ }
632
+ interface PerpDepositDirectResult {
633
+ txHash: Hex;
634
+ receipt?: TransactionReceipt;
635
+ /** Resolved USDC fee charged by the Relay. */
636
+ relayTokenFee: bigint;
637
+ /** Cap applied to the Relay fee (slippage allowance). */
638
+ maxFee: bigint;
639
+ /** USDC reaching Orderly Vault after Relay's fee = amount - relayTokenFee. */
640
+ netDeposit: bigint;
641
+ /** Operator fee actually included (0n on default direct path). */
642
+ feeAmountUsed: bigint;
643
+ accountId: Hex;
644
+ brokerHash: Hex;
645
+ usdcAddress: Address;
646
+ relayAddress: Address;
647
+ }
648
+ /**
649
+ * One-shot helper for the **FE-direct perp deposit** path — no AA, no
650
+ * bundler, no PAFI sponsor-relayer. The user EOA pays gas in ETH and
651
+ * broadcasts a single type-2 transaction calling its own EIP-7702
652
+ * delegated bytecode (`Simple7702Account.executeBatch`).
653
+ *
654
+ * Flow:
655
+ * 1. Verify EIP-7702 delegation (same precondition as `swapDirect`).
656
+ * 2. Resolve USDC + verify broker is whitelisted on Orderly Vault.
657
+ * 3. Quote `Relay.tokenFee` for the deposit; compute `netDeposit`.
658
+ * 4. `buildPerpDepositViaRelay` → encoded `executeBatch(calls)`
659
+ * (USDC.approve(relay) + Relay.deposit + optional fee transfer).
660
+ * 5. `walletClient.sendTransaction({ to: userAddress, data: callData })`.
661
+ * 6. Wait for receipt (optional).
662
+ *
663
+ * Use when: same conditions as `swapDirect` — FE doesn't have Pimlico
664
+ * key, doesn't want sponsor-relayer dependency, user has small ETH.
665
+ *
666
+ * Throws when the user is NOT yet delegated.
667
+ *
668
+ * @example
669
+ * ```ts
670
+ * import { perpDepositDirect } from "@pafi-dev/trading";
671
+ *
672
+ * const result = await perpDepositDirect({
673
+ * userAddress: wallet.address,
674
+ * chainId: 8453,
675
+ * amount: parseUnits("10", 6), // 10 USDC
676
+ * brokerId: "orderly",
677
+ * publicClient,
678
+ * walletClient,
679
+ * });
680
+ * console.log("Deposit tx:", result.txHash);
681
+ * console.log("Net deposit to Orderly:", result.netDeposit, "USDC raw");
682
+ * ```
683
+ */
684
+ declare function perpDepositDirect(params: PerpDepositDirectParams): Promise<PerpDepositDirectResult>;
685
+
686
+ export { type ApiPerpDepositRequest, type ApiPerpDepositResponse, type ApiQuoteError, type ApiQuoteRequest, type ApiQuoteResponse, type ApiSwapRequest, type ApiSwapResponse, type BuildSwapUserOpParams, type PerpDepositDirectParams, type PerpDepositDirectResult, type SwapDirectParams, type SwapDirectResult, type SwapSimulationResult, TradingHandlers, type TradingHandlersConfig, buildAllPaths, buildErc20ApprovalCalldata, buildPermit2ApprovalCalldata, buildSwapFromQuote, buildSwapUserOp, buildUniversalRouterExecuteArgs, buildV4SwapInput, checkAllowance, combineRoutes, findBestQuote, perpDepositDirect, quoteBestRoute, quoteExactInput, quoteExactInputSingle, simulateSwap, swapDirect };
package/dist/index.js CHANGED
@@ -767,6 +767,261 @@ async function quoteOperatorFeeOutput(provider, chainId, outputTokenAddress) {
767
767
 
768
768
  // src/pools.ts
769
769
  import { fetchPafiPools, PAFI_SUBGRAPH_URL } from "@pafi-dev/core";
770
+
771
+ // src/direct/swapDirect.ts
772
+ import {
773
+ UNIVERSAL_ROUTER_ADDRESSES as UNIVERSAL_ROUTER_ADDRESSES2,
774
+ getContractAddresses as getContractAddresses2,
775
+ parseEip7702DelegatedAddress,
776
+ detectDelegateImpl,
777
+ SIMPLE_7702_IMPL_BASE_MAINNET,
778
+ BATCH_EXECUTOR_7702_IMPL
779
+ } from "@pafi-dev/core";
780
+ async function swapDirect(params) {
781
+ const code = await params.publicClient.getCode({
782
+ address: params.userAddress
783
+ });
784
+ const delegate = parseEip7702DelegatedAddress(code);
785
+ if (!delegate) {
786
+ throw new Error(
787
+ `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.`
788
+ );
789
+ }
790
+ const impl = detectDelegateImpl(delegate);
791
+ if (impl === "unknown") {
792
+ params.onWarning?.(
793
+ `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
+ );
795
+ }
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
+ let quoteResult;
804
+ try {
805
+ quoteResult = await findBestQuote(
806
+ params.publicClient,
807
+ params.chainId,
808
+ params.inputTokenAddress,
809
+ params.outputTokenAddress,
810
+ params.amount,
811
+ params.pools ?? []
812
+ );
813
+ } catch (err) {
814
+ const cause = err instanceof Error ? err.message : String(err);
815
+ throw new Error(
816
+ `swapDirect: no swap path found from ${params.inputTokenAddress} to ${params.outputTokenAddress} (cause: ${cause})`
817
+ );
818
+ }
819
+ const hops = quoteResult.bestRoute.path.length;
820
+ const slippageBps = params.slippageBps ?? (hops > 1 ? 100 : 50);
821
+ const estimatedOutputAmount = quoteResult.bestRoute.amountOut;
822
+ const minAmountOut = estimatedOutputAmount * BigInt(1e4 - slippageBps) / 10000n;
823
+ const gasFeeAmountOutput = params.gasFeeAmountOutput ?? 0n;
824
+ if (gasFeeAmountOutput > 0n && minAmountOut < gasFeeAmountOutput) {
825
+ throw new Error(
826
+ `swapDirect: minAmountOut (${minAmountOut}) below operator fee (${gasFeeAmountOutput})`
827
+ );
828
+ }
829
+ const deadline = params.deadline ?? BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
830
+ const { pafiFeeRecipient } = getContractAddresses2(params.chainId);
831
+ const userOp = buildSwapUserOp({
832
+ userAddress: params.userAddress,
833
+ aaNonce: 0n,
834
+ // ignored on the native-tx path; nonce comes from EOA tx count
835
+ inputTokenAddress: params.inputTokenAddress,
836
+ outputTokenAddress: params.outputTokenAddress,
837
+ universalRouterAddress: universalRouter,
838
+ amountIn: params.amount,
839
+ minAmountOut,
840
+ swapPath: quoteResult.bestRoute.path,
841
+ deadline,
842
+ gasFeeAmountOutput,
843
+ feeRecipient: pafiFeeRecipient
844
+ });
845
+ const account = params.walletClient.account;
846
+ if (!account) {
847
+ throw new Error(
848
+ "swapDirect: walletClient has no account attached \u2014 cannot send tx"
849
+ );
850
+ }
851
+ const txHash = await params.walletClient.sendTransaction({
852
+ account,
853
+ chain: params.walletClient.chain,
854
+ to: params.userAddress,
855
+ value: 0n,
856
+ data: userOp.callData
857
+ });
858
+ let receipt;
859
+ if (params.waitForReceipt !== false) {
860
+ try {
861
+ receipt = await params.publicClient.waitForTransactionReceipt({
862
+ hash: txHash
863
+ });
864
+ } catch (err) {
865
+ params.onWarning?.(
866
+ `swapDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
867
+ );
868
+ }
869
+ }
870
+ return {
871
+ txHash,
872
+ receipt,
873
+ estimatedOutputAmount,
874
+ minAmountOut,
875
+ hops,
876
+ deadline,
877
+ feeAmountUsed: gasFeeAmountOutput
878
+ };
879
+ }
880
+
881
+ // src/direct/perpDepositDirect.ts
882
+ import {
883
+ BROKER_HASHES as BROKER_HASHES2,
884
+ ORDERLY_RELAY_ABI as ORDERLY_RELAY_ABI2,
885
+ ORDERLY_VAULT_ABI as ORDERLY_VAULT_ABI2,
886
+ ORDERLY_VAULT_ADDRESSES as ORDERLY_VAULT_ADDRESSES2,
887
+ TOKEN_HASHES as TOKEN_HASHES2,
888
+ buildPerpDepositViaRelay as buildPerpDepositViaRelay2,
889
+ 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
895
+ } from "@pafi-dev/core";
896
+ async function perpDepositDirect(params) {
897
+ if (params.amount <= 0n) {
898
+ throw new Error("perpDepositDirect: amount must be positive");
899
+ }
900
+ const code = await params.publicClient.getCode({
901
+ address: params.userAddress
902
+ });
903
+ const delegate = parseEip7702DelegatedAddress2(code);
904
+ if (!delegate) {
905
+ throw new Error(
906
+ `perpDepositDirect: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first or use the AA path via \`TradingHandlers.handlePerpDeposit()\` + sponsor-relayer.`
907
+ );
908
+ }
909
+ const impl = detectDelegateImpl2(delegate);
910
+ if (impl === "unknown") {
911
+ 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.`
913
+ );
914
+ }
915
+ const vault = ORDERLY_VAULT_ADDRESSES2[params.chainId];
916
+ if (!vault) {
917
+ throw new Error(
918
+ `perpDepositDirect: no Orderly Vault for chainId ${params.chainId}`
919
+ );
920
+ }
921
+ const brokerHash = BROKER_HASHES2[params.brokerId];
922
+ if (!brokerHash) {
923
+ throw new Error(
924
+ `perpDepositDirect: unknown brokerId "${params.brokerId}"`
925
+ );
926
+ }
927
+ const tokenHash = TOKEN_HASHES2.USDC;
928
+ const [usdcAddress, brokerAllowed] = await Promise.all([
929
+ params.publicClient.readContract({
930
+ address: vault,
931
+ abi: ORDERLY_VAULT_ABI2,
932
+ functionName: "getAllowedToken",
933
+ args: [tokenHash]
934
+ }),
935
+ params.publicClient.readContract({
936
+ address: vault,
937
+ abi: ORDERLY_VAULT_ABI2,
938
+ functionName: "getAllowedBroker",
939
+ args: [brokerHash]
940
+ })
941
+ ]);
942
+ if (!brokerAllowed) {
943
+ throw new Error(
944
+ `perpDepositDirect: broker "${params.brokerId}" is not whitelisted on Orderly Vault`
945
+ );
946
+ }
947
+ const { orderlyRelay: relayAddress, pafiFeeRecipient } = getContractAddresses3(
948
+ params.chainId
949
+ );
950
+ const RELAY_FEE_FLOOR_USDC = 2000000n;
951
+ const percentCap = params.amount * 500n / 10000n;
952
+ const maxFee = params.maxRelayFee ?? (percentCap > RELAY_FEE_FLOOR_USDC ? percentCap : RELAY_FEE_FLOOR_USDC);
953
+ const relayRequest = {
954
+ token: usdcAddress,
955
+ receiver: params.userAddress,
956
+ brokerHash,
957
+ totalAmount: params.amount,
958
+ maxFee
959
+ };
960
+ const relayTokenFee = await params.publicClient.readContract({
961
+ address: relayAddress,
962
+ abi: ORDERLY_RELAY_ABI2,
963
+ functionName: "quoteTokenFee",
964
+ args: [relayRequest]
965
+ });
966
+ if (relayTokenFee > maxFee) {
967
+ throw new Error(
968
+ `perpDepositDirect: Relay tokenFee ${relayTokenFee} exceeds maxFee ${maxFee} \u2014 pass a larger \`maxRelayFee\` or increase \`amount\`.`
969
+ );
970
+ }
971
+ if (relayTokenFee >= params.amount) {
972
+ throw new Error(
973
+ `perpDepositDirect: deposit amount ${params.amount} below Relay fee ${relayTokenFee} \u2014 increase \`amount\`.`
974
+ );
975
+ }
976
+ const gasFeeUsdc = params.gasFeeUsdc ?? 0n;
977
+ const partial = buildPerpDepositViaRelay2({
978
+ userAddress: params.userAddress,
979
+ aaNonce: 0n,
980
+ // ignored on the native-tx path
981
+ relayAddress,
982
+ request: relayRequest,
983
+ gasFeeUsdc: gasFeeUsdc > 0n ? gasFeeUsdc : void 0,
984
+ gasFeeUsdcRecipient: gasFeeUsdc > 0n ? pafiFeeRecipient : void 0
985
+ });
986
+ const account = params.walletClient.account;
987
+ if (!account) {
988
+ throw new Error(
989
+ "perpDepositDirect: walletClient has no account attached \u2014 cannot send tx"
990
+ );
991
+ }
992
+ const txHash = await params.walletClient.sendTransaction({
993
+ account,
994
+ chain: params.walletClient.chain,
995
+ to: params.userAddress,
996
+ value: 0n,
997
+ data: partial.callData
998
+ });
999
+ let receipt;
1000
+ if (params.waitForReceipt !== false) {
1001
+ try {
1002
+ receipt = await params.publicClient.waitForTransactionReceipt({
1003
+ hash: txHash
1004
+ });
1005
+ } catch (err) {
1006
+ params.onWarning?.(
1007
+ `perpDepositDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
1008
+ );
1009
+ }
1010
+ }
1011
+ const accountId = computeAccountId2(params.userAddress, brokerHash);
1012
+ return {
1013
+ txHash,
1014
+ receipt,
1015
+ relayTokenFee,
1016
+ maxFee,
1017
+ netDeposit: params.amount - relayTokenFee,
1018
+ feeAmountUsed: gasFeeUsdc,
1019
+ accountId,
1020
+ brokerHash,
1021
+ usdcAddress,
1022
+ relayAddress
1023
+ };
1024
+ }
770
1025
  export {
771
1026
  PAFI_SUBGRAPH_URL,
772
1027
  TradingHandlers,
@@ -781,9 +1036,11 @@ export {
781
1036
  combineRoutes,
782
1037
  fetchPafiPools,
783
1038
  findBestQuote,
1039
+ perpDepositDirect,
784
1040
  quoteBestRoute,
785
1041
  quoteExactInput,
786
1042
  quoteExactInputSingle,
787
- simulateSwap
1043
+ simulateSwap,
1044
+ swapDirect
788
1045
  };
789
1046
  //# sourceMappingURL=index.js.map