@pafi-dev/trading 0.3.2 → 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
@@ -10,6 +10,7 @@ import {
10
10
  ORDERLY_VAULT_ADDRESSES,
11
11
  BROKER_HASHES,
12
12
  TOKEN_HASHES,
13
+ ValidationError,
13
14
  computeAccountId,
14
15
  quoteOperatorFeePt,
15
16
  quoteOperatorFeeUsdt
@@ -100,6 +101,12 @@ async function quoteBestRoute(client, quoterAddress, exactCurrency, routes, exac
100
101
  {
101
102
  exactCurrency,
102
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.
103
110
  exactAmount: BigInt(exactAmount)
104
111
  }
105
112
  ]
@@ -107,15 +114,21 @@ async function quoteBestRoute(client, quoterAddress, exactCurrency, routes, exac
107
114
  allowFailure: true
108
115
  });
109
116
  const allRoutes = [];
117
+ let firstFailure;
110
118
  for (let i = 0; i < results.length; i++) {
111
119
  const r = results[i];
112
120
  if (r.status === "success") {
113
121
  const [amountOut, gasEstimate] = r.result;
114
122
  allRoutes.push({ amountOut, gasEstimate, path: routes[i] });
123
+ } else if (firstFailure === void 0) {
124
+ const errMsg = r.error instanceof Error ? r.error.message : String(r.error ?? "unknown");
125
+ firstFailure = errMsg;
115
126
  }
116
127
  }
117
128
  if (allRoutes.length === 0) {
118
- throw new Error("No valid routes found");
129
+ throw new Error(
130
+ `No valid routes found from ${exactCurrency} (${routes.length} candidates probed)` + (firstFailure ? `; first failure: ${firstFailure}` : "")
131
+ );
119
132
  }
120
133
  const bestRoute = allRoutes.reduce(
121
134
  (best, current) => current.amountOut > best.amountOut ? current : best
@@ -296,6 +309,12 @@ function buildSwapUserOp(params) {
296
309
  "buildSwapUserOp: swapPath must contain at least one PathKey"
297
310
  );
298
311
  }
