@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/README.md +95 -0
- package/dist/index.cjs +840 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +541 -19
- package/dist/index.d.ts +541 -19
- package/dist/index.js +854 -14
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
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
|
-
|
|
1623
|
+
quoteExactOutput,
|
|
1624
|
+
quoteExactOutputSingle,
|
|
1625
|
+
simulateSwap,
|
|
1626
|
+
swapDirect,
|
|
1627
|
+
swapDirectExactOut
|
|
788
1628
|
};
|
|
789
1629
|
//# sourceMappingURL=index.js.map
|