four-flap-meme-sdk 1.5.32 → 1.5.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/xlayer/bundle.d.ts +2 -0
- package/dist/xlayer/bundle.js +272 -22
- package/dist/xlayer/buy-first-volume.d.ts +18 -0
- package/dist/xlayer/buy-first-volume.js +68 -0
- package/dist/xlayer/dex-bundle-swap.js +150 -19
- package/dist/xlayer/dex-bundle.d.ts +42 -0
- package/dist/xlayer/dex-bundle.js +378 -0
- package/dist/xlayer/dex-buy-first.d.ts +24 -0
- package/dist/xlayer/dex-buy-first.js +404 -0
- package/dist/xlayer/dex-volume.d.ts +30 -0
- package/dist/xlayer/dex-volume.js +80 -0
- package/dist/xlayer/index.d.ts +5 -0
- package/dist/xlayer/index.js +14 -0
- package/dist/xlayer/portal-bundle-swap.js +153 -12
- package/dist/xlayer/portal-buy-first.d.ts +30 -0
- package/dist/xlayer/portal-buy-first.js +415 -0
- package/dist/xlayer/types.d.ts +109 -2
- package/package.json +1 -1
|
@@ -3,15 +3,37 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Wallet, ethers } from 'ethers';
|
|
5
5
|
import { AANonceMap, } from './types.js';
|
|
6
|
-
import { POTATOSWAP_V2_ROUTER, WOKB, } from './constants.js';
|
|
6
|
+
import { POTATOSWAP_V2_ROUTER, WOKB, MULTICALL3, } from './constants.js';
|
|
7
7
|
import { AAAccountManager, encodeExecute } from './aa-account.js';
|
|
8
8
|
import { encodeApproveCall, } from './portal-ops.js';
|
|
9
9
|
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, } from './dex.js';
|
|
10
10
|
import { BundleExecutor } from './bundle.js';
|
|
11
11
|
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
12
|
+
const multicallIface = new ethers.Interface([
|
|
13
|
+
'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
|
|
14
|
+
]);
|
|
15
|
+
function chunkArray(arr, size) {
|
|
16
|
+
const out = [];
|
|
17
|
+
const n = Math.max(1, Math.floor(size));
|
|
18
|
+
for (let i = 0; i < arr.length; i += n)
|
|
19
|
+
out.push(arr.slice(i, i + n));
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function encodeNativeDisperseViaMulticall3(params) {
|
|
23
|
+
const calls = params.to.map((target, idx) => ({
|
|
24
|
+
target,
|
|
25
|
+
allowFailure: false,
|
|
26
|
+
value: params.values[idx] ?? 0n,
|
|
27
|
+
callData: '0x',
|
|
28
|
+
}));
|
|
29
|
+
const totalValue = params.values.reduce((a, b) => a + (b ?? 0n), 0n);
|
|
30
|
+
const data = multicallIface.encodeFunctionData('aggregate3Value', [calls]);
|
|
31
|
+
return { totalValue, data };
|
|
32
|
+
}
|
|
12
33
|
function resolveProfitSettings(config) {
|
|
13
34
|
const extractProfit = config?.extractProfit !== false; // 默认 true(对齐 BSC)
|
|
14
|
-
|
|
35
|
+
// ✅ 对齐 BSC “捆绑换手模式”默认费率
|
|
36
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
|
|
15
37
|
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
16
38
|
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
17
39
|
return { extractProfit, profitBps, profitRecipient };
|
|
@@ -54,7 +76,7 @@ export class AADexSwapExecutor {
|
|
|
54
76
|
* AA 外盘单钱包换手签名
|
|
55
77
|
*/
|
|
56
78
|
async bundleSwapSign(params) {
|
|
57
|
-
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
79
|
+
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
58
80
|
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
59
81
|
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
60
82
|
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
|
|
@@ -101,16 +123,7 @@ export class AADexSwapExecutor {
|
|
|
101
123
|
const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerSender, effectiveRouter);
|
|
102
124
|
needApprove = allowance < sellAmountWei;
|
|
103
125
|
}
|
|
104
|
-
//
|
|
105
|
-
let finalBuyAmountWei;
|
|
106
|
-
if (buyAmountOkb) {
|
|
107
|
-
finalBuyAmountWei = ethers.parseEther(String(buyAmountOkb));
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
const quoted = await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
111
|
-
finalBuyAmountWei = (quoted * BigInt(10000 - slippageBps)) / 10000n;
|
|
112
|
-
}
|
|
113
|
-
// 用 quoteTokenToOkb 估算卖出输出(用于利润提取)
|
|
126
|
+
// 估算卖出输出(用于利润提取与 buy 预算)
|
|
114
127
|
const quotedSellOutWei = await (async () => {
|
|
115
128
|
try {
|
|
116
129
|
return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
@@ -119,7 +132,18 @@ export class AADexSwapExecutor {
|
|
|
119
132
|
return 0n;
|
|
120
133
|
}
|
|
121
134
|
})();
|
|
122
|
-
|
|
135
|
+
// buyAmount:若未传则按 quotedSellOut 计算;利润从 quotedSellOut 中刮取,但要保证买入资金充足
|
|
136
|
+
const requestedBuyWei = buyAmountOkb
|
|
137
|
+
? ethers.parseEther(String(buyAmountOkb))
|
|
138
|
+
: (quotedSellOutWei * BigInt(10000 - slippageBps)) / 10000n;
|
|
139
|
+
if (quotedSellOutWei > 0n && requestedBuyWei > quotedSellOutWei) {
|
|
140
|
+
throw new Error('AA 捆绑换手:buyAmountOkb 超过预估卖出输出(quotedSellOut)');
|
|
141
|
+
}
|
|
142
|
+
const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
143
|
+
const profitCap = quotedSellOutWei > requestedBuyWei ? (quotedSellOutWei - requestedBuyWei) : 0n;
|
|
144
|
+
const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
|
|
145
|
+
const finalBuyAmountWei = requestedBuyWei;
|
|
146
|
+
const hopCount = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
|
|
123
147
|
const outOps = [];
|
|
124
148
|
if (needApprove) {
|
|
125
149
|
const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
@@ -158,6 +182,19 @@ export class AADexSwapExecutor {
|
|
|
158
182
|
});
|
|
159
183
|
outOps.push(signedProfit.userOp);
|
|
160
184
|
}
|
|
185
|
+
// ✅ 卖出所得 OKB 转给买方 AA(Sender)
|
|
186
|
+
if (sellerSender.toLowerCase() !== buyerSender.toLowerCase() && finalBuyAmountWei > 0n) {
|
|
187
|
+
const transferCallData = encodeExecute(buyerSender, finalBuyAmountWei, '0x');
|
|
188
|
+
const signedTransfer = await this.aaManager.buildUserOpWithState({
|
|
189
|
+
ownerWallet: sellerOwner,
|
|
190
|
+
sender: sellerSender,
|
|
191
|
+
nonce: nonceMap.next(sellerSender),
|
|
192
|
+
initCode: consumeInitCode(sellerSender),
|
|
193
|
+
callData: transferCallData,
|
|
194
|
+
signOnly: true,
|
|
195
|
+
});
|
|
196
|
+
outOps.push(signedTransfer.userOp);
|
|
197
|
+
}
|
|
161
198
|
// Buy op
|
|
162
199
|
const buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], buyerSender, getDexDeadline());
|
|
163
200
|
const buyCallData = encodeExecute(effectiveRouter, finalBuyAmountWei, buySwapData);
|
|
@@ -208,6 +245,7 @@ export class AADexSwapExecutor {
|
|
|
208
245
|
profitRecipient,
|
|
209
246
|
profitWei: profitWei.toString(),
|
|
210
247
|
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
248
|
+
disperseHopCount: String(hopCount),
|
|
211
249
|
},
|
|
212
250
|
};
|
|
213
251
|
}
|
|
@@ -215,7 +253,7 @@ export class AADexSwapExecutor {
|
|
|
215
253
|
* AA 外盘批量换手签名
|
|
216
254
|
*/
|
|
217
255
|
async bundleBatchSwapSign(params) {
|
|
218
|
-
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
256
|
+
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
219
257
|
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
220
258
|
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
221
259
|
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
|
|
@@ -286,7 +324,10 @@ export class AADexSwapExecutor {
|
|
|
286
324
|
signOnly: true,
|
|
287
325
|
});
|
|
288
326
|
outOps.push(signedSell.userOp);
|
|
289
|
-
//
|
|
327
|
+
// 先计算 buyAmountsWei(用于后续分发与校验)
|
|
328
|
+
const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
|
|
329
|
+
const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
|
|
330
|
+
// Profit op:估算卖出输出,按比例刮取(但必须保证分发/买入资金充足)
|
|
290
331
|
const quotedSellOutWei = await (async () => {
|
|
291
332
|
try {
|
|
292
333
|
return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
@@ -295,7 +336,12 @@ export class AADexSwapExecutor {
|
|
|
295
336
|
return 0n;
|
|
296
337
|
}
|
|
297
338
|
})();
|
|
298
|
-
|
|
339
|
+
if (quotedSellOutWei > 0n && totalBuyWei > quotedSellOutWei) {
|
|
340
|
+
throw new Error('AA 批量换手:buyAmountsOkb 总和超过预估卖出输出(quotedSellOut)');
|
|
341
|
+
}
|
|
342
|
+
const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
343
|
+
const profitCap = quotedSellOutWei > totalBuyWei ? (quotedSellOutWei - totalBuyWei) : 0n;
|
|
344
|
+
const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
|
|
299
345
|
if (extractProfit && profitWei > 0n) {
|
|
300
346
|
const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
|
|
301
347
|
const signedProfit = await this.aaManager.buildUserOpWithState({
|
|
@@ -308,8 +354,92 @@ export class AADexSwapExecutor {
|
|
|
308
354
|
});
|
|
309
355
|
outOps.push(signedProfit.userOp);
|
|
310
356
|
}
|
|
311
|
-
//
|
|
312
|
-
const
|
|
357
|
+
// ✅ 卖出所得 OKB 分发给多个买方 AA(Sender)(支持多跳)
|
|
358
|
+
const buyerSenders = buyerAis.map(ai => ai.sender);
|
|
359
|
+
const hopCountRaw = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
|
|
360
|
+
const hopCount = Math.min(hopCountRaw, buyerSenders.length);
|
|
361
|
+
const maxPerOp = Math.max(1, Math.floor(Number(effectiveConfig.maxTransfersPerUserOpNative ?? 30)));
|
|
362
|
+
if (buyerSenders.length > 0 && totalBuyWei > 0n) {
|
|
363
|
+
if (hopCount <= 0) {
|
|
364
|
+
const items = buyerSenders.map((to, i) => ({ to, value: buyAmountsWei[i] ?? 0n })).filter(x => x.value > 0n);
|
|
365
|
+
const chunks = chunkArray(items, maxPerOp);
|
|
366
|
+
for (const ch of chunks) {
|
|
367
|
+
const { totalValue, data } = encodeNativeDisperseViaMulticall3({
|
|
368
|
+
to: ch.map(x => x.to),
|
|
369
|
+
values: ch.map(x => x.value),
|
|
370
|
+
});
|
|
371
|
+
const callData = encodeExecute(MULTICALL3, totalValue, data);
|
|
372
|
+
const signedDisperse = await this.aaManager.buildUserOpWithState({
|
|
373
|
+
ownerWallet: sellerOwner,
|
|
374
|
+
sender: sellerAi.sender,
|
|
375
|
+
nonce: nonceMap.next(sellerAi.sender),
|
|
376
|
+
initCode: consumeInitCode(sellerAi.sender),
|
|
377
|
+
callData,
|
|
378
|
+
signOnly: true,
|
|
379
|
+
});
|
|
380
|
+
outOps.push(signedDisperse.userOp);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
const hopSenders = buyerSenders.slice(0, hopCount);
|
|
385
|
+
const hopOwners = buyerOwners.slice(0, hopCount);
|
|
386
|
+
const hop0 = hopSenders[0];
|
|
387
|
+
const callData0 = encodeExecute(hop0, totalBuyWei, '0x');
|
|
388
|
+
const signedToHop0 = await this.aaManager.buildUserOpWithState({
|
|
389
|
+
ownerWallet: sellerOwner,
|
|
390
|
+
sender: sellerAi.sender,
|
|
391
|
+
nonce: nonceMap.next(sellerAi.sender),
|
|
392
|
+
initCode: consumeInitCode(sellerAi.sender),
|
|
393
|
+
callData: callData0,
|
|
394
|
+
signOnly: true,
|
|
395
|
+
});
|
|
396
|
+
outOps.push(signedToHop0.userOp);
|
|
397
|
+
let prefixKept = 0n;
|
|
398
|
+
for (let j = 0; j < hopSenders.length - 1; j++) {
|
|
399
|
+
const sender = hopSenders[j];
|
|
400
|
+
const next = hopSenders[j + 1];
|
|
401
|
+
const keep = buyAmountsWei[j] ?? 0n;
|
|
402
|
+
prefixKept += keep;
|
|
403
|
+
const remaining = totalBuyWei - prefixKept;
|
|
404
|
+
if (remaining <= 0n)
|
|
405
|
+
break;
|
|
406
|
+
const callData = encodeExecute(next, remaining, '0x');
|
|
407
|
+
const signedHop = await this.aaManager.buildUserOpWithState({
|
|
408
|
+
ownerWallet: hopOwners[j],
|
|
409
|
+
sender,
|
|
410
|
+
nonce: nonceMap.next(sender),
|
|
411
|
+
initCode: consumeInitCode(sender),
|
|
412
|
+
callData,
|
|
413
|
+
signOnly: true,
|
|
414
|
+
});
|
|
415
|
+
outOps.push(signedHop.userOp);
|
|
416
|
+
}
|
|
417
|
+
const lastHopIdx = hopSenders.length - 1;
|
|
418
|
+
const lastHopSender = hopSenders[lastHopIdx];
|
|
419
|
+
const lastHopOwner = hopOwners[lastHopIdx];
|
|
420
|
+
const rest = buyerSenders.slice(hopSenders.length);
|
|
421
|
+
const restAmounts = buyAmountsWei.slice(hopSenders.length);
|
|
422
|
+
const restItems = rest.map((to, i) => ({ to, value: restAmounts[i] ?? 0n })).filter(x => x.value > 0n);
|
|
423
|
+
const restChunks = chunkArray(restItems, maxPerOp);
|
|
424
|
+
for (const ch of restChunks) {
|
|
425
|
+
const { totalValue, data } = encodeNativeDisperseViaMulticall3({
|
|
426
|
+
to: ch.map(x => x.to),
|
|
427
|
+
values: ch.map(x => x.value),
|
|
428
|
+
});
|
|
429
|
+
const callData = encodeExecute(MULTICALL3, totalValue, data);
|
|
430
|
+
const signedDisperse = await this.aaManager.buildUserOpWithState({
|
|
431
|
+
ownerWallet: lastHopOwner,
|
|
432
|
+
sender: lastHopSender,
|
|
433
|
+
nonce: nonceMap.next(lastHopSender),
|
|
434
|
+
initCode: consumeInitCode(lastHopSender),
|
|
435
|
+
callData,
|
|
436
|
+
signOnly: true,
|
|
437
|
+
});
|
|
438
|
+
outOps.push(signedDisperse.userOp);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Batch Buy ops(分发后再执行)
|
|
313
443
|
for (let i = 0; i < buyerOwners.length; i++) {
|
|
314
444
|
const ai = buyerAis[i];
|
|
315
445
|
const buyWei = buyAmountsWei[i];
|
|
@@ -363,6 +493,7 @@ export class AADexSwapExecutor {
|
|
|
363
493
|
profitRecipient,
|
|
364
494
|
profitWei: profitWei.toString(),
|
|
365
495
|
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
496
|
+
disperseHopCount: String(hopCount),
|
|
366
497
|
},
|
|
367
498
|
};
|
|
368
499
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer 外盘(PotatoSwap/DEX)捆绑交易 SDK(AA / ERC-4337)
|
|
3
|
+
*
|
|
4
|
+
* 目标:对齐内盘 `bundle.ts` 的能力,但 Router/报价/授权逻辑走 DEX。
|
|
5
|
+
* - 捆绑买卖(买入 ->(可选授权)-> 卖出 ->(可选归集))
|
|
6
|
+
* - 自动归集 OKB 到 owner
|
|
7
|
+
* - ✅ 支持“刮取利润”:在归集时从可转出金额中按 bps 拆分转到 PROFIT_CONFIG.RECIPIENT
|
|
8
|
+
*/
|
|
9
|
+
import type { XLayerConfig, BundleBuySellResult, BundleBuySellParams } from './types.js';
|
|
10
|
+
export interface DexBundleConfig extends XLayerConfig {
|
|
11
|
+
dexKey?: string;
|
|
12
|
+
routerAddress?: string;
|
|
13
|
+
deadlineMinutes?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DexBundleBuySellParams extends BundleBuySellParams {
|
|
16
|
+
dexKey?: string;
|
|
17
|
+
routerAddress?: string;
|
|
18
|
+
deadlineMinutes?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 外盘捆绑交易执行器
|
|
22
|
+
*/
|
|
23
|
+
export declare class DexBundleExecutor {
|
|
24
|
+
private aaManager;
|
|
25
|
+
private portalQuery;
|
|
26
|
+
private dexQuery;
|
|
27
|
+
private config;
|
|
28
|
+
constructor(config?: DexBundleConfig);
|
|
29
|
+
private getEffectiveRouter;
|
|
30
|
+
private getDeadlineMinutes;
|
|
31
|
+
/**
|
|
32
|
+
* 执行 handleOps 并解析结果
|
|
33
|
+
*/
|
|
34
|
+
private runHandleOps;
|
|
35
|
+
private buildWithdrawUserOpWithState;
|
|
36
|
+
/**
|
|
37
|
+
* 外盘捆绑买卖(先买后卖)
|
|
38
|
+
*/
|
|
39
|
+
bundleBuySell(params: DexBundleBuySellParams): Promise<BundleBuySellResult>;
|
|
40
|
+
}
|
|
41
|
+
export declare function createDexBundleExecutor(config?: DexBundleConfig): DexBundleExecutor;
|
|
42
|
+
export declare function dexBundleBuySell(params: DexBundleBuySellParams): Promise<BundleBuySellResult>;
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer 外盘(PotatoSwap/DEX)捆绑交易 SDK(AA / ERC-4337)
|
|
3
|
+
*
|
|
4
|
+
* 目标:对齐内盘 `bundle.ts` 的能力,但 Router/报价/授权逻辑走 DEX。
|
|
5
|
+
* - 捆绑买卖(买入 ->(可选授权)-> 卖出 ->(可选归集))
|
|
6
|
+
* - 自动归集 OKB 到 owner
|
|
7
|
+
* - ✅ 支持“刮取利润”:在归集时从可转出金额中按 bps 拆分转到 PROFIT_CONFIG.RECIPIENT
|
|
8
|
+
*/
|
|
9
|
+
import { Wallet, Contract, Interface, ethers } from 'ethers';
|
|
10
|
+
import { ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, POTATOSWAP_V2_ROUTER, WOKB, } from './constants.js';
|
|
11
|
+
import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
|
|
12
|
+
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee } from './dex.js';
|
|
13
|
+
import { PortalQuery, encodeApproveCall, parseOkb, formatOkb } from './portal-ops.js';
|
|
14
|
+
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
15
|
+
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
16
|
+
import { AANonceMap } from './types.js';
|
|
17
|
+
function getDexDeadline(minutes = 20) {
|
|
18
|
+
return Math.floor(Date.now() / 1000) + minutes * 60;
|
|
19
|
+
}
|
|
20
|
+
function resolveProfitSettings(config) {
|
|
21
|
+
const extractProfit = config?.extractProfit !== false; // 默认 true
|
|
22
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
|
|
23
|
+
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
24
|
+
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
25
|
+
return { extractProfit, profitBps, profitRecipient };
|
|
26
|
+
}
|
|
27
|
+
function calculateProfitWei(amountWei, profitBps) {
|
|
28
|
+
if (amountWei <= 0n)
|
|
29
|
+
return 0n;
|
|
30
|
+
if (!Number.isFinite(profitBps) || profitBps <= 0)
|
|
31
|
+
return 0n;
|
|
32
|
+
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
33
|
+
}
|
|
34
|
+
// 固定 gas(用于大规模减少 RPC);具体值允许通过 config.fixedGas 覆盖
|
|
35
|
+
const DEFAULT_CALL_GAS_LIMIT_BUY = DEFAULT_CALL_GAS_LIMIT_SELL;
|
|
36
|
+
const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
|
|
37
|
+
const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
|
|
38
|
+
/**
|
|
39
|
+
* 外盘捆绑交易执行器
|
|
40
|
+
*/
|
|
41
|
+
export class DexBundleExecutor {
|
|
42
|
+
constructor(config = {}) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
this.aaManager = new AAAccountManager(config);
|
|
45
|
+
this.portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
|
|
46
|
+
this.dexQuery = new DexQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId, routerAddress: config.routerAddress });
|
|
47
|
+
}
|
|
48
|
+
getEffectiveRouter(params) {
|
|
49
|
+
if (params.routerAddress)
|
|
50
|
+
return params.routerAddress;
|
|
51
|
+
if (params.dexKey === 'DYORSWAP') {
|
|
52
|
+
return '0xfb001fbbace32f09cb6d3c449b935183de53ee96';
|
|
53
|
+
}
|
|
54
|
+
return POTATOSWAP_V2_ROUTER;
|
|
55
|
+
}
|
|
56
|
+
getDeadlineMinutes(params) {
|
|
57
|
+
return params.deadlineMinutes ?? this.config.deadlineMinutes ?? 20;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 执行 handleOps 并解析结果
|
|
61
|
+
*/
|
|
62
|
+
async runHandleOps(label, ops, bundlerSigner, beneficiary) {
|
|
63
|
+
if (ops.length === 0) {
|
|
64
|
+
console.log(`\n[${label}] 没有 ops,跳过`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const provider = this.aaManager.getProvider();
|
|
68
|
+
const entryPointAddress = this.aaManager.getEntryPointAddress();
|
|
69
|
+
const feeData = await provider.getFeeData();
|
|
70
|
+
console.log(`\n[${label}] 发送 handleOps,ops=${ops.length} ...`);
|
|
71
|
+
const entryPointWithSigner = new Contract(entryPointAddress, ENTRYPOINT_ABI, bundlerSigner);
|
|
72
|
+
const tx = await entryPointWithSigner.handleOps(ops, beneficiary, { gasPrice: feeData.gasPrice ?? 100000000n });
|
|
73
|
+
console.log(`[${label}] txHash:`, tx.hash);
|
|
74
|
+
const receipt = await tx.wait();
|
|
75
|
+
console.log(`[${label}] mined block=${receipt.blockNumber} status=${receipt.status}`);
|
|
76
|
+
const epIface = new Interface(ENTRYPOINT_ABI);
|
|
77
|
+
const userOpEvents = [];
|
|
78
|
+
for (const log of receipt.logs) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = epIface.parseLog(log);
|
|
81
|
+
if (parsed?.name === 'UserOperationEvent') {
|
|
82
|
+
const e = parsed.args;
|
|
83
|
+
userOpEvents.push({
|
|
84
|
+
userOpHash: e.userOpHash,
|
|
85
|
+
sender: e.sender,
|
|
86
|
+
paymaster: e.paymaster,
|
|
87
|
+
success: e.success,
|
|
88
|
+
actualGasCost: e.actualGasCost,
|
|
89
|
+
actualGasUsed: e.actualGasUsed,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
txHash: tx.hash,
|
|
97
|
+
blockNumber: receipt.blockNumber,
|
|
98
|
+
status: receipt.status,
|
|
99
|
+
userOpEvents,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async buildWithdrawUserOpWithState(params) {
|
|
103
|
+
const senderBalance = params.senderBalance;
|
|
104
|
+
if (senderBalance <= params.reserveWei) {
|
|
105
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] sender OKB 太少,跳过归集:${formatOkb(senderBalance)} OKB`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// 先估算 prefund(使用空调用)
|
|
109
|
+
const tempCallData = encodeExecute(params.ownerWallet.address, 0n, '0x');
|
|
110
|
+
if (!params.signOnly) {
|
|
111
|
+
await this.aaManager.ensureSenderBalance(params.ownerWallet, params.sender, parseOkb('0.0002'), `${params.ownerName ?? 'owner'}/withdraw-prefund`);
|
|
112
|
+
}
|
|
113
|
+
const effConfig = { ...(this.config ?? {}), ...(params.configOverride ?? {}) };
|
|
114
|
+
const gasPolicyRaw = effConfig.gasPolicy ?? 'bundlerEstimate';
|
|
115
|
+
const gasPolicy = params.signOnly && gasPolicyRaw === 'bundlerEstimate' ? 'fixed' : gasPolicyRaw;
|
|
116
|
+
const { prefundWei } = gasPolicy === 'fixed'
|
|
117
|
+
? await this.aaManager.buildUserOpWithFixedGas({
|
|
118
|
+
ownerWallet: params.ownerWallet,
|
|
119
|
+
sender: params.sender,
|
|
120
|
+
callData: tempCallData,
|
|
121
|
+
nonce: params.nonce,
|
|
122
|
+
initCode: params.initCode,
|
|
123
|
+
deployed: params.initCode === '0x',
|
|
124
|
+
fixedGas: {
|
|
125
|
+
...(effConfig.fixedGas ?? {}),
|
|
126
|
+
callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
: gasPolicy === 'localEstimate'
|
|
130
|
+
? await this.aaManager.buildUserOpWithLocalEstimate({
|
|
131
|
+
ownerWallet: params.ownerWallet,
|
|
132
|
+
sender: params.sender,
|
|
133
|
+
callData: tempCallData,
|
|
134
|
+
nonce: params.nonce,
|
|
135
|
+
initCode: params.initCode,
|
|
136
|
+
})
|
|
137
|
+
: await this.aaManager.buildUserOpWithBundlerEstimate({
|
|
138
|
+
ownerWallet: params.ownerWallet,
|
|
139
|
+
sender: params.sender,
|
|
140
|
+
callData: tempCallData,
|
|
141
|
+
nonce: params.nonce,
|
|
142
|
+
initCode: params.initCode,
|
|
143
|
+
});
|
|
144
|
+
const withdrawAmount = senderBalance > prefundWei + params.reserveWei
|
|
145
|
+
? senderBalance - prefundWei - params.reserveWei
|
|
146
|
+
: 0n;
|
|
147
|
+
if (withdrawAmount <= 0n) {
|
|
148
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] 归集后可转出=0(余额不足以覆盖 prefund+reserve)`);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
|
|
152
|
+
const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
|
|
153
|
+
const toOwnerWei = withdrawAmount - profitWei;
|
|
154
|
+
const callData = extractProfit && profitWei > 0n
|
|
155
|
+
? encodeExecuteBatch([profitRecipient, params.ownerWallet.address], [profitWei, toOwnerWei], ['0x', '0x'])
|
|
156
|
+
: encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
|
|
157
|
+
const { userOp } = gasPolicy === 'fixed'
|
|
158
|
+
? await this.aaManager.buildUserOpWithFixedGas({
|
|
159
|
+
ownerWallet: params.ownerWallet,
|
|
160
|
+
sender: params.sender,
|
|
161
|
+
callData,
|
|
162
|
+
nonce: params.nonce,
|
|
163
|
+
initCode: params.initCode,
|
|
164
|
+
deployed: params.initCode === '0x',
|
|
165
|
+
fixedGas: {
|
|
166
|
+
...(effConfig.fixedGas ?? {}),
|
|
167
|
+
callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
: gasPolicy === 'localEstimate'
|
|
171
|
+
? await this.aaManager.buildUserOpWithLocalEstimate({
|
|
172
|
+
ownerWallet: params.ownerWallet,
|
|
173
|
+
sender: params.sender,
|
|
174
|
+
callData,
|
|
175
|
+
nonce: params.nonce,
|
|
176
|
+
initCode: params.initCode,
|
|
177
|
+
})
|
|
178
|
+
: await this.aaManager.buildUserOpWithBundlerEstimate({
|
|
179
|
+
ownerWallet: params.ownerWallet,
|
|
180
|
+
sender: params.sender,
|
|
181
|
+
callData,
|
|
182
|
+
nonce: params.nonce,
|
|
183
|
+
initCode: params.initCode,
|
|
184
|
+
});
|
|
185
|
+
if (extractProfit && profitWei > 0n) {
|
|
186
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(toOwnerWei)} OKB (profit: ${formatOkb(profitWei)} OKB -> ${profitRecipient})`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
|
|
190
|
+
}
|
|
191
|
+
return { userOp, prefundWei };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 外盘捆绑买卖(先买后卖)
|
|
195
|
+
*/
|
|
196
|
+
async bundleBuySell(params) {
|
|
197
|
+
const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, dexKey, routerAddress, deadlineMinutes, } = params;
|
|
198
|
+
if (privateKeys.length !== buyAmounts.length) {
|
|
199
|
+
throw new Error('私钥数量和购买金额数量必须一致');
|
|
200
|
+
}
|
|
201
|
+
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress });
|
|
202
|
+
const deadline = getDexDeadline(this.getDeadlineMinutes({ deadlineMinutes }));
|
|
203
|
+
const sharedProvider = this.aaManager.getProvider();
|
|
204
|
+
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
205
|
+
const bundlerSigner = wallets[0];
|
|
206
|
+
const beneficiary = bundlerSigner.address;
|
|
207
|
+
const reserveWei = parseOkb(withdrawReserve);
|
|
208
|
+
console.log('=== XLayer DEX Bundle Buy -> Sell ===');
|
|
209
|
+
console.log('router:', effectiveRouter);
|
|
210
|
+
console.log('token:', tokenAddress);
|
|
211
|
+
console.log('owners:', wallets.length);
|
|
212
|
+
console.log('sellPercent:', sellPercent);
|
|
213
|
+
const effCfg = { ...(this.config ?? {}), ...(config ?? {}) };
|
|
214
|
+
const profitSettings = resolveProfitSettings(effCfg);
|
|
215
|
+
// 1) accountInfos / nonceMap
|
|
216
|
+
const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
|
|
217
|
+
const senders = accountInfos.map((ai) => ai.sender);
|
|
218
|
+
const nonceMap = new AANonceMap();
|
|
219
|
+
for (const ai of accountInfos)
|
|
220
|
+
nonceMap.init(ai.sender, ai.nonce);
|
|
221
|
+
// 2) 买入(批量估算 + 补余额 + 并发签名)
|
|
222
|
+
const buyWeis = buyAmounts.map((a) => parseOkb(a));
|
|
223
|
+
await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
|
|
224
|
+
const buyWei = buyWeis[i] ?? 0n;
|
|
225
|
+
if (buyWei <= 0n)
|
|
226
|
+
return;
|
|
227
|
+
await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWei + parseOkb('0.0003'), `owner${i + 1}/dex-buy-prefund-before-estimate`);
|
|
228
|
+
});
|
|
229
|
+
const buyCallDatas = buyWeis.map((buyWei, i) => {
|
|
230
|
+
const sender = senders[i];
|
|
231
|
+
const swapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], sender, deadline);
|
|
232
|
+
return encodeExecute(effectiveRouter, buyWei, swapData);
|
|
233
|
+
});
|
|
234
|
+
const initCodes = accountInfos.map((ai, i) => (ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address)));
|
|
235
|
+
const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
|
|
236
|
+
ops: accountInfos.map((ai, i) => ({
|
|
237
|
+
sender: ai.sender,
|
|
238
|
+
nonce: nonceMap.next(ai.sender),
|
|
239
|
+
callData: buyCallDatas[i],
|
|
240
|
+
initCode: initCodes[i],
|
|
241
|
+
})),
|
|
242
|
+
});
|
|
243
|
+
await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
|
|
244
|
+
const buyWei = buyWeis[i] ?? 0n;
|
|
245
|
+
await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWei + (prefundWeis[i] ?? 0n) + parseOkb('0.0002'), `owner${i + 1}/dex-buy-fund`);
|
|
246
|
+
});
|
|
247
|
+
const signedBuy = await mapWithConcurrency(buyUserOps, 10, async (op, i) => this.aaManager.signUserOp(op, wallets[i]));
|
|
248
|
+
const buyOps = signedBuy.map((s) => s.userOp);
|
|
249
|
+
const buyResult = await this.runHandleOps('dex-buyBundle', buyOps, bundlerSigner, beneficiary);
|
|
250
|
+
if (!buyResult) {
|
|
251
|
+
throw new Error('买入交易失败');
|
|
252
|
+
}
|
|
253
|
+
// 3) 授权 + 卖出(同一笔 handleOps)
|
|
254
|
+
const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
|
|
255
|
+
const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders, effectiveRouter);
|
|
256
|
+
const sellOps = [];
|
|
257
|
+
const sellPerWallet = await mapWithConcurrency(wallets, 4, async (w, i) => {
|
|
258
|
+
const sender = senders[i];
|
|
259
|
+
const balance = tokenBalances.get(sender) ?? 0n;
|
|
260
|
+
if (balance === 0n) {
|
|
261
|
+
console.log(`[owner${i + 1}] 没买到代币,跳过卖出`);
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
const allowance = allowances.get(sender) ?? 0n;
|
|
265
|
+
const needApprove = allowance < balance;
|
|
266
|
+
const out = [];
|
|
267
|
+
// buy 已经用过一次,因此 initCode = 0x(且 nonce 已更新过)
|
|
268
|
+
const initCode = '0x';
|
|
269
|
+
if (needApprove) {
|
|
270
|
+
const approveNonce = nonceMap.next(sender);
|
|
271
|
+
const approveCallData = encodeExecute(tokenAddress, 0n, encodeApproveCall(effectiveRouter, ethers.MaxUint256));
|
|
272
|
+
const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
273
|
+
ownerWallet: w,
|
|
274
|
+
sender,
|
|
275
|
+
nonce: approveNonce,
|
|
276
|
+
initCode,
|
|
277
|
+
callData: approveCallData,
|
|
278
|
+
deployed: true,
|
|
279
|
+
fixedGas: {
|
|
280
|
+
...(effCfg.fixedGas ?? {}),
|
|
281
|
+
callGasLimit: effCfg.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_APPROVE,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
const signed = await this.aaManager.signUserOp(userOp, w);
|
|
285
|
+
out.push(signed.userOp);
|
|
286
|
+
}
|
|
287
|
+
const sellAmount = (balance * BigInt(sellPercent)) / 100n;
|
|
288
|
+
if (sellAmount === 0n)
|
|
289
|
+
return out;
|
|
290
|
+
const sellNonce = nonceMap.next(sender);
|
|
291
|
+
const sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellAmount, 0n, [tokenAddress, WOKB], sender, deadline);
|
|
292
|
+
const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
|
|
293
|
+
const signedSell = await this.aaManager.buildUserOpWithState({
|
|
294
|
+
ownerWallet: w,
|
|
295
|
+
sender,
|
|
296
|
+
nonce: sellNonce,
|
|
297
|
+
initCode,
|
|
298
|
+
callData: sellCallData,
|
|
299
|
+
signOnly: false,
|
|
300
|
+
});
|
|
301
|
+
out.push(signedSell.userOp);
|
|
302
|
+
return out;
|
|
303
|
+
});
|
|
304
|
+
for (const ops of sellPerWallet)
|
|
305
|
+
sellOps.push(...ops);
|
|
306
|
+
const sellResult = await this.runHandleOps('dex-sellBundle', sellOps, bundlerSigner, beneficiary);
|
|
307
|
+
if (!sellResult) {
|
|
308
|
+
throw new Error('卖出交易失败');
|
|
309
|
+
}
|
|
310
|
+
// 4) 归集 OKB(sell 后状态)
|
|
311
|
+
let withdrawResult;
|
|
312
|
+
let totalProfitWei = 0n;
|
|
313
|
+
if (withdrawToOwner) {
|
|
314
|
+
const withdrawOps = [];
|
|
315
|
+
const okbBalances = await this.portalQuery.getMultipleOkbBalances(senders);
|
|
316
|
+
const withdrawItems = wallets.map((w, i) => ({
|
|
317
|
+
i,
|
|
318
|
+
ownerWallet: w,
|
|
319
|
+
sender: senders[i],
|
|
320
|
+
senderBalance: okbBalances.get(senders[i]) ?? 0n,
|
|
321
|
+
nonce: nonceMap.peek(senders[i]),
|
|
322
|
+
initCode: '0x',
|
|
323
|
+
}));
|
|
324
|
+
const signedWithdraws = await mapWithConcurrency(withdrawItems, 3, async (it) => {
|
|
325
|
+
const built = await this.buildWithdrawUserOpWithState({
|
|
326
|
+
ownerWallet: it.ownerWallet,
|
|
327
|
+
sender: it.sender,
|
|
328
|
+
nonce: it.nonce,
|
|
329
|
+
initCode: it.initCode,
|
|
330
|
+
senderBalance: it.senderBalance,
|
|
331
|
+
reserveWei,
|
|
332
|
+
ownerName: `owner${it.i + 1}`,
|
|
333
|
+
configOverride: config,
|
|
334
|
+
});
|
|
335
|
+
if (built) {
|
|
336
|
+
nonceMap.commit(it.sender, it.nonce);
|
|
337
|
+
withdrawOps.push(built.userOp);
|
|
338
|
+
if (profitSettings.extractProfit) {
|
|
339
|
+
const withdrawAmount = it.senderBalance > built.prefundWei + reserveWei
|
|
340
|
+
? it.senderBalance - built.prefundWei - reserveWei
|
|
341
|
+
: 0n;
|
|
342
|
+
totalProfitWei += calculateProfitWei(withdrawAmount, profitSettings.profitBps);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
void signedWithdraws; // silence unused (build is applied in loop)
|
|
347
|
+
if (withdrawOps.length > 0) {
|
|
348
|
+
withdrawResult = await this.runHandleOps('dex-withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
|
|
352
|
+
return {
|
|
353
|
+
buyResult,
|
|
354
|
+
sellResult,
|
|
355
|
+
withdrawResult,
|
|
356
|
+
finalBalances,
|
|
357
|
+
profit: withdrawToOwner
|
|
358
|
+
? {
|
|
359
|
+
extractProfit: profitSettings.extractProfit,
|
|
360
|
+
profitBps: profitSettings.profitBps,
|
|
361
|
+
profitRecipient: profitSettings.profitRecipient,
|
|
362
|
+
totalProfitWei: totalProfitWei.toString(),
|
|
363
|
+
}
|
|
364
|
+
: undefined,
|
|
365
|
+
// approveResult: 这里我们把 approve 合并进 sellBundle 了,不单独返回
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ============================================================================
|
|
370
|
+
// 便捷函数
|
|
371
|
+
// ============================================================================
|
|
372
|
+
export function createDexBundleExecutor(config) {
|
|
373
|
+
return new DexBundleExecutor(config);
|
|
374
|
+
}
|
|
375
|
+
export async function dexBundleBuySell(params) {
|
|
376
|
+
const executor = createDexBundleExecutor(params.config);
|
|
377
|
+
return executor.bundleBuySell(params);
|
|
378
|
+
}
|