312
+ const PERMIT2_EXPIRATION_MAX = 2n ** 48n - 1n;
313
+ if (params.deadline <= 0n || params.deadline > PERMIT2_EXPIRATION_MAX) {
314
+ throw new Error(
315
+ `buildSwapUserOp: deadline (${params.deadline}) must be unix seconds in (0, 2^48-1]. Did you accidentally pass milliseconds?`
316
+ );
317
+ }
299
318
  const { commands, inputs } = buildUniversalRouterExecuteArgs(
300
319
  params.inputTokenAddress,
301
320
  params.swapPath,
@@ -361,7 +380,11 @@ var TradingHandlers = class {
361
380
  */
362
381
  async handleQuote(request) {
363
382
  if (request.chainId !== this.chainId) {
364
- throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);
383
+ throw new ValidationError(
384
+ "UNSUPPORTED_CHAIN_ID",
385
+ `handleQuote: unsupported chainId ${request.chainId}`,
386
+ { requested: request.chainId, supported: this.chainId }
387
+ );
365
388
  }
366
389
  if (request.amount === 0n) {
367
390
  return {
@@ -430,20 +453,31 @@ var TradingHandlers = class {
430
453
  */
431
454
  async handleSwap(authenticatedAddress, request) {
432
455
  if (getAddress(authenticatedAddress) !== getAddress(request.userAddress)) {
433
- throw new Error(
456
+ throw new ValidationError(
457
+ "USER_ADDRESS_MISMATCH",
434
458
  `handleSwap: authenticatedAddress (${authenticatedAddress}) does not match request.userAddress (${request.userAddress})`
435
459
  );
436
460
  }
437
461
  if (request.chainId !== this.chainId) {
438
- throw new Error(`handleSwap: unsupported chainId ${request.chainId}`);
462
+ throw new ValidationError(
463
+ "UNSUPPORTED_CHAIN_ID",
464
+ `handleSwap: unsupported chainId ${request.chainId}`,
465
+ { requested: request.chainId, supported: this.chainId }
466
+ );
439
467
  }
440
468
  if (request.amount <= 0n) {
441
- throw new Error("handleSwap: amount must be positive");
469
+ throw new ValidationError(
470
+ "INVALID_AMOUNT",
471
+ "handleSwap: amount must be positive"
472
+ );
442
473
  }
443
474
  const { pafiFeeRecipient } = getContractAddresses(request.chainId);
444
475
  const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
445
476
  if (!universalRouter) {
446
- throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);
477
+ throw new ValidationError(
478
+ "ROUTER_NOT_DEPLOYED",
479
+ `handleSwap: no UniversalRouter for chainId ${request.chainId}`
480
+ );
447
481
  }
448
482
  const inputTokenAddress = getAddress(request.inputTokenAddress);
449
483
  const outputTokenAddress = getAddress(request.outputTokenAddress);
@@ -459,9 +493,11 @@ var TradingHandlers = class {
459
493
  request.amount,
460
494
  pools
461
495
  );
462
- } catch {
463
- throw new Error(
464
- `handleSwap: no swap path found from ${inputTokenAddress} to ${outputTokenAddress}`
496
+ } catch (err) {
497
+ const cause = err instanceof Error ? err.message : String(err);
498
+ throw new ValidationError(
499
+ "NO_SWAP_PATH",
500
+ `handleSwap: no swap path found from ${inputTokenAddress} to ${outputTokenAddress} (cause: ${cause})`
465
501
  );
466
502
  }
467
503
  const gasFeeAmountOutput = request.gasFeeAmountOutput !== void 0 ? request.gasFeeAmountOutput : await quoteOperatorFeeOutput(
@@ -474,8 +510,13 @@ var TradingHandlers = class {
474
510
  const estimatedOutputAmount = quoteResult.bestRoute.amountOut;
475
511
  const minAmountOut = estimatedOutputAmount * BigInt(1e4 - slippageBps) / 10000n;
476
512
  if (gasFeeAmountOutput > 0n && minAmountOut < gasFeeAmountOutput) {
477
- throw new Error(
478
- `handleSwap: minAmountOut (${minAmountOut}) below operator fee (${gasFeeAmountOutput}) \u2014 increase swap amount or accept tighter slippage`
513
+ throw new ValidationError(
514
+ "AMOUNT_TOO_SMALL_FOR_FEE",
515
+ `handleSwap: minAmountOut (${minAmountOut}) below operator fee (${gasFeeAmountOutput}) \u2014 increase swap amount or accept tighter slippage`,
516
+ {
517
+ minAmountOut: minAmountOut.toString(),
518
+ gasFeeAmountOutput: gasFeeAmountOutput.toString()
519
+ }
479
520
  );
480
521
  }
481
522
  const deadline = BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
@@ -539,23 +580,38 @@ var TradingHandlers = class {
539
580
  */
540
581
  async handlePerpDeposit(authenticatedAddress, request) {
541
582
  if (getAddress(authenticatedAddress) !== getAddress(request.userAddress)) {
542
- throw new Error(
583
+ throw new ValidationError(
584
+ "USER_ADDRESS_MISMATCH",
543
585
  `handlePerpDeposit: authenticatedAddress (${authenticatedAddress}) does not match request.userAddress (${request.userAddress})`
544
586
  );
545
587
  }
546
588
  if (request.chainId !== this.chainId) {
547
- throw new Error(`handlePerpDeposit: unsupported chainId ${request.chainId}`);
589
+ throw new ValidationError(
590
+ "UNSUPPORTED_CHAIN_ID",
591
+ `handlePerpDeposit: unsupported chainId ${request.chainId}`,
592
+ { requested: request.chainId, supported: this.chainId }
593
+ );
548
594
  }
549
595
  if (request.amount <= 0n) {
550
- throw new Error("handlePerpDeposit: amount must be positive");
596
+ throw new ValidationError(
597
+ "INVALID_AMOUNT",
598
+ "handlePerpDeposit: amount must be positive"
599
+ );
551
600
  }
552
601
  const vault = ORDERLY_VAULT_ADDRESSES[request.chainId];
553
602
  if (!vault) {
554
- throw new Error(`handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`);
603
+ throw new ValidationError(
604
+ "VAULT_NOT_DEPLOYED",
605
+ `handlePerpDeposit: no Orderly Vault for chainId ${request.chainId}`
606
+ );
555
607
  }
556
608
  const brokerHash = BROKER_HASHES[request.brokerId];
557
609
  if (!brokerHash) {
558
- throw new Error(`handlePerpDeposit: unknown brokerId "${request.brokerId}"`);
610
+ throw new ValidationError(
611
+ "UNKNOWN_BROKER_ID",
612
+ `handlePerpDeposit: unknown brokerId "${request.brokerId}"`,
613
+ { brokerId: request.brokerId }
614
+ );
559
615
  }
560
616
  const tokenHash = TOKEN_HASHES.USDC;
561
617
  const userAddress = getAddress(request.userAddress);
@@ -574,8 +630,10 @@ var TradingHandlers = class {
574
630
  })
575
631
  ]);
576
632
  if (!brokerAllowed) {
577
- throw new Error(
578
- `handlePerpDeposit: broker "${request.brokerId}" is not whitelisted on Orderly Vault`
633
+ throw new ValidationError(
634
+ "BROKER_NOT_WHITELISTED",
635
+ `handlePerpDeposit: broker "${request.brokerId}" is not whitelisted on Orderly Vault`,
636
+ { brokerId: request.brokerId, brokerHash }
579
637
  );
580
638
  }
581
639
  const accountId = computeAccountId(userAddress, brokerHash);
@@ -616,15 +674,25 @@ var TradingHandlers = class {
616
674
  args: [relayRequest]
617
675
  });
618
676
  if (relayTokenFee > maxRelayFee) {
619
- throw new Error(
620
- `handlePerpDeposit: Relay tokenFee ${relayTokenFee} (\u2248 ${Number(relayTokenFee) / 1e6} USDC) exceeds maxRelayFee ${maxRelayFee} \u2014 pass a larger \`maxRelayFee\` or increase the deposit \`amount\` so the fee becomes a smaller share of the total.`
677
+ throw new ValidationError(
678
+ "RELAY_FEE_EXCEEDS_MAX",
679
+ `handlePerpDeposit: Relay tokenFee ${relayTokenFee} (\u2248 ${Number(relayTokenFee) / 1e6} USDC) exceeds maxRelayFee ${maxRelayFee} \u2014 pass a larger \`maxRelayFee\` or increase the deposit \`amount\` so the fee becomes a smaller share of the total.`,
680
+ {
681
+ relayTokenFee: relayTokenFee.toString(),
682
+ maxRelayFee: maxRelayFee.toString()
683
+ }
621
684
  );
622
685
  }
623
686
  if (relayTokenFee >= request.amount) {
624
687
  const feeUsdc = Number(relayTokenFee) / 1e6;
625
688
  const amountUsdc = Number(request.amount) / 1e6;
626
- throw new Error(
627
- `handlePerpDeposit: deposit amount ${amountUsdc} USDC is below the Relay fee ${feeUsdc} USDC \u2014 increase \`amount\` to at least ${(feeUsdc * 2).toFixed(6)} USDC so a meaningful balance reaches your Orderly account after the Relay charge.`
689
+ throw new ValidationError(
690
+ "RELAY_FEE_EXCEEDS_AMOUNT",
691
+ `handlePerpDeposit: deposit amount ${amountUsdc} USDC is below the Relay fee ${feeUsdc} USDC \u2014 increase \`amount\` to at least ${(feeUsdc * 2).toFixed(6)} USDC so a meaningful balance reaches your Orderly account after the Relay charge.`,
692
+ {
693
+ relayTokenFee: relayTokenFee.toString(),
694
+ amount: request.amount.toString()
695
+ }
628
696
  );
629
697
  }
630
698
  const userOp2 = buildPerpDepositViaRelay({
@@ -699,6 +767,261 @@ async function quoteOperatorFeeOutput(provider, chainId, outputTokenAddress) {
699
767
 
700
768
  // src/pools.ts
701
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
+ }
702
1025
  export {
703
1026
  PAFI_SUBGRAPH_URL,
704
1027
  TradingHandlers,
@@ -713,9 +1036,11 @@ export {
713
1036
  combineRoutes,
714
1037
  fetchPafiPools,
715
1038
  findBestQuote,
1039
+ perpDepositDirect,
716
1040
  quoteBestRoute,
717
1041
  quoteExactInput,
718
1042
  quoteExactInputSingle,
719
- simulateSwap
1043
+ simulateSwap,
1044
+ swapDirect
720
1045
  };
721
1046
  //# sourceMappingURL=index.js.map