@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/README.md +95 -0
- package/dist/index.cjs +330 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +178 -3
- package/dist/index.d.ts +178 -3
- package/dist/index.js +348 -23
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
464
|
-
|
|
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
|
|
478
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
578
|
-
|
|
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
|
|
620
|
-
|
|
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
|
|
627
|
-
|
|
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
|