@pafi-dev/trading 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  // src/api/handlers.ts
2
2
  import { getAddress } from "viem";
3
3
  import {
4
- findBestQuote,
5
- buildSwapWithGasDeduction,
6
4
  buildPerpDepositWithGasDeduction,
7
5
  buildPerpDepositViaRelay,
8
6
  ORDERLY_RELAY_ABI,
@@ -15,6 +13,329 @@ import {
15
13
  computeAccountId,
16
14
  quoteOperatorFeePt
17
15
  } from "@pafi-dev/core";
16
+
17
+ // src/quoting/routes.ts
18
+ import { COMMON_POOLS, POINT_TOKEN_POOLS } from "@pafi-dev/core";
19
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
20
+ function combineRoutes(chainId, pointTokenAddress) {
21
+ const commonPools = COMMON_POOLS[chainId] ?? [];
22
+ const pointPools = POINT_TOKEN_POOLS[chainId]?.[pointTokenAddress] ?? [];
23
+ return [...pointPools, ...commonPools];
24
+ }
25
+ function buildAllPaths(pools, tokenIn, tokenOut, maxHops = 3) {
26
+ const results = [];
27
+ function dfs(currentToken, currentPath, usedPoolIndices) {
28
+ if (currentPath.length > maxHops) return;
29
+ if (currentPath.length > 0 && currentToken.toLowerCase() === tokenOut.toLowerCase()) {
30
+ results.push([...currentPath]);
31
+ return;
32
+ }
33
+ for (let i = 0; i < pools.length; i++) {
34
+ if (usedPoolIndices.has(i)) continue;
35
+ const pool = pools[i];
36
+ const c0 = pool.currency0.toLowerCase();
37
+ const c1 = pool.currency1.toLowerCase();
38
+ const curr = currentToken.toLowerCase();
39
+ let nextToken = null;
40
+ if (curr === c0) {
41
+ nextToken = pool.currency1;
42
+ } else if (curr === c1) {
43
+ nextToken = pool.currency0;
44
+ }
45
+ if (!nextToken) continue;
46
+ const hop = {
47
+ intermediateCurrency: nextToken,
48
+ fee: pool.fee,
49
+ tickSpacing: pool.tickSpacing,
50
+ hooks: pool.hooks ?? ZERO_ADDRESS,
51
+ hookData: "0x"
52
+ };
53
+ usedPoolIndices.add(i);
54
+ currentPath.push(hop);
55
+ dfs(nextToken, currentPath, usedPoolIndices);
56
+ currentPath.pop();
57
+ usedPoolIndices.delete(i);
58
+ }
59
+ }
60
+ dfs(tokenIn, [], /* @__PURE__ */ new Set());
61
+ return results;
62
+ }
63
+
64
+ // src/quoting/quote.ts
65
+ import { v4QuoterAbi } from "@pafi-dev/core";
66
+ import { COMMON_POOLS as COMMON_POOLS2, V4_QUOTER_ADDRESSES } from "@pafi-dev/core";
67
+ async function quoteExactInput(client, quoterAddress, exactCurrency, path, exactAmount) {
68
+ const [amountOut, gasEstimate] = await client.readContract({
69
+ address: quoterAddress,
70
+ abi: v4QuoterAbi,
71
+ functionName: "quoteExactInput",
72
+ args: [{ exactCurrency, path, exactAmount: BigInt(exactAmount) }]
73
+ });
74
+ return { amountOut, gasEstimate, path };
75
+ }
76
+ async function quoteExactInputSingle(client, quoterAddress, poolKey, zeroForOne, exactAmount, hookData) {
77
+ const [amountOut, gasEstimate] = await client.readContract({
78
+ address: quoterAddress,
79
+ abi: v4QuoterAbi,
80
+ functionName: "quoteExactInputSingle",
81
+ args: [
82
+ {
83
+ poolKey,
84
+ zeroForOne,
85
+ exactAmount: BigInt(exactAmount),
86
+ hookData
87
+ }
88
+ ]
89
+ });
90
+ return { amountOut, gasEstimate };
91
+ }
92
+ async function quoteBestRoute(client, quoterAddress, exactCurrency, routes, exactAmount) {
93
+ const results = await client.multicall({
94
+ contracts: routes.map((path) => ({
95
+ address: quoterAddress,
96
+ abi: v4QuoterAbi,
97
+ functionName: "quoteExactInput",
98
+ args: [
99
+ {
100
+ exactCurrency,
101
+ path,
102
+ exactAmount: BigInt(exactAmount)
103
+ }
104
+ ]
105
+ })),
106
+ allowFailure: true
107
+ });
108
+ const allRoutes = [];
109
+ for (let i = 0; i < results.length; i++) {
110
+ const r = results[i];
111
+ if (r.status === "success") {
112
+ const [amountOut, gasEstimate] = r.result;
113
+ allRoutes.push({ amountOut, gasEstimate, path: routes[i] });
114
+ }
115
+ }
116
+ if (allRoutes.length === 0) {
117
+ throw new Error("No valid routes found");
118
+ }
119
+ const bestRoute = allRoutes.reduce(
120
+ (best, current) => current.amountOut > best.amountOut ? current : best
121
+ );
122
+ return { bestRoute, allRoutes };
123
+ }
124
+ async function findBestQuote(client, chainId, tokenIn, tokenOut, exactAmount, pools = [], quoterAddress, maxHops = 3) {
125
+ const quoter = quoterAddress ?? V4_QUOTER_ADDRESSES[chainId];
126
+ if (!quoter) {
127
+ throw new Error(`No V4 Quoter address configured for chain ${chainId}`);
128
+ }
129
+ const commonPools = COMMON_POOLS2[chainId] ?? [];
130
+ const allPools = [...pools, ...commonPools];
131
+ const paths = buildAllPaths(allPools, tokenIn, tokenOut, maxHops);
132
+ if (paths.length === 0) {
133
+ throw new Error(`No paths found from ${tokenIn} to ${tokenOut}`);
134
+ }
135
+ return quoteBestRoute(client, quoter, tokenIn, paths, exactAmount);
136
+ }
137
+
138
+ // src/swap/approval.ts
139
+ import { encodeFunctionData } from "viem";
140
+ import { erc20Abi } from "@pafi-dev/core";
141
+ import { permit2Abi } from "@pafi-dev/core";
142
+ async function checkAllowance(client, token, owner, spender) {
143
+ return client.readContract({
144
+ address: token,
145
+ abi: erc20Abi,
146
+ functionName: "allowance",
147
+ args: [owner, spender]
148
+ });
149
+ }
150
+ function buildErc20ApprovalCalldata(spender, amount) {
151
+ return encodeFunctionData({
152
+ abi: erc20Abi,
153
+ functionName: "approve",
154
+ args: [spender, amount]
155
+ });
156
+ }
157
+ function buildPermit2ApprovalCalldata(token, spender, amount, expiration) {
158
+ return encodeFunctionData({
159
+ abi: permit2Abi,
160
+ functionName: "approve",
161
+ args: [token, spender, amount, expiration]
162
+ });
163
+ }
164
+
165
+ // src/swap/universalRouter.ts
166
+ import { encodeAbiParameters, encodePacked } from "viem";
167
+ var V4_SWAP = 16;
168
+ var SWAP_EXACT_IN = 7;
169
+ var SETTLE_ALL = 12;
170
+ var TAKE_ALL = 15;
171
+ var PATH_KEY_ABI_COMPONENTS = [
172
+ { name: "intermediateCurrency", type: "address" },
173
+ { name: "fee", type: "uint256" },
174
+ { name: "tickSpacing", type: "int24" },
175
+ { name: "hooks", type: "address" },
176
+ { name: "hookData", type: "bytes" }
177
+ ];
178
+ var EXACT_INPUT_PARAMS_ABI = [
179
+ { name: "currencyIn", type: "address" },
180
+ {
181
+ name: "path",
182
+ type: "tuple[]",
183
+ components: PATH_KEY_ABI_COMPONENTS
184
+ },
185
+ { name: "amountIn", type: "uint128" },
186
+ { name: "amountOutMinimum", type: "uint128" }
187
+ ];
188
+ function buildV4SwapInput(currencyIn, path, amountIn, minAmountOut, outputCurrency) {
189
+ const actions = encodePacked(
190
+ ["uint8", "uint8", "uint8"],
191
+ [SWAP_EXACT_IN, SETTLE_ALL, TAKE_ALL]
192
+ );
193
+ const swapParam = encodeAbiParameters(
194
+ [{ name: "swap", type: "tuple", components: EXACT_INPUT_PARAMS_ABI }],
195
+ [
196
+ {
197
+ currencyIn,
198
+ path: path.map((p) => ({
199
+ intermediateCurrency: p.intermediateCurrency,
200
+ fee: BigInt(p.fee),
201
+ tickSpacing: p.tickSpacing,
202
+ hooks: p.hooks,
203
+ hookData: p.hookData
204
+ })),
205
+ amountIn,
206
+ amountOutMinimum: minAmountOut
207
+ }
208
+ ]
209
+ );
210
+ const settleParam = encodeAbiParameters(
211
+ [
212
+ { name: "currency", type: "address" },
213
+ { name: "maxAmount", type: "uint256" }
214
+ ],
215
+ [currencyIn, amountIn]
216
+ );
217
+ const takeParam = encodeAbiParameters(
218
+ [
219
+ { name: "currency", type: "address" },
220
+ { name: "minAmount", type: "uint256" }
221
+ ],
222
+ [outputCurrency, minAmountOut]
223
+ );
224
+ return encodeAbiParameters(
225
+ [
226
+ { name: "actions", type: "bytes" },
227
+ { name: "params", type: "bytes[]" }
228
+ ],
229
+ [actions, [swapParam, settleParam, takeParam]]
230
+ );
231
+ }
232
+ function buildUniversalRouterExecuteArgs(currencyIn, path, amountIn, minAmountOut, outputCurrency) {
233
+ const commands = encodePacked(["uint8"], [V4_SWAP]);
234
+ const inputs = [
235
+ buildV4SwapInput(currencyIn, path, amountIn, minAmountOut, outputCurrency)
236
+ ];
237
+ return { commands, inputs };
238
+ }
239
+ function buildSwapFromQuote(params) {
240
+ return buildUniversalRouterExecuteArgs(
241
+ params.currencyIn,
242
+ params.quote.path,
243
+ params.amountIn,
244
+ params.minAmountOut,
245
+ params.currencyOut
246
+ );
247
+ }
248
+
249
+ // src/swap/simulate.ts
250
+ import { universalRouterAbi } from "@pafi-dev/core";
251
+ import { SimulationError } from "@pafi-dev/core";
252
+ async function simulateSwap(client, routerAddress, commands, inputs, deadline, from) {
253
+ try {
254
+ const gasEstimate = await client.estimateContractGas({
255
+ address: routerAddress,
256
+ abi: universalRouterAbi,
257
+ functionName: "execute",
258
+ args: [commands, inputs, deadline],
259
+ account: from
260
+ });
261
+ return { success: true, gasEstimate };
262
+ } catch (error) {
263
+ const message = error instanceof Error ? error.message : "Unknown simulation error";
264
+ throw new SimulationError("swap", message);
265
+ }
266
+ }
267
+
268
+ // src/swap/buildSwap.ts
269
+ import { encodeFunctionData as encodeFunctionData2 } from "viem";
270
+ import {
271
+ PERMIT2_ADDRESS,
272
+ buildPartialUserOperation,
273
+ erc20ApproveOp,
274
+ erc20TransferOp,
275
+ rawCallOp,
276
+ universalRouterAbi as universalRouterAbi2
277
+ } from "@pafi-dev/core";
278
+ function buildSwapUserOp(params) {
279
+ if (params.amountIn <= 0n) {
280
+ throw new Error("buildSwapUserOp: amountIn must be positive");
281
+ }
282
+ if (params.minAmountOut < 0n) {
283
+ throw new Error("buildSwapUserOp: minAmountOut must be non-negative");
284
+ }
285
+ if (params.gasFeeAmount < 0n) {
286
+ throw new Error("buildSwapUserOp: gasFeeAmount must be non-negative");
287
+ }
288
+ if (params.swapPath.length === 0) {
289
+ throw new Error(
290
+ "buildSwapUserOp: swapPath must contain at least one PathKey"
291
+ );
292
+ }
293
+ const { commands, inputs } = buildUniversalRouterExecuteArgs(
294
+ params.inputTokenAddress,
295
+ params.swapPath,
296
+ params.amountIn,
297
+ params.minAmountOut,
298
+ params.outputTokenAddress
299
+ );
300
+ const swapCallData = encodeFunctionData2({
301
+ abi: universalRouterAbi2,
302
+ functionName: "execute",
303
+ args: [commands, inputs, params.deadline]
304
+ });
305
+ const totalInputApproval = params.amountIn + params.gasFeeAmount;
306
+ const permit2ApproveData = buildPermit2ApprovalCalldata(
307
+ params.inputTokenAddress,
308
+ params.universalRouterAddress,
309
+ params.amountIn,
310
+ Number(params.deadline)
311
+ );
312
+ const operations = [
313
+ erc20ApproveOp(params.inputTokenAddress, PERMIT2_ADDRESS, totalInputApproval),
314
+ rawCallOp(PERMIT2_ADDRESS, permit2ApproveData),
315
+ rawCallOp(params.universalRouterAddress, swapCallData)
316
+ ];
317
+ if (params.gasFeeAmount > 0n) {
318
+ operations.push(
319
+ erc20TransferOp(
320
+ params.inputTokenAddress,
321
+ params.feeRecipient,
322
+ params.gasFeeAmount
323
+ )
324
+ );
325
+ }
326
+ return buildPartialUserOperation({
327
+ sender: params.userAddress,
328
+ nonce: params.aaNonce,
329
+ operations,
330
+ gasLimits: {
331
+ callGasLimit: params.gasLimits?.callGasLimit ?? 700000n,
332
+ verificationGasLimit: params.gasLimits?.verificationGasLimit ?? 150000n,
333
+ preVerificationGas: params.gasLimits?.preVerificationGas ?? 50000n
334
+ }
335
+ });
336
+ }
337
+
338
+ // src/api/handlers.ts
18
339
  var TradingHandlers = class {
19
340
  provider;
20
341
  chainId;
@@ -38,29 +359,33 @@ var TradingHandlers = class {
38
359
  throw new Error(`handleQuote: unsupported chainId ${request.chainId}`);
39
360
  }
40
361
  if (request.amount === 0n) {
41
- return { pointAmount: 0n, estimatedUsdtOut: 0n, gasEstimate: 0n };
362
+ return {
363
+ inputAmount: 0n,
364
+ estimatedOutputAmount: 0n,
365
+ gasEstimate: 0n
366
+ };
42
367
  }
43
- const { usdt } = getContractAddresses(request.chainId);
44
- const pointTokenAddress = getAddress(request.pointTokenAddress);
368
+ const inputTokenAddress = getAddress(request.inputTokenAddress);
369
+ const outputTokenAddress = getAddress(request.outputTokenAddress);
45
370
  const pools = request.pools ?? [];
46
371
  try {
47
372
  const best = await findBestQuote(
48
373
  this.provider,
49
374
  request.chainId,
50
- pointTokenAddress,
51
- usdt,
375
+ inputTokenAddress,
376
+ outputTokenAddress,
52
377
  request.amount,
53
378
  pools
54
379
  );
55
380
  return {
56
- pointAmount: request.amount,
57
- estimatedUsdtOut: best.bestRoute.amountOut,
381
+ inputAmount: request.amount,
382
+ estimatedOutputAmount: best.bestRoute.amountOut,
58
383
  gasEstimate: best.bestRoute.gasEstimate
59
384
  };
60
385
  } catch {
61
386
  return {
62
- pointAmount: request.amount,
63
- estimatedUsdtOut: 0n,
387
+ inputAmount: request.amount,
388
+ estimatedOutputAmount: 0n,
64
389
  gasEstimate: 0n,
65
390
  quoteError: "QUOTE_UNAVAILABLE"
66
391
  };
@@ -85,71 +410,74 @@ var TradingHandlers = class {
85
410
  if (request.amount <= 0n) {
86
411
  throw new Error("handleSwap: amount must be positive");
87
412
  }
88
- const { usdt, pafiFeeRecipient } = getContractAddresses(request.chainId);
413
+ const { pafiFeeRecipient } = getContractAddresses(request.chainId);
89
414
  const universalRouter = UNIVERSAL_ROUTER_ADDRESSES[request.chainId];
90
415
  if (!universalRouter) {
91
416
  throw new Error(`handleSwap: no UniversalRouter for chainId ${request.chainId}`);
92
417
  }
93
- const pointTokenAddress = getAddress(request.pointTokenAddress);
418
+ const inputTokenAddress = getAddress(request.inputTokenAddress);
419
+ const outputTokenAddress = getAddress(request.outputTokenAddress);
94
420
  const userAddress = getAddress(request.userAddress);
95
421
  const pools = request.pools ?? [];
96
- const slippageBps = request.slippageBps ?? 50;
97
- const gasFeePt = request.gasFeePt !== void 0 ? request.gasFeePt : await quoteOperatorFeePt({
422
+ const gasFeeAmount = request.gasFeeAmount !== void 0 ? request.gasFeeAmount : await quoteOperatorFeePt({
98
423
  provider: this.provider,
99
424
  chainId: request.chainId,
100
- pointTokenAddress
101
- });
425
+ pointTokenAddress: inputTokenAddress
426
+ }).catch(() => 0n);
102
427
  let quoteResult;
103
428
  try {
104
429
  quoteResult = await findBestQuote(
105
430
  this.provider,
106
431
  request.chainId,
107
- pointTokenAddress,
108
- usdt,
432
+ inputTokenAddress,
433
+ outputTokenAddress,
109
434
  request.amount,
110
435
  pools
111
436
  );
112
437
  } catch {
113
- throw new Error("handleSwap: no swap path found for this point token");
438
+ throw new Error(
439
+ `handleSwap: no swap path found from ${inputTokenAddress} to ${outputTokenAddress}`
440
+ );
114
441
  }
115
- const estimatedUsdtOut = quoteResult.bestRoute.amountOut;
116
- const minAmountOut = estimatedUsdtOut * BigInt(1e4 - slippageBps) / 10000n;
442
+ const hops = quoteResult.bestRoute.path.length;
443
+ const slippageBps = request.slippageBps ?? (hops > 1 ? 100 : 50);
444
+ const estimatedOutputAmount = quoteResult.bestRoute.amountOut;
445
+ const minAmountOut = estimatedOutputAmount * BigInt(1e4 - slippageBps) / 10000n;
117
446
  const deadline = BigInt(Math.floor(Date.now() / 1e3) + 5 * 60);
118
- const userOp = buildSwapWithGasDeduction({
447
+ const userOp = buildSwapUserOp({
119
448
  userAddress,
120
449
  aaNonce: request.aaNonce,
121
- pointTokenAddress,
122
- outputTokenAddress: usdt,
450
+ inputTokenAddress,
451
+ outputTokenAddress,
123
452
  universalRouterAddress: universalRouter,
124
453
  amountIn: request.amount,
125
454
  minAmountOut,
126
455
  swapPath: quoteResult.bestRoute.path,
127
456
  deadline,
128
- gasFeePt,
129
- // Recipient is always PAFI's canonical address — sponsor-relayer's
130
- // L1 gate will reject any other recipient anyway. No override.
457
+ gasFeeAmount,
131
458
  feeRecipient: pafiFeeRecipient
132
459
  });
133
- const userOpFallback = gasFeePt > 0n ? buildSwapWithGasDeduction({
460
+ const userOpFallback = gasFeeAmount > 0n ? buildSwapUserOp({
134
461
  userAddress,
135
462
  aaNonce: request.aaNonce,
136
- pointTokenAddress,
137
- outputTokenAddress: usdt,
463
+ inputTokenAddress,
464
+ outputTokenAddress,
138
465
  universalRouterAddress: universalRouter,
139
466
  amountIn: request.amount,
140
467
  minAmountOut,
141
468
  swapPath: quoteResult.bestRoute.path,
142
469
  deadline,
143
- gasFeePt: 0n,
144
- feeRecipient: userAddress
470
+ gasFeeAmount: 0n,
471
+ feeRecipient: pafiFeeRecipient
145
472
  }) : void 0;
146
473
  return {
147
474
  userOp,
148
475
  userOpFallback,
149
- estimatedUsdtOut,
476
+ estimatedOutputAmount,
150
477
  minAmountOut,
478
+ hops,
151
479
  deadline,
152
- feeAmountUsed: gasFeePt,
480
+ feeAmountUsed: gasFeeAmount,
153
481
  feeRecipient: pafiFeeRecipient
154
482
  };
155
483
  }
@@ -330,6 +658,20 @@ import { fetchPafiPools, PAFI_SUBGRAPH_URL } from "@pafi-dev/core";
330
658
  export {
331
659
  PAFI_SUBGRAPH_URL,
332
660
  TradingHandlers,
333
- fetchPafiPools
661
+ buildAllPaths,
662
+ buildErc20ApprovalCalldata,
663
+ buildPermit2ApprovalCalldata,
664
+ buildSwapFromQuote,
665
+ buildSwapUserOp,
666
+ buildUniversalRouterExecuteArgs,
667
+ buildV4SwapInput,
668
+ checkAllowance,
669
+ combineRoutes,
670
+ fetchPafiPools,
671
+ findBestQuote,
672
+ quoteBestRoute,
673
+ quoteExactInput,
674
+ quoteExactInputSingle,
675
+ simulateSwap
334
676
  };
335
677
  //# sourceMappingURL=index.js.map