@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.cjs
CHANGED
|
@@ -21,22 +21,34 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
PAFI_SUBGRAPH_URL: () => import_core10.PAFI_SUBGRAPH_URL,
|
|
24
|
+
SWAP_EXACT_OUT: () => SWAP_EXACT_OUT,
|
|
25
|
+
SWAP_EXACT_OUT_SINGLE: () => SWAP_EXACT_OUT_SINGLE,
|
|
24
26
|
TradingHandlers: () => TradingHandlers,
|
|
25
27
|
buildAllPaths: () => buildAllPaths,
|
|
26
28
|
buildErc20ApprovalCalldata: () => buildErc20ApprovalCalldata,
|
|
27
29
|
buildPermit2ApprovalCalldata: () => buildPermit2ApprovalCalldata,
|
|
28
30
|
buildSwapFromQuote: () => buildSwapFromQuote,
|
|
29
31
|
buildSwapUserOp: () => buildSwapUserOp,
|
|
32
|
+
buildSwapUserOpExactOut: () => buildSwapUserOpExactOut,
|
|
30
33
|
buildUniversalRouterExecuteArgs: () => buildUniversalRouterExecuteArgs,
|
|
34
|
+
buildUniversalRouterExecuteArgsExactOut: () => buildUniversalRouterExecuteArgsExactOut,
|
|
31
35
|
buildV4SwapInput: () => buildV4SwapInput,
|
|
36
|
+
buildV4SwapInputExactOut: () => buildV4SwapInputExactOut,
|
|
32
37
|
checkAllowance: () => checkAllowance,
|
|
33
38
|
combineRoutes: () => combineRoutes,
|
|
34
39
|
fetchPafiPools: () => import_core10.fetchPafiPools,
|
|
35
40
|
findBestQuote: () => findBestQuote,
|
|
41
|
+
findBestQuoteExactOut: () => findBestQuoteExactOut,
|
|
42
|
+
perpDepositDirect: () => perpDepositDirect,
|
|
36
43
|
quoteBestRoute: () => quoteBestRoute,
|
|
44
|
+
quoteBestRouteExactOut: () => quoteBestRouteExactOut,
|
|
37
45
|
quoteExactInput: () => quoteExactInput,
|
|
38
46
|
quoteExactInputSingle: () => quoteExactInputSingle,
|
|
39
|
-
|
|
47
|
+
quoteExactOutput: () => quoteExactOutput,
|
|
48
|
+
quoteExactOutputSingle: () => quoteExactOutputSingle,
|
|
49
|
+
simulateSwap: () => simulateSwap,
|
|
50
|
+
swapDirect: () => swapDirect,
|
|
51
|
+
swapDirectExactOut: () => swapDirectExactOut
|
|
40
52
|
});
|
|
41
53
|
module.exports = __toCommonJS(index_exports);
|
|
42
54
|
|
|
@@ -99,7 +111,7 @@ async function quoteExactInput(client, quoterAddress, exactCurrency, path, exact
|
|
|
99
111
|
address: quoterAddress,
|
|
100
112
|
abi: import_core2.v4QuoterAbi,
|
|
101
113
|
functionName: "quoteExactInput",
|
|
102
|
-
args: [{ exactCurrency, path, exactAmount
|
|
114
|
+
args: [{ exactCurrency, path, exactAmount }]
|
|
103
115
|
});
|
|
104
116
|
return { amountOut, gasEstimate, path };
|
|
105
117
|
}
|
|
@@ -112,7 +124,7 @@ async function quoteExactInputSingle(client, quoterAddress, poolKey, zeroForOne,
|
|
|
112
124
|
{
|
|
113
125
|
poolKey,
|
|
114
126
|
zeroForOne,
|
|
115
|
-
exactAmount
|
|
127
|
+
exactAmount,
|
|
116
128
|
hookData
|
|
117
129
|
}
|
|
118
130
|
]
|
|
@@ -129,13 +141,7 @@ async function quoteBestRoute(client, quoterAddress, exactCurrency, routes, exac
|
|
|
129
141
|
{
|
|
130
142
|
exactCurrency,
|
|
131
143
|
path,
|
|
132
|
-
|
|
133
|
-
// signature takes `uint256` (a bigint at the wire level), but
|
|
134
|
-
// our pinned ABI types it as `number`. The runtime is fine
|
|
135
|
-
// (bigint serializes correctly) — this cast tells TS to skip
|
|
136
|
-
// the structural check until we re-generate the ABI with the
|
|
137
|
-
// correct uint256 type. Tracked: SDK_CORE_TRADING_AUDIT.md H11.
|
|
138
|
-
exactAmount: BigInt(exactAmount)
|
|
144
|
+
exactAmount
|
|
139
145
|
}
|
|
140
146
|
]
|
|
141
147
|
})),
|
|
@@ -176,6 +182,84 @@ async function findBestQuote(client, chainId, tokenIn, tokenOut, exactAmount, po
|
|
|
176
182
|
}
|
|
177
183
|
return quoteBestRoute(client, quoter, tokenIn, paths, exactAmount);
|
|
178
184
|
}
|
|
185
|
+
async function quoteExactOutput(client, quoterAddress, exactCurrency, path, exactAmount) {
|
|
186
|
+
const [amountIn, gasEstimate] = await client.readContract({
|
|
187
|
+
address: quoterAddress,
|
|
188
|
+
abi: import_core2.v4QuoterAbi,
|
|
189
|
+
functionName: "quoteExactOutput",
|
|
190
|
+
args: [{ exactCurrency, path, exactAmount }]
|
|
191
|
+
});
|
|
192
|
+
return { amountIn, gasEstimate, path };
|
|
193
|
+
}
|
|
194
|
+
async function quoteExactOutputSingle(client, quoterAddress, poolKey, zeroForOne, exactAmount, hookData) {
|
|
195
|
+
const [amountIn, gasEstimate] = await client.readContract({
|
|
196
|
+
address: quoterAddress,
|
|
197
|
+
abi: import_core2.v4QuoterAbi,
|
|
198
|
+
functionName: "quoteExactOutputSingle",
|
|
199
|
+
args: [
|
|
200
|
+
{
|
|
201
|
+
poolKey,
|
|
202
|
+
zeroForOne,
|
|
203
|
+
exactAmount,
|
|
204
|
+
hookData
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
});
|
|
208
|
+
return { amountIn, gasEstimate };
|
|
209
|
+
}
|
|
210
|
+
async function quoteBestRouteExactOut(client, quoterAddress, exactCurrency, routes, exactAmount) {
|
|
211
|
+
const results = await client.multicall({
|
|
212
|
+
contracts: routes.map((path) => ({
|
|
213
|
+
address: quoterAddress,
|
|
214
|
+
abi: import_core2.v4QuoterAbi,
|
|
215
|
+
functionName: "quoteExactOutput",
|
|
216
|
+
args: [
|
|
217
|
+
{
|
|
218
|
+
exactCurrency,
|
|
219
|
+
path,
|
|
220
|
+
exactAmount
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
})),
|
|
224
|
+
allowFailure: true
|
|
225
|
+
});
|
|
226
|
+
const allRoutes = [];
|
|
227
|
+
let firstFailure;
|
|
228
|
+
for (let i = 0; i < results.length; i++) {
|
|
229
|
+
const r = results[i];
|
|
230
|
+
if (r.status === "success") {
|
|
231
|
+
const [amountIn, gasEstimate] = r.result;
|
|
232
|
+
allRoutes.push({ amountIn, gasEstimate, path: routes[i] });
|
|
233
|
+
} else if (firstFailure === void 0) {
|
|
234
|
+
const errMsg = r.error instanceof Error ? r.error.message : String(r.error ?? "unknown");
|
|
235
|
+
firstFailure = errMsg;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (allRoutes.length === 0) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`No valid exact-output routes found to ${exactCurrency} (${routes.length} candidates probed)` + (firstFailure ? `; first failure: ${firstFailure}` : "")
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const bestRoute = allRoutes.reduce(
|
|
244
|
+
(best, current) => current.amountIn < best.amountIn ? current : best
|
|
245
|
+
);
|
|
246
|
+
return { bestRoute, allRoutes };
|
|
247
|
+
}
|
|
248
|
+
async function findBestQuoteExactOut(client, chainId, tokenIn, tokenOut, exactAmount, pools = [], quoterAddress, maxHops = 3) {
|
|
249
|
+
const quoter = quoterAddress ?? import_core3.V4_QUOTER_ADDRESSES[chainId];
|
|
250
|
+
if (!quoter) {
|
|
251
|
+
throw new Error(`No V4 Quoter address configured for chain ${chainId}`);
|
|
252
|
+
}
|
|
253
|
+
const commonPools = import_core3.COMMON_POOLS[chainId] ?? [];
|
|
254
|
+
const allPools = [...pools, ...commonPools];
|
|
255
|
+
const paths = buildAllPaths(allPools, tokenOut, tokenIn, maxHops);
|
|
256
|
+
if (paths.length === 0) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`No exact-output paths found to ${tokenOut} from ${tokenIn}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return quoteBestRouteExactOut(client, quoter, tokenOut, paths, exactAmount);
|
|
262
|
+
}
|
|
179
263
|
|
|
180
264
|
// src/swap/approval.ts
|
|
181
265
|
var import_viem = require("viem");
|
|
@@ -208,8 +292,11 @@ function buildPermit2ApprovalCalldata(token, spender, amount, expiration) {
|
|
|
208
292
|
var import_viem2 = require("viem");
|
|
209
293
|
var V4_SWAP = 16;
|
|
210
294
|
var SWAP_EXACT_IN = 7;
|
|
295
|
+
var SWAP_EXACT_OUT_SINGLE = 8;
|
|
296
|
+
var SWAP_EXACT_OUT = 9;
|
|
211
297
|
var SETTLE_ALL = 12;
|
|
212
298
|
var TAKE_ALL = 15;
|
|
299
|
+
var UINT128_MAX = 2n ** 128n - 1n;
|
|
213
300
|
var PATH_KEY_ABI_COMPONENTS = [
|
|
214
301
|
{ name: "intermediateCurrency", type: "address" },
|
|
215
302
|
{ name: "fee", type: "uint256" },
|
|
@@ -227,6 +314,16 @@ var EXACT_INPUT_PARAMS_ABI = [
|
|
|
227
314
|
{ name: "amountIn", type: "uint128" },
|
|
228
315
|
{ name: "amountOutMinimum", type: "uint128" }
|
|
229
316
|
];
|
|
317
|
+
var EXACT_OUTPUT_PARAMS_ABI = [
|
|
318
|
+
{ name: "currencyOut", type: "address" },
|
|
319
|
+
{
|
|
320
|
+
name: "path",
|
|
321
|
+
type: "tuple[]",
|
|
322
|
+
components: PATH_KEY_ABI_COMPONENTS
|
|
323
|
+
},
|
|
324
|
+
{ name: "amountOut", type: "uint128" },
|
|
325
|
+
{ name: "amountInMaximum", type: "uint128" }
|
|
326
|
+
];
|
|
230
327
|
function buildV4SwapInput(currencyIn, path, amountIn, minAmountOut, outputCurrency) {
|
|
231
328
|
const actions = (0, import_viem2.encodePacked)(
|
|
232
329
|
["uint8", "uint8", "uint8"],
|
|
@@ -278,7 +375,81 @@ function buildUniversalRouterExecuteArgs(currencyIn, path, amountIn, minAmountOu
|
|
|
278
375
|
];
|
|
279
376
|
return { commands, inputs };
|
|
280
377
|
}
|
|
378
|
+
function buildV4SwapInputExactOut(currencyOut, path, amountOut, maxAmountIn, inputCurrency) {
|
|
379
|
+
if (amountOut <= 0n || amountOut > UINT128_MAX) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`buildV4SwapInputExactOut: amountOut (${amountOut}) must be in (0, 2^128-1]`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (maxAmountIn <= 0n || maxAmountIn > UINT128_MAX) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`buildV4SwapInputExactOut: maxAmountIn (${maxAmountIn}) must be in (0, 2^128-1]`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const actions = (0, import_viem2.encodePacked)(
|
|
390
|
+
["uint8", "uint8", "uint8"],
|
|
391
|
+
[SWAP_EXACT_OUT, SETTLE_ALL, TAKE_ALL]
|
|
392
|
+
);
|
|
393
|
+
const swapParam = (0, import_viem2.encodeAbiParameters)(
|
|
394
|
+
[{ name: "swap", type: "tuple", components: EXACT_OUTPUT_PARAMS_ABI }],
|
|
395
|
+
[
|
|
396
|
+
{
|
|
397
|
+
currencyOut,
|
|
398
|
+
path: path.map((p) => ({
|
|
399
|
+
intermediateCurrency: p.intermediateCurrency,
|
|
400
|
+
fee: BigInt(p.fee),
|
|
401
|
+
tickSpacing: p.tickSpacing,
|
|
402
|
+
hooks: p.hooks,
|
|
403
|
+
hookData: p.hookData
|
|
404
|
+
})),
|
|
405
|
+
amountOut,
|
|
406
|
+
amountInMaximum: maxAmountIn
|
|
407
|
+
}
|
|
408
|
+
]
|
|
409
|
+
);
|
|
410
|
+
const settleParam = (0, import_viem2.encodeAbiParameters)(
|
|
411
|
+
[
|
|
412
|
+
{ name: "currency", type: "address" },
|
|
413
|
+
{ name: "maxAmount", type: "uint256" }
|
|
414
|
+
],
|
|
415
|
+
[inputCurrency, maxAmountIn]
|
|
416
|
+
);
|
|
417
|
+
const takeParam = (0, import_viem2.encodeAbiParameters)(
|
|
418
|
+
[
|
|
419
|
+
{ name: "currency", type: "address" },
|
|
420
|
+
{ name: "minAmount", type: "uint256" }
|
|
421
|
+
],
|
|
422
|
+
[currencyOut, amountOut]
|
|
423
|
+
);
|
|
424
|
+
return (0, import_viem2.encodeAbiParameters)(
|
|
425
|
+
[
|
|
426
|
+
{ name: "actions", type: "bytes" },
|
|
427
|
+
{ name: "params", type: "bytes[]" }
|
|
428
|
+
],
|
|
429
|
+
[actions, [swapParam, settleParam, takeParam]]
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
function buildUniversalRouterExecuteArgsExactOut(currencyOut, path, amountOut, maxAmountIn, inputCurrency) {
|
|
433
|
+
const commands = (0, import_viem2.encodePacked)(["uint8"], [V4_SWAP]);
|
|
434
|
+
const inputs = [
|
|
435
|
+
buildV4SwapInputExactOut(
|
|
436
|
+
currencyOut,
|
|
437
|
+
path,
|
|
438
|
+
amountOut,
|
|
439
|
+
maxAmountIn,
|
|
440
|
+
inputCurrency
|
|
441
|
+
)
|
|
442
|
+
];
|
|
443
|
+
return { commands, inputs };
|
|
444
|
+
}
|
|
445
|
+
var _buildSwapFromQuoteWarned = false;
|
|
281
446
|
function buildSwapFromQuote(params) {
|
|
447
|
+
if (!_buildSwapFromQuoteWarned) {
|
|
448
|
+
_buildSwapFromQuoteWarned = true;
|
|
449
|
+
console.warn(
|
|
450
|
+
"[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`."
|
|
451
|
+
);
|
|
452
|
+
}
|
|
282
453
|
return buildUniversalRouterExecuteArgs(
|
|
283
454
|
params.currencyIn,
|
|
284
455
|
params.quote.path,
|
|
@@ -379,6 +550,82 @@ function buildSwapUserOp(params) {
|
|
|
379
550
|
}
|
|
380
551
|
});
|
|
381
552
|
}
|
|
553
|
+
function buildSwapUserOpExactOut(params) {
|
|
554
|
+
if (params.amountOut <= 0n) {
|
|
555
|
+
throw new Error("buildSwapUserOpExactOut: amountOut must be positive");
|
|
556
|
+
}
|
|
557
|
+
if (params.maxAmountIn <= 0n) {
|
|
558
|
+
throw new Error("buildSwapUserOpExactOut: maxAmountIn must be positive");
|
|
559
|
+
}
|
|
560
|
+
if (params.gasFeeAmountInput < 0n) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
"buildSwapUserOpExactOut: gasFeeAmountInput must be non-negative"
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if (params.swapPath.length === 0) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
"buildSwapUserOpExactOut: swapPath must contain at least one PathKey"
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const PERMIT2_EXPIRATION_MAX = 2n ** 48n - 1n;
|
|
571
|
+
if (params.deadline <= 0n || params.deadline > PERMIT2_EXPIRATION_MAX) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`buildSwapUserOpExactOut: deadline (${params.deadline}) must be unix seconds in (0, 2^48-1]. Did you accidentally pass milliseconds?`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const { commands, inputs } = buildUniversalRouterExecuteArgsExactOut(
|
|
577
|
+
params.outputTokenAddress,
|
|
578
|
+
params.swapPath,
|
|
579
|
+
params.amountOut,
|
|
580
|
+
params.maxAmountIn,
|
|
581
|
+
params.inputTokenAddress
|
|
582
|
+
);
|
|
583
|
+
const swapCallData = (0, import_viem3.encodeFunctionData)({
|
|
584
|
+
abi: import_core8.universalRouterAbi,
|
|
585
|
+
functionName: "execute",
|
|
586
|
+
args: [commands, inputs, params.deadline]
|
|
587
|
+
});
|
|
588
|
+
const permit2ApproveData = buildPermit2ApprovalCalldata(
|
|
589
|
+
params.inputTokenAddress,
|
|
590
|
+
params.universalRouterAddress,
|
|
591
|
+
params.maxAmountIn,
|
|
592
|
+
Number(params.deadline)
|
|
593
|
+
);
|
|
594
|
+
const operations = [];
|
|
595
|
+
if (params.gasFeeAmountInput > 0n) {
|
|
596
|
+
operations.push(
|
|
597
|
+
(0, import_core8.erc20TransferOp)(
|
|
598
|
+
params.inputTokenAddress,
|
|
599
|
+
params.feeRecipient,
|
|
600
|
+
params.gasFeeAmountInput
|
|
601
|
+
)
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
operations.push(
|
|
605
|
+
(0, import_core8.erc20ApproveOp)(params.inputTokenAddress, import_core8.PERMIT2_ADDRESS, params.maxAmountIn),
|
|
606
|
+
(0, import_core8.rawCallOp)(import_core8.PERMIT2_ADDRESS, permit2ApproveData),
|
|
607
|
+
(0, import_core8.rawCallOp)(params.universalRouterAddress, swapCallData)
|
|
608
|
+
);
|
|
609
|
+
return (0, import_core8.buildPartialUserOperation)({
|
|
610
|
+
sender: params.userAddress,
|
|
611
|
+
nonce: params.aaNonce,
|
|
612
|
+
operations,
|
|
613
|
+
gasLimits: {
|
|
614
|
+
// +50k headroom for the additional pre-swap ERC-20 transfer when
|
|
615
|
+
// the input-side fee is non-zero. Keeps margin even when fee=0n.
|
|
616
|
+
callGasLimit: params.gasLimits?.callGasLimit ?? 750000n,
|
|
617
|
+
verificationGasLimit: params.gasLimits?.verificationGasLimit ?? 150000n,
|
|
618
|
+
preVerificationGas: params.gasLimits?.preVerificationGas ?? 50000n
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/swap/slippage.ts
|
|
624
|
+
var MAX_SLIPPAGE_BPS = 5e3;
|
|
625
|
+
function isValidSlippageBps(value) {
|
|
626
|
+
if (value === void 0) return true;
|
|
627
|
+
return Number.isInteger(value) && value >= 0 && value <= MAX_SLIPPAGE_BPS;
|
|
628
|
+
}
|
|
382
629
|
|
|
383
630
|
// src/api/handlers.ts
|
|
384
631
|
var TradingHandlers = class {
|
|
@@ -467,10 +714,9 @@ var TradingHandlers = class {
|
|
|
467
714
|
* net. Quote response surfaces both `estimatedOutputAmount` (gross)
|
|
468
715
|
* and `outputNet` so the FE can display reality.
|
|
469
716
|
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* `request.userAddress`. See SDK_CORE_TRADING_AUDIT.md C6.
|
|
717
|
+
* `authenticatedAddress` first param: caller (issuer controller / FE
|
|
718
|
+
* proxy) MUST pass the address extracted from the verified
|
|
719
|
+
* session/JWT. Handler asserts it equals `request.userAddress`.
|
|
474
720
|
*/
|
|
475
721
|
async handleSwap(authenticatedAddress, request) {
|
|
476
722
|
if ((0, import_viem4.getAddress)(authenticatedAddress) !== (0, import_viem4.getAddress)(request.userAddress)) {
|
|
@@ -492,6 +738,13 @@ var TradingHandlers = class {
|
|
|
492
738
|
"handleSwap: amount must be positive"
|
|
493
739
|
);
|
|
494
740
|
}
|
|
741
|
+
if (!isValidSlippageBps(request.slippageBps)) {
|
|
742
|
+
throw new import_core9.ValidationError(
|
|
743
|
+
"INVALID_SLIPPAGE",
|
|
744
|
+
`handleSwap: slippageBps (${request.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`,
|
|
745
|
+
{ received: request.slippageBps, max: MAX_SLIPPAGE_BPS }
|
|
746
|
+
);
|
|
747
|
+
}
|
|
495
748
|
const { pafiFeeRecipient } = (0, import_core9.getContractAddresses)(request.chainId);
|
|
496
749
|
const universalRouter = import_core9.UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
|
|
497
750
|
if (!universalRouter) {
|
|
@@ -579,6 +832,198 @@ var TradingHandlers = class {
|
|
|
579
832
|
};
|
|
580
833
|
}
|
|
581
834
|
// =========================================================================
|
|
835
|
+
// GET /quote/exact-out — V4 exact-output quote
|
|
836
|
+
// =========================================================================
|
|
837
|
+
/**
|
|
838
|
+
* Quote the input required to receive `request.amount` of the output
|
|
839
|
+
* token via Uniswap V4. Input-side operator fee is auto-quoted, so
|
|
840
|
+
* `inputGross = estimatedInputAmount + feeAmountInput` is the total
|
|
841
|
+
* the user must hold.
|
|
842
|
+
*
|
|
843
|
+
* Returns `quoteError: "QUOTE_UNAVAILABLE"` rather than throwing when
|
|
844
|
+
* no path exists.
|
|
845
|
+
*/
|
|
846
|
+
async handleQuoteExactOut(request) {
|
|
847
|
+
if (request.chainId !== this.chainId) {
|
|
848
|
+
throw new import_core9.ValidationError(
|
|
849
|
+
"UNSUPPORTED_CHAIN_ID",
|
|
850
|
+
`handleQuoteExactOut: unsupported chainId ${request.chainId}`,
|
|
851
|
+
{ requested: request.chainId, supported: this.chainId }
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
if (request.amount === 0n) {
|
|
855
|
+
return {
|
|
856
|
+
outputAmount: 0n,
|
|
857
|
+
estimatedInputAmount: 0n,
|
|
858
|
+
inputGross: 0n,
|
|
859
|
+
feeAmountInput: 0n,
|
|
860
|
+
gasEstimate: 0n
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
const inputTokenAddress = (0, import_viem4.getAddress)(request.inputTokenAddress);
|
|
864
|
+
const outputTokenAddress = (0, import_viem4.getAddress)(request.outputTokenAddress);
|
|
865
|
+
const pools = request.pools ?? [];
|
|
866
|
+
try {
|
|
867
|
+
const best = await findBestQuoteExactOut(
|
|
868
|
+
this.provider,
|
|
869
|
+
request.chainId,
|
|
870
|
+
inputTokenAddress,
|
|
871
|
+
outputTokenAddress,
|
|
872
|
+
request.amount,
|
|
873
|
+
pools
|
|
874
|
+
);
|
|
875
|
+
const feeAmountInput = await quoteOperatorFeeInput(
|
|
876
|
+
this.provider,
|
|
877
|
+
request.chainId,
|
|
878
|
+
inputTokenAddress
|
|
879
|
+
).catch(() => 0n);
|
|
880
|
+
const estimatedInputAmount = best.bestRoute.amountIn;
|
|
881
|
+
return {
|
|
882
|
+
outputAmount: request.amount,
|
|
883
|
+
estimatedInputAmount,
|
|
884
|
+
inputGross: estimatedInputAmount + feeAmountInput,
|
|
885
|
+
feeAmountInput,
|
|
886
|
+
gasEstimate: best.bestRoute.gasEstimate
|
|
887
|
+
};
|
|
888
|
+
} catch {
|
|
889
|
+
return {
|
|
890
|
+
outputAmount: request.amount,
|
|
891
|
+
estimatedInputAmount: 0n,
|
|
892
|
+
inputGross: 0n,
|
|
893
|
+
feeAmountInput: 0n,
|
|
894
|
+
gasEstimate: 0n,
|
|
895
|
+
quoteError: "QUOTE_UNAVAILABLE"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
// =========================================================================
|
|
900
|
+
// POST /swap/exact-out — V4 exact-output swap UserOp
|
|
901
|
+
// =========================================================================
|
|
902
|
+
/**
|
|
903
|
+
* Build a V4 exact-output swap UserOp.
|
|
904
|
+
*
|
|
905
|
+
* Quotes the best exact-output route, applies slippage as a CEILING on
|
|
906
|
+
* `maxAmountIn` (so the cap is never silently tightened by floor
|
|
907
|
+
* division), then encodes a 4-step batch:
|
|
908
|
+
*
|
|
909
|
+
* inputToken.transfer(feeRecipient, gasFeeAmountInput) [if > 0]
|
|
910
|
+
* → input.approve(Permit2, maxAmountIn)
|
|
911
|
+
* → Permit2.approve(router, maxAmountIn)
|
|
912
|
+
* → UniversalRouter.execute (V4 SWAP_EXACT_OUT)
|
|
913
|
+
*
|
|
914
|
+
* Operator fee is INPUT-token-side (charged before swap) so the user
|
|
915
|
+
* receives exactly `request.amount` of output.
|
|
916
|
+
*
|
|
917
|
+
* `authenticatedAddress` first param: caller MUST pass the address
|
|
918
|
+
* extracted from the verified session/JWT. Handler asserts equality
|
|
919
|
+
* with `request.userAddress`.
|
|
920
|
+
*/
|
|
921
|
+
async handleSwapExactOut(authenticatedAddress, request) {
|
|
922
|
+
if ((0, import_viem4.getAddress)(authenticatedAddress) !== (0, import_viem4.getAddress)(request.userAddress)) {
|
|
923
|
+
throw new import_core9.ValidationError(
|
|
924
|
+
"USER_ADDRESS_MISMATCH",
|
|
925
|
+
`handleSwapExactOut: authenticatedAddress (${authenticatedAddress}) does not match request.userAddress (${request.userAddress})`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
if (request.chainId !== this.chainId) {
|
|
929
|
+
throw new import_core9.ValidationError(
|
|
930
|
+
"UNSUPPORTED_CHAIN_ID",
|
|
931
|
+
`handleSwapExactOut: unsupported chainId ${request.chainId}`,
|
|
932
|
+
{ requested: request.chainId, supported: this.chainId }
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
if (request.amount <= 0n) {
|
|
936
|
+
throw new import_core9.ValidationError(
|
|
937
|
+
"INVALID_AMOUNT",
|
|
938
|
+
"handleSwapExactOut: amount must be positive"
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
if (!isValidSlippageBps(request.slippageBps)) {
|
|
942
|
+
throw new import_core9.ValidationError(
|
|
943
|
+
"INVALID_SLIPPAGE",
|
|
944
|
+
`handleSwapExactOut: slippageBps (${request.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`,
|
|
945
|
+
{ received: request.slippageBps, max: MAX_SLIPPAGE_BPS }
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
const { pafiFeeRecipient } = (0, import_core9.getContractAddresses)(request.chainId);
|
|
949
|
+
const universalRouter = import_core9.UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
|
|
950
|
+
if (!universalRouter) {
|
|
951
|
+
throw new import_core9.ValidationError(
|
|
952
|
+
"ROUTER_NOT_DEPLOYED",
|
|
953
|
+
`handleSwapExactOut: no UniversalRouter for chainId ${request.chainId}`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const inputTokenAddress = (0, import_viem4.getAddress)(request.inputTokenAddress);
|
|
957
|
+
const outputTokenAddress = (0, import_viem4.getAddress)(request.outputTokenAddress);
|
|
958
|
+
const userAddress = (0, import_viem4.getAddress)(request.userAddress);
|
|
959
|
+
const pools = request.pools ?? [];
|
|
960
|
+
let quoteResult;
|
|
961
|
+
try {
|
|
962
|
+
quoteResult = await findBestQuoteExactOut(
|
|
963
|
+
this.provider,
|
|
964
|
+
request.chainId,
|
|
965
|
+
inputTokenAddress,
|
|
966
|
+
outputTokenAddress,
|
|
967
|
+
request.amount,
|
|
968
|
+
pools
|
|
969
|
+
);
|
|
970
|
+
} catch (err) {
|
|
971
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
972
|
+
throw new import_core9.ValidationError(
|
|
973
|
+
"NO_SWAP_PATH",
|
|
974
|
+
`handleSwapExactOut: no swap path found from ${inputTokenAddress} to ${outputTokenAddress} (cause: ${cause})`
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const gasFeeAmountInput = request.gasFeeAmountInput !== void 0 ? request.gasFeeAmountInput : await quoteOperatorFeeInput(
|
|
978
|
+
this.provider,
|
|
979
|
+
request.chainId,
|
|
980
|
+
inputTokenAddress
|
|
981
|
+
).catch(() => 0n);
|
|
982
|
+
const hops = quoteResult.bestRoute.path.length;
|
|
983
|
+
const slippageBps = request.slippageBps ?? (hops > 1 ? 100 : 50);
|
|
984
|
+
const estimatedInputAmount = quoteResult.bestRoute.amountIn;
|
|
985
|
+
const slippageNumerator = estimatedInputAmount * BigInt(1e4 + slippageBps);
|
|
986
|
+
const maxAmountIn = (slippageNumerator + 9999n) / 10000n;
|
|
987
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
|
|
988
|
+
const userOp = buildSwapUserOpExactOut({
|
|
989
|
+
userAddress,
|
|
990
|
+
aaNonce: request.aaNonce,
|
|
991
|
+
inputTokenAddress,
|
|
992
|
+
outputTokenAddress,
|
|
993
|
+
universalRouterAddress: universalRouter,
|
|
994
|
+
amountOut: request.amount,
|
|
995
|
+
maxAmountIn,
|
|
996
|
+
swapPath: quoteResult.bestRoute.path,
|
|
997
|
+
deadline,
|
|
998
|
+
gasFeeAmountInput,
|
|
999
|
+
feeRecipient: pafiFeeRecipient
|
|
1000
|
+
});
|
|
1001
|
+
const userOpFallback = gasFeeAmountInput > 0n ? buildSwapUserOpExactOut({
|
|
1002
|
+
userAddress,
|
|
1003
|
+
aaNonce: request.aaNonce,
|
|
1004
|
+
inputTokenAddress,
|
|
1005
|
+
outputTokenAddress,
|
|
1006
|
+
universalRouterAddress: universalRouter,
|
|
1007
|
+
amountOut: request.amount,
|
|
1008
|
+
maxAmountIn,
|
|
1009
|
+
swapPath: quoteResult.bestRoute.path,
|
|
1010
|
+
deadline,
|
|
1011
|
+
gasFeeAmountInput: 0n,
|
|
1012
|
+
feeRecipient: pafiFeeRecipient
|
|
1013
|
+
}) : void 0;
|
|
1014
|
+
return {
|
|
1015
|
+
userOp,
|
|
1016
|
+
userOpFallback,
|
|
1017
|
+
outputAmount: request.amount,
|
|
1018
|
+
estimatedInputAmount,
|
|
1019
|
+
maxAmountIn,
|
|
1020
|
+
hops,
|
|
1021
|
+
deadline,
|
|
1022
|
+
feeAmountUsed: gasFeeAmountInput,
|
|
1023
|
+
feeRecipient: pafiFeeRecipient
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
// =========================================================================
|
|
582
1027
|
// POST /perp-deposit
|
|
583
1028
|
// =========================================================================
|
|
584
1029
|
/**
|
|
@@ -785,27 +1230,407 @@ async function quoteOperatorFeeOutput(provider, chainId, outputTokenAddress) {
|
|
|
785
1230
|
pointTokenAddress: outputTokenAddress
|
|
786
1231
|
});
|
|
787
1232
|
}
|
|
1233
|
+
async function quoteOperatorFeeInput(provider, chainId, inputTokenAddress) {
|
|
1234
|
+
const { usdt } = (0, import_core9.getContractAddresses)(chainId);
|
|
1235
|
+
if (usdt && (0, import_viem4.getAddress)(inputTokenAddress) === (0, import_viem4.getAddress)(usdt)) {
|
|
1236
|
+
return (0, import_core9.quoteOperatorFeeUsdt)({ provider, chainId });
|
|
1237
|
+
}
|
|
1238
|
+
return (0, import_core9.quoteOperatorFeePt)({
|
|
1239
|
+
provider,
|
|
1240
|
+
chainId,
|
|
1241
|
+
pointTokenAddress: inputTokenAddress
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
788
1244
|
|
|
789
1245
|
// src/pools.ts
|
|
790
1246
|
var import_core10 = require("@pafi-dev/core");
|
|
1247
|
+
|
|
1248
|
+
// src/direct/swapDirect.ts
|
|
1249
|
+
var import_core11 = require("@pafi-dev/core");
|
|
1250
|
+
async function swapDirect(params) {
|
|
1251
|
+
const universalRouter = import_core11.UNIVERSAL_ROUTER_ADDRESSES[params.chainId];
|
|
1252
|
+
if (!universalRouter) {
|
|
1253
|
+
throw new Error(`swapDirect: no UniversalRouter for chainId ${params.chainId}`);
|
|
1254
|
+
}
|
|
1255
|
+
if (params.amount <= 0n) {
|
|
1256
|
+
throw new Error("swapDirect: amount must be positive");
|
|
1257
|
+
}
|
|
1258
|
+
if (!isValidSlippageBps(params.slippageBps)) {
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
`swapDirect: slippageBps (${params.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
const account = params.walletClient.account;
|
|
1264
|
+
if (!account) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
"swapDirect: walletClient has no account attached \u2014 cannot send tx"
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
if (account.address.toLowerCase() !== params.userAddress.toLowerCase()) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
`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`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
const code = await params.publicClient.getCode({
|
|
1275
|
+
address: params.userAddress
|
|
1276
|
+
});
|
|
1277
|
+
const delegate = (0, import_core11.parseEip7702DelegatedAddress)(code);
|
|
1278
|
+
if (!delegate) {
|
|
1279
|
+
throw new Error(
|
|
1280
|
+
`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.`
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
const impl = (0, import_core11.detectDelegateImpl)(delegate);
|
|
1284
|
+
if (impl === "unknown") {
|
|
1285
|
+
params.onWarning?.(
|
|
1286
|
+
`swapDirect: user delegated to ${delegate} which is not a PAFI-recognised impl (expected ${import_core11.SIMPLE_7702_IMPL_BASE_MAINNET} or ${import_core11.BATCH_EXECUTOR_7702_IMPL}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
let quoteResult;
|
|
1290
|
+
try {
|
|
1291
|
+
quoteResult = await findBestQuote(
|
|
1292
|
+
params.publicClient,
|
|
1293
|
+
params.chainId,
|
|
1294
|
+
params.inputTokenAddress,
|
|
1295
|
+
params.outputTokenAddress,
|
|
1296
|
+
params.amount,
|
|
1297
|
+
params.pools ?? []
|
|
1298
|
+
);
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
1301
|
+
throw new Error(
|
|
1302
|
+
`swapDirect: no swap path found from ${params.inputTokenAddress} to ${params.outputTokenAddress} (cause: ${cause})`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
const hops = quoteResult.bestRoute.path.length;
|
|
1306
|
+
const slippageBps = params.slippageBps ?? (hops > 1 ? 100 : 50);
|
|
1307
|
+
const estimatedOutputAmount = quoteResult.bestRoute.amountOut;
|
|
1308
|
+
const minAmountOut = estimatedOutputAmount * BigInt(1e4 - slippageBps) / 10000n;
|
|
1309
|
+
const gasFeeAmountOutput = params.gasFeeAmountOutput ?? 0n;
|
|
1310
|
+
if (gasFeeAmountOutput > 0n && minAmountOut < gasFeeAmountOutput) {
|
|
1311
|
+
throw new Error(
|
|
1312
|
+
`swapDirect: minAmountOut (${minAmountOut}) below operator fee (${gasFeeAmountOutput})`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
const deadline = params.deadline ?? BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
|
|
1316
|
+
const { pafiFeeRecipient } = (0, import_core11.getContractAddresses)(params.chainId);
|
|
1317
|
+
const userOp = buildSwapUserOp({
|
|
1318
|
+
userAddress: params.userAddress,
|
|
1319
|
+
aaNonce: 0n,
|
|
1320
|
+
// ignored on the native-tx path; nonce comes from EOA tx count
|
|
1321
|
+
inputTokenAddress: params.inputTokenAddress,
|
|
1322
|
+
outputTokenAddress: params.outputTokenAddress,
|
|
1323
|
+
universalRouterAddress: universalRouter,
|
|
1324
|
+
amountIn: params.amount,
|
|
1325
|
+
minAmountOut,
|
|
1326
|
+
swapPath: quoteResult.bestRoute.path,
|
|
1327
|
+
deadline,
|
|
1328
|
+
gasFeeAmountOutput,
|
|
1329
|
+
feeRecipient: pafiFeeRecipient
|
|
1330
|
+
});
|
|
1331
|
+
const txHash = await params.walletClient.sendTransaction({
|
|
1332
|
+
account,
|
|
1333
|
+
chain: params.walletClient.chain,
|
|
1334
|
+
to: params.userAddress,
|
|
1335
|
+
value: 0n,
|
|
1336
|
+
data: userOp.callData
|
|
1337
|
+
});
|
|
1338
|
+
let receipt;
|
|
1339
|
+
if (params.waitForReceipt !== false) {
|
|
1340
|
+
try {
|
|
1341
|
+
receipt = await params.publicClient.waitForTransactionReceipt({
|
|
1342
|
+
hash: txHash
|
|
1343
|
+
});
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
params.onWarning?.(
|
|
1346
|
+
`swapDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return {
|
|
1351
|
+
txHash,
|
|
1352
|
+
receipt,
|
|
1353
|
+
estimatedOutputAmount,
|
|
1354
|
+
minAmountOut,
|
|
1355
|
+
hops,
|
|
1356
|
+
deadline,
|
|
1357
|
+
feeAmountUsed: gasFeeAmountOutput
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/direct/swapDirectExactOut.ts
|
|
1362
|
+
var import_core12 = require("@pafi-dev/core");
|
|
1363
|
+
async function swapDirectExactOut(params) {
|
|
1364
|
+
const universalRouter = import_core12.UNIVERSAL_ROUTER_ADDRESSES[params.chainId];
|
|
1365
|
+
if (!universalRouter) {
|
|
1366
|
+
throw new Error(
|
|
1367
|
+
`swapDirectExactOut: no UniversalRouter for chainId ${params.chainId}`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
if (params.amount <= 0n) {
|
|
1371
|
+
throw new Error("swapDirectExactOut: amount must be positive");
|
|
1372
|
+
}
|
|
1373
|
+
if (!isValidSlippageBps(params.slippageBps)) {
|
|
1374
|
+
throw new Error(
|
|
1375
|
+
`swapDirectExactOut: slippageBps (${params.slippageBps}) must be an integer in [0, ${MAX_SLIPPAGE_BPS}]`
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
const account = params.walletClient.account;
|
|
1379
|
+
if (!account) {
|
|
1380
|
+
throw new Error(
|
|
1381
|
+
"swapDirectExactOut: walletClient has no account attached \u2014 cannot send tx"
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
if (account.address.toLowerCase() !== params.userAddress.toLowerCase()) {
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
`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`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const code = await params.publicClient.getCode({
|
|
1390
|
+
address: params.userAddress
|
|
1391
|
+
});
|
|
1392
|
+
const delegate = (0, import_core12.parseEip7702DelegatedAddress)(code);
|
|
1393
|
+
if (!delegate) {
|
|
1394
|
+
throw new Error(
|
|
1395
|
+
`swapDirectExactOut: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first or use the AA path via \`TradingHandlers.handleSwapExactOut()\` + sponsor-relayer.`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
const impl = (0, import_core12.detectDelegateImpl)(delegate);
|
|
1399
|
+
if (impl === "unknown") {
|
|
1400
|
+
params.onWarning?.(
|
|
1401
|
+
`swapDirectExactOut: user delegated to ${delegate} which is not a PAFI-recognised impl (expected ${import_core12.SIMPLE_7702_IMPL_BASE_MAINNET} or ${import_core12.BATCH_EXECUTOR_7702_IMPL}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
let quoteResult;
|
|
1405
|
+
try {
|
|
1406
|
+
quoteResult = await findBestQuoteExactOut(
|
|
1407
|
+
params.publicClient,
|
|
1408
|
+
params.chainId,
|
|
1409
|
+
params.inputTokenAddress,
|
|
1410
|
+
params.outputTokenAddress,
|
|
1411
|
+
params.amount,
|
|
1412
|
+
params.pools ?? []
|
|
1413
|
+
);
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
1416
|
+
throw new Error(
|
|
1417
|
+
`swapDirectExactOut: no swap path found from ${params.inputTokenAddress} to ${params.outputTokenAddress} (cause: ${cause})`
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
const hops = quoteResult.bestRoute.path.length;
|
|
1421
|
+
const slippageBps = params.slippageBps ?? (hops > 1 ? 100 : 50);
|
|
1422
|
+
const estimatedInputAmount = quoteResult.bestRoute.amountIn;
|
|
1423
|
+
const slippageNumerator = estimatedInputAmount * BigInt(1e4 + slippageBps);
|
|
1424
|
+
const maxAmountIn = (slippageNumerator + 9999n) / 10000n;
|
|
1425
|
+
const gasFeeAmountInput = params.gasFeeAmountInput ?? 0n;
|
|
1426
|
+
const deadline = params.deadline ?? BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
|
|
1427
|
+
const { pafiFeeRecipient } = (0, import_core12.getContractAddresses)(params.chainId);
|
|
1428
|
+
const userOp = buildSwapUserOpExactOut({
|
|
1429
|
+
userAddress: params.userAddress,
|
|
1430
|
+
aaNonce: 0n,
|
|
1431
|
+
// ignored on the native-tx path; nonce comes from EOA tx count
|
|
1432
|
+
inputTokenAddress: params.inputTokenAddress,
|
|
1433
|
+
outputTokenAddress: params.outputTokenAddress,
|
|
1434
|
+
universalRouterAddress: universalRouter,
|
|
1435
|
+
amountOut: params.amount,
|
|
1436
|
+
maxAmountIn,
|
|
1437
|
+
swapPath: quoteResult.bestRoute.path,
|
|
1438
|
+
deadline,
|
|
1439
|
+
gasFeeAmountInput,
|
|
1440
|
+
feeRecipient: pafiFeeRecipient
|
|
1441
|
+
});
|
|
1442
|
+
const txHash = await params.walletClient.sendTransaction({
|
|
1443
|
+
account,
|
|
1444
|
+
chain: params.walletClient.chain,
|
|
1445
|
+
to: params.userAddress,
|
|
1446
|
+
value: 0n,
|
|
1447
|
+
data: userOp.callData
|
|
1448
|
+
});
|
|
1449
|
+
let receipt;
|
|
1450
|
+
if (params.waitForReceipt !== false) {
|
|
1451
|
+
try {
|
|
1452
|
+
receipt = await params.publicClient.waitForTransactionReceipt({
|
|
1453
|
+
hash: txHash
|
|
1454
|
+
});
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
params.onWarning?.(
|
|
1457
|
+
`swapDirectExactOut: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
txHash,
|
|
1463
|
+
receipt,
|
|
1464
|
+
outputAmount: params.amount,
|
|
1465
|
+
estimatedInputAmount,
|
|
1466
|
+
maxAmountIn,
|
|
1467
|
+
hops,
|
|
1468
|
+
deadline,
|
|
1469
|
+
feeAmountUsed: gasFeeAmountInput
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/direct/perpDepositDirect.ts
|
|
1474
|
+
var import_core13 = require("@pafi-dev/core");
|
|
1475
|
+
async function perpDepositDirect(params) {
|
|
1476
|
+
if (params.amount <= 0n) {
|
|
1477
|
+
throw new Error("perpDepositDirect: amount must be positive");
|
|
1478
|
+
}
|
|
1479
|
+
const code = await params.publicClient.getCode({
|
|
1480
|
+
address: params.userAddress
|
|
1481
|
+
});
|
|
1482
|
+
const delegate = (0, import_core13.parseEip7702DelegatedAddress)(code);
|
|
1483
|
+
if (!delegate) {
|
|
1484
|
+
throw new Error(
|
|
1485
|
+
`perpDepositDirect: user ${params.userAddress} is not EIP-7702 delegated. Run \`delegateDirect()\` first or use the AA path via \`TradingHandlers.handlePerpDeposit()\` + sponsor-relayer.`
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
const impl = (0, import_core13.detectDelegateImpl)(delegate);
|
|
1489
|
+
if (impl === "unknown") {
|
|
1490
|
+
params.onWarning?.(
|
|
1491
|
+
`perpDepositDirect: user delegated to ${delegate} (not a PAFI-recognised impl ${import_core13.SIMPLE_7702_IMPL_BASE_MAINNET} / ${import_core13.BATCH_EXECUTOR_7702_IMPL}). Continuing \u2014 execute will revert if the impl doesn't expose executeBatch.`
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
const vault = import_core13.ORDERLY_VAULT_ADDRESSES[params.chainId];
|
|
1495
|
+
if (!vault) {
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`perpDepositDirect: no Orderly Vault for chainId ${params.chainId}`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
const brokerHash = import_core13.BROKER_HASHES[params.brokerId];
|
|
1501
|
+
if (!brokerHash) {
|
|
1502
|
+
throw new Error(
|
|
1503
|
+
`perpDepositDirect: unknown brokerId "${params.brokerId}"`
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
const tokenHash = import_core13.TOKEN_HASHES.USDC;
|
|
1507
|
+
const [usdcAddress, brokerAllowed] = await Promise.all([
|
|
1508
|
+
params.publicClient.readContract({
|
|
1509
|
+
address: vault,
|
|
1510
|
+
abi: import_core13.ORDERLY_VAULT_ABI,
|
|
1511
|
+
functionName: "getAllowedToken",
|
|
1512
|
+
args: [tokenHash]
|
|
1513
|
+
}),
|
|
1514
|
+
params.publicClient.readContract({
|
|
1515
|
+
address: vault,
|
|
1516
|
+
abi: import_core13.ORDERLY_VAULT_ABI,
|
|
1517
|
+
functionName: "getAllowedBroker",
|
|
1518
|
+
args: [brokerHash]
|
|
1519
|
+
})
|
|
1520
|
+
]);
|
|
1521
|
+
if (!brokerAllowed) {
|
|
1522
|
+
throw new Error(
|
|
1523
|
+
`perpDepositDirect: broker "${params.brokerId}" is not whitelisted on Orderly Vault`
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
const { orderlyRelay: relayAddress, pafiFeeRecipient } = (0, import_core13.getContractAddresses)(
|
|
1527
|
+
params.chainId
|
|
1528
|
+
);
|
|
1529
|
+
const RELAY_FEE_FLOOR_USDC = 2000000n;
|
|
1530
|
+
const percentCap = params.amount * 500n / 10000n;
|
|
1531
|
+
const maxFee = params.maxRelayFee ?? (percentCap > RELAY_FEE_FLOOR_USDC ? percentCap : RELAY_FEE_FLOOR_USDC);
|
|
1532
|
+
const relayRequest = {
|
|
1533
|
+
token: usdcAddress,
|
|
1534
|
+
receiver: params.userAddress,
|
|
1535
|
+
brokerHash,
|
|
1536
|
+
totalAmount: params.amount,
|
|
1537
|
+
maxFee
|
|
1538
|
+
};
|
|
1539
|
+
const relayTokenFee = await params.publicClient.readContract({
|
|
1540
|
+
address: relayAddress,
|
|
1541
|
+
abi: import_core13.ORDERLY_RELAY_ABI,
|
|
1542
|
+
functionName: "quoteTokenFee",
|
|
1543
|
+
args: [relayRequest]
|
|
1544
|
+
});
|
|
1545
|
+
if (relayTokenFee > maxFee) {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
`perpDepositDirect: Relay tokenFee ${relayTokenFee} exceeds maxFee ${maxFee} \u2014 pass a larger \`maxRelayFee\` or increase \`amount\`.`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
if (relayTokenFee >= params.amount) {
|
|
1551
|
+
throw new Error(
|
|
1552
|
+
`perpDepositDirect: deposit amount ${params.amount} below Relay fee ${relayTokenFee} \u2014 increase \`amount\`.`
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
const gasFeeUsdc = params.gasFeeUsdc ?? 0n;
|
|
1556
|
+
const partial = (0, import_core13.buildPerpDepositViaRelay)({
|
|
1557
|
+
userAddress: params.userAddress,
|
|
1558
|
+
aaNonce: 0n,
|
|
1559
|
+
// ignored on the native-tx path
|
|
1560
|
+
relayAddress,
|
|
1561
|
+
request: relayRequest,
|
|
1562
|
+
gasFeeUsdc: gasFeeUsdc > 0n ? gasFeeUsdc : void 0,
|
|
1563
|
+
gasFeeUsdcRecipient: gasFeeUsdc > 0n ? pafiFeeRecipient : void 0
|
|
1564
|
+
});
|
|
1565
|
+
const account = params.walletClient.account;
|
|
1566
|
+
if (!account) {
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
"perpDepositDirect: walletClient has no account attached \u2014 cannot send tx"
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
const txHash = await params.walletClient.sendTransaction({
|
|
1572
|
+
account,
|
|
1573
|
+
chain: params.walletClient.chain,
|
|
1574
|
+
to: params.userAddress,
|
|
1575
|
+
value: 0n,
|
|
1576
|
+
data: partial.callData
|
|
1577
|
+
});
|
|
1578
|
+
let receipt;
|
|
1579
|
+
if (params.waitForReceipt !== false) {
|
|
1580
|
+
try {
|
|
1581
|
+
receipt = await params.publicClient.waitForTransactionReceipt({
|
|
1582
|
+
hash: txHash
|
|
1583
|
+
});
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
params.onWarning?.(
|
|
1586
|
+
`perpDepositDirect: tx ${txHash} sent but receipt fetch failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
const accountId = (0, import_core13.computeAccountId)(params.userAddress, brokerHash);
|
|
1591
|
+
return {
|
|
1592
|
+
txHash,
|
|
1593
|
+
receipt,
|
|
1594
|
+
relayTokenFee,
|
|
1595
|
+
maxFee,
|
|
1596
|
+
netDeposit: params.amount - relayTokenFee,
|
|
1597
|
+
feeAmountUsed: gasFeeUsdc,
|
|
1598
|
+
accountId,
|
|
1599
|
+
brokerHash,
|
|
1600
|
+
usdcAddress,
|
|
1601
|
+
relayAddress
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
791
1604
|
// Annotate the CommonJS export names for ESM import in node:
|
|
792
1605
|
0 && (module.exports = {
|
|
793
1606
|
PAFI_SUBGRAPH_URL,
|
|
1607
|
+
SWAP_EXACT_OUT,
|
|
1608
|
+
SWAP_EXACT_OUT_SINGLE,
|
|
794
1609
|
TradingHandlers,
|
|
795
1610
|
buildAllPaths,
|
|
796
1611
|
buildErc20ApprovalCalldata,
|
|
797
1612
|
buildPermit2ApprovalCalldata,
|
|
798
1613
|
buildSwapFromQuote,
|
|
799
1614
|
buildSwapUserOp,
|
|
1615
|
+
buildSwapUserOpExactOut,
|
|
800
1616
|
buildUniversalRouterExecuteArgs,
|
|
1617
|
+
buildUniversalRouterExecuteArgsExactOut,
|
|
801
1618
|
buildV4SwapInput,
|
|
1619
|
+
buildV4SwapInputExactOut,
|
|
802
1620
|
checkAllowance,
|
|
803
1621
|
combineRoutes,
|
|
804
1622
|
fetchPafiPools,
|
|
805
1623
|
findBestQuote,
|
|
1624
|
+
findBestQuoteExactOut,
|
|
1625
|
+
perpDepositDirect,
|
|
806
1626
|
quoteBestRoute,
|
|
1627
|
+
quoteBestRouteExactOut,
|
|
807
1628
|
quoteExactInput,
|
|
808
1629
|
quoteExactInputSingle,
|
|
809
|
-
|
|
1630
|
+
quoteExactOutput,
|
|
1631
|
+
quoteExactOutputSingle,
|
|
1632
|
+
simulateSwap,
|
|
1633
|
+
swapDirect,
|
|
1634
|
+
swapDirectExactOut
|
|
810
1635
|
});
|
|
811
1636
|
//# sourceMappingURL=index.cjs.map
|