four-flap-meme-sdk 1.5.32 → 1.5.33
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/xlayer/bundle.js +47 -3
- 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-volume.d.ts +30 -0
- package/dist/xlayer/dex-volume.js +80 -0
- package/dist/xlayer/index.d.ts +2 -0
- package/dist/xlayer/index.js +8 -0
- package/dist/xlayer/portal-bundle-swap.js +153 -12
- package/dist/xlayer/types.d.ts +41 -0
- package/package.json +1 -1
package/dist/xlayer/bundle.js
CHANGED
|
@@ -75,7 +75,8 @@ const DEFAULT_CALL_GAS_LIMIT_TRANSFER = 150000n;
|
|
|
75
75
|
const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
|
|
76
76
|
function resolveProfitSettings(config) {
|
|
77
77
|
const extractProfit = config?.extractProfit !== false; // 默认 true
|
|
78
|
-
|
|
78
|
+
// ✅ 对齐 bundle/刷量模式默认费率(与 BSC bundle-buy-first 语义一致)
|
|
79
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
|
|
79
80
|
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
80
81
|
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
81
82
|
return { extractProfit, profitBps, profitRecipient };
|
|
@@ -706,6 +707,9 @@ export class BundleExecutor {
|
|
|
706
707
|
}
|
|
707
708
|
// 3. 可选:归集 OKB
|
|
708
709
|
let withdrawResult;
|
|
710
|
+
let totalProfitWei = 0n;
|
|
711
|
+
const effProfitCfg = { ...(this.config ?? {}), ...(config ?? {}) };
|
|
712
|
+
const profitSettings = resolveProfitSettings(effProfitCfg);
|
|
709
713
|
if (withdrawToOwner) {
|
|
710
714
|
const withdrawOps = [];
|
|
711
715
|
// 批量获取 sender OKB 余额
|
|
@@ -731,6 +735,12 @@ export class BundleExecutor {
|
|
|
731
735
|
ownerName: `owner${it.i + 1}`,
|
|
732
736
|
configOverride: config,
|
|
733
737
|
});
|
|
738
|
+
if (signed && profitSettings.extractProfit) {
|
|
739
|
+
const withdrawAmount = it.senderBalance > signed.prefundWei + reserveWei
|
|
740
|
+
? it.senderBalance - signed.prefundWei - reserveWei
|
|
741
|
+
: 0n;
|
|
742
|
+
totalProfitWei += calculateProfitWei(withdrawAmount, profitSettings.profitBps);
|
|
743
|
+
}
|
|
734
744
|
if (signed?.userOp)
|
|
735
745
|
nonceMap.commit(it.sender, it.nonce);
|
|
736
746
|
return signed?.userOp ?? null;
|
|
@@ -743,7 +753,19 @@ export class BundleExecutor {
|
|
|
743
753
|
withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
|
|
744
754
|
}
|
|
745
755
|
}
|
|
746
|
-
return {
|
|
756
|
+
return {
|
|
757
|
+
approveResult,
|
|
758
|
+
sellResult,
|
|
759
|
+
withdrawResult,
|
|
760
|
+
profit: withdrawToOwner
|
|
761
|
+
? {
|
|
762
|
+
extractProfit: profitSettings.extractProfit,
|
|
763
|
+
profitBps: profitSettings.profitBps,
|
|
764
|
+
profitRecipient: profitSettings.profitRecipient,
|
|
765
|
+
totalProfitWei: totalProfitWei.toString(),
|
|
766
|
+
}
|
|
767
|
+
: undefined,
|
|
768
|
+
};
|
|
747
769
|
}
|
|
748
770
|
/**
|
|
749
771
|
* 捆绑买卖(先买后卖)
|
|
@@ -838,6 +860,9 @@ export class BundleExecutor {
|
|
|
838
860
|
}
|
|
839
861
|
// 3. 可选:归集 OKB
|
|
840
862
|
let withdrawResult;
|
|
863
|
+
let totalProfitWei = 0n;
|
|
864
|
+
const effProfitCfg = { ...(this.config ?? {}), ...(config ?? {}) };
|
|
865
|
+
const profitSettings = resolveProfitSettings(effProfitCfg);
|
|
841
866
|
if (withdrawToOwner) {
|
|
842
867
|
const withdrawOps = [];
|
|
843
868
|
// 批量获取 OKB 余额(sell 后状态)
|
|
@@ -861,6 +886,12 @@ export class BundleExecutor {
|
|
|
861
886
|
ownerName: `owner${it.i + 1}`,
|
|
862
887
|
configOverride: config,
|
|
863
888
|
});
|
|
889
|
+
if (signed && profitSettings.extractProfit) {
|
|
890
|
+
const withdrawAmount = it.senderBalance > signed.prefundWei + reserveWei
|
|
891
|
+
? it.senderBalance - signed.prefundWei - reserveWei
|
|
892
|
+
: 0n;
|
|
893
|
+
totalProfitWei += calculateProfitWei(withdrawAmount, profitSettings.profitBps);
|
|
894
|
+
}
|
|
864
895
|
if (signed?.userOp)
|
|
865
896
|
nonceMap.commit(it.sender, it.nonce);
|
|
866
897
|
return signed?.userOp ?? null;
|
|
@@ -875,7 +906,20 @@ export class BundleExecutor {
|
|
|
875
906
|
}
|
|
876
907
|
// 最终余额
|
|
877
908
|
const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
|
|
878
|
-
return {
|
|
909
|
+
return {
|
|
910
|
+
buyResult,
|
|
911
|
+
sellResult,
|
|
912
|
+
withdrawResult,
|
|
913
|
+
finalBalances,
|
|
914
|
+
profit: withdrawToOwner
|
|
915
|
+
? {
|
|
916
|
+
extractProfit: profitSettings.extractProfit,
|
|
917
|
+
profitBps: profitSettings.profitBps,
|
|
918
|
+
profitRecipient: profitSettings.profitRecipient,
|
|
919
|
+
totalProfitWei: totalProfitWei.toString(),
|
|
920
|
+
}
|
|
921
|
+
: undefined,
|
|
922
|
+
};
|
|
879
923
|
}
|
|
880
924
|
}
|
|
881
925
|
// ============================================================================
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer 外盘刷量/做市 SDK(AA / ERC-4337)
|
|
3
|
+
*
|
|
4
|
+
* - 与 `src/xlayer/volume.ts`(内盘)分离
|
|
5
|
+
* - 底层使用 `DexBundleExecutor.bundleBuySell`(买入 -> 卖出 -> 归集 -> 刮利润)
|
|
6
|
+
*/
|
|
7
|
+
import type { XLayerConfig, VolumeResult } from './types.js';
|
|
8
|
+
export interface DexVolumeParams {
|
|
9
|
+
tokenAddress: string;
|
|
10
|
+
privateKeys: string[];
|
|
11
|
+
buyAmountPerRound: string;
|
|
12
|
+
rounds: number;
|
|
13
|
+
intervalMs?: number;
|
|
14
|
+
sellImmediately?: boolean;
|
|
15
|
+
sellPercent?: number;
|
|
16
|
+
withdrawToOwner?: boolean;
|
|
17
|
+
withdrawReserve?: string;
|
|
18
|
+
dexKey?: string;
|
|
19
|
+
routerAddress?: string;
|
|
20
|
+
deadlineMinutes?: number;
|
|
21
|
+
config?: Partial<XLayerConfig>;
|
|
22
|
+
}
|
|
23
|
+
export declare class DexVolumeExecutor {
|
|
24
|
+
private dexBundleExecutor;
|
|
25
|
+
private config;
|
|
26
|
+
constructor(config?: XLayerConfig);
|
|
27
|
+
execute(params: DexVolumeParams): Promise<VolumeResult>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createDexVolumeExecutor(config?: XLayerConfig): DexVolumeExecutor;
|
|
30
|
+
export declare function makeDexVolume(params: DexVolumeParams): Promise<VolumeResult>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer 外盘刷量/做市 SDK(AA / ERC-4337)
|
|
3
|
+
*
|
|
4
|
+
* - 与 `src/xlayer/volume.ts`(内盘)分离
|
|
5
|
+
* - 底层使用 `DexBundleExecutor.bundleBuySell`(买入 -> 卖出 -> 归集 -> 刮利润)
|
|
6
|
+
*/
|
|
7
|
+
import { parseOkb, formatOkb } from './portal-ops.js';
|
|
8
|
+
import { DexBundleExecutor } from './dex-bundle.js';
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
export class DexVolumeExecutor {
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.dexBundleExecutor = new DexBundleExecutor(config);
|
|
16
|
+
}
|
|
17
|
+
async execute(params) {
|
|
18
|
+
const { tokenAddress, privateKeys, buyAmountPerRound, rounds, intervalMs = 3000, sellImmediately = true, sellPercent, withdrawToOwner = true, withdrawReserve, dexKey, routerAddress, deadlineMinutes, config, } = params;
|
|
19
|
+
console.log('=== XLayer DEX Volume Maker ===');
|
|
20
|
+
console.log('token:', tokenAddress);
|
|
21
|
+
console.log('owners:', privateKeys.length);
|
|
22
|
+
console.log('rounds:', rounds);
|
|
23
|
+
console.log('buyAmountPerRound:', buyAmountPerRound, 'OKB');
|
|
24
|
+
console.log('intervalMs:', intervalMs);
|
|
25
|
+
console.log('sellImmediately:', sellImmediately);
|
|
26
|
+
const roundResults = [];
|
|
27
|
+
let successRounds = 0;
|
|
28
|
+
let failedRounds = 0;
|
|
29
|
+
let totalVolume = 0n;
|
|
30
|
+
const buyWei = parseOkb(buyAmountPerRound);
|
|
31
|
+
for (let round = 0; round < rounds; round++) {
|
|
32
|
+
console.log(`\n========== DEX Round ${round + 1}/${rounds} ==========`);
|
|
33
|
+
try {
|
|
34
|
+
const buyAmounts = privateKeys.map(() => buyAmountPerRound);
|
|
35
|
+
const effSellPercent = typeof sellPercent === 'number' ? sellPercent : (sellImmediately ? 100 : 0);
|
|
36
|
+
const result = await this.dexBundleExecutor.bundleBuySell({
|
|
37
|
+
tokenAddress,
|
|
38
|
+
privateKeys,
|
|
39
|
+
buyAmounts,
|
|
40
|
+
sellPercent: effSellPercent,
|
|
41
|
+
withdrawToOwner,
|
|
42
|
+
withdrawReserve,
|
|
43
|
+
dexKey,
|
|
44
|
+
routerAddress,
|
|
45
|
+
deadlineMinutes,
|
|
46
|
+
config: { ...(this.config ?? {}), ...(config ?? {}) },
|
|
47
|
+
});
|
|
48
|
+
roundResults.push(result);
|
|
49
|
+
successRounds++;
|
|
50
|
+
totalVolume += buyWei * BigInt(privateKeys.length);
|
|
51
|
+
console.log(`DEX Round ${round + 1} 完成`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error(`DEX Round ${round + 1} 失败:`, error);
|
|
55
|
+
failedRounds++;
|
|
56
|
+
}
|
|
57
|
+
if (round < rounds - 1 && intervalMs > 0) {
|
|
58
|
+
console.log(`等待 ${intervalMs}ms...`);
|
|
59
|
+
await sleep(intervalMs);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log('\n=== DEX Volume Summary ===');
|
|
63
|
+
console.log('成功轮数:', successRounds);
|
|
64
|
+
console.log('失败轮数:', failedRounds);
|
|
65
|
+
console.log('总交易量:', formatOkb(totalVolume), 'OKB');
|
|
66
|
+
return {
|
|
67
|
+
successRounds,
|
|
68
|
+
failedRounds,
|
|
69
|
+
roundResults,
|
|
70
|
+
totalVolume,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function createDexVolumeExecutor(config) {
|
|
75
|
+
return new DexVolumeExecutor(config);
|
|
76
|
+
}
|
|
77
|
+
export async function makeDexVolume(params) {
|
|
78
|
+
const executor = createDexVolumeExecutor(params.config);
|
|
79
|
+
return executor.execute(params);
|
|
80
|
+
}
|
package/dist/xlayer/index.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export { BundlerClient, createBundlerClient, type BundlerConfig, type BundlerRec
|
|
|
63
63
|
export { AAAccountManager, createAAAccountManager, predictSender, createWallet, encodeExecute, encodeExecuteBatch, generateAAWallets, generateAAWalletsFromMnemonic, predictSendersFromPrivateKeys, type GeneratedAAWallet, type GenerateAAWalletsParams, type GenerateAAWalletsResult, } from './aa-account.js';
|
|
64
64
|
export { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, createPortalQuery, applySlippage, formatOkb, parseOkb, formatTokenAmount, parseTokenAmount, type PortalQueryConfig, } from './portal-ops.js';
|
|
65
65
|
export { BundleExecutor, createBundleExecutor, bundleBuy, bundleSell, bundleBuySell, } from './bundle.js';
|
|
66
|
+
export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, type DexBundleConfig, type DexBundleBuySellParams, } from './dex-bundle.js';
|
|
66
67
|
export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
|
|
67
68
|
export { AADexSwapExecutor, } from './dex-bundle-swap.js';
|
|
68
69
|
/**
|
|
@@ -78,6 +79,7 @@ export declare function bundleBatchSwapSign(params: BundleBatchSwapSignParams &
|
|
|
78
79
|
skipApprovalCheck?: boolean;
|
|
79
80
|
}): Promise<BundleBatchSwapSignResult>;
|
|
80
81
|
export { VolumeExecutor, createVolumeExecutor, makeVolume, singleRoundVolume, } from './volume.js';
|
|
82
|
+
export { DexVolumeExecutor, createDexVolumeExecutor, makeDexVolume, type DexVolumeParams, } from './dex-volume.js';
|
|
81
83
|
export { DexQuery, DexExecutor, createDexQuery, createDexExecutor, quoteOkbToToken, quoteTokenToOkb, encodeSwapExactETHForTokens, encodeSwapExactTokensForETH, encodeSwapExactTokensForTokens, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactTokensForTokensSupportingFee, type DexConfig, } from './dex.js';
|
|
82
84
|
export declare const xlayer: {
|
|
83
85
|
bundleBuy: (params: import("./types.js").BundleBuyParams) => Promise<import("./types.js").BundleBuyResult>;
|
package/dist/xlayer/index.js
CHANGED
|
@@ -83,6 +83,10 @@ export { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, P
|
|
|
83
83
|
// ============================================================================
|
|
84
84
|
export { BundleExecutor, createBundleExecutor, bundleBuy, bundleSell, bundleBuySell, } from './bundle.js';
|
|
85
85
|
// ============================================================================
|
|
86
|
+
// 外盘捆绑交易(DEX Bundle)
|
|
87
|
+
// ============================================================================
|
|
88
|
+
export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, } from './dex-bundle.js';
|
|
89
|
+
// ============================================================================
|
|
86
90
|
// 捆绑换手 (AASwap)
|
|
87
91
|
// ============================================================================
|
|
88
92
|
export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
|
|
@@ -116,6 +120,10 @@ export async function bundleBatchSwapSign(params) {
|
|
|
116
120
|
// ============================================================================
|
|
117
121
|
export { VolumeExecutor, createVolumeExecutor, makeVolume, singleRoundVolume, } from './volume.js';
|
|
118
122
|
// ============================================================================
|
|
123
|
+
// 外盘刷量/做市(DEX Volume)
|
|
124
|
+
// ============================================================================
|
|
125
|
+
export { DexVolumeExecutor, createDexVolumeExecutor, makeDexVolume, } from './dex-volume.js';
|
|
126
|
+
// ============================================================================
|
|
119
127
|
// 外盘交易 (DEX)
|
|
120
128
|
// ============================================================================
|
|
121
129
|
export { DexQuery, DexExecutor, createDexQuery, createDexExecutor, quoteOkbToToken, quoteTokenToOkb,
|
|
@@ -3,14 +3,36 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Wallet, ethers } from 'ethers';
|
|
5
5
|
import { AANonceMap, } from './types.js';
|
|
6
|
-
import { FLAP_PORTAL, ZERO_ADDRESS, } from './constants.js';
|
|
6
|
+
import { FLAP_PORTAL, ZERO_ADDRESS, MULTICALL3, } from './constants.js';
|
|
7
7
|
import { AAAccountManager, encodeExecute } from './aa-account.js';
|
|
8
8
|
import { encodeBuyCall, encodeSellCall, encodeApproveCall, PortalQuery, parseOkb, } from './portal-ops.js';
|
|
9
9
|
import { BundleExecutor } from './bundle.js';
|
|
10
10
|
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
11
|
+
const multicallIface = new ethers.Interface([
|
|
12
|
+
'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
|
|
13
|
+
]);
|
|
14
|
+
function chunkArray(arr, size) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const n = Math.max(1, Math.floor(size));
|
|
17
|
+
for (let i = 0; i < arr.length; i += n)
|
|
18
|
+
out.push(arr.slice(i, i + n));
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function encodeNativeDisperseViaMulticall3(params) {
|
|
22
|
+
const calls = params.to.map((target, idx) => ({
|
|
23
|
+
target,
|
|
24
|
+
allowFailure: false,
|
|
25
|
+
value: params.values[idx] ?? 0n,
|
|
26
|
+
callData: '0x',
|
|
27
|
+
}));
|
|
28
|
+
const totalValue = params.values.reduce((a, b) => a + (b ?? 0n), 0n);
|
|
29
|
+
const data = multicallIface.encodeFunctionData('aggregate3Value', [calls]);
|
|
30
|
+
return { totalValue, data };
|
|
31
|
+
}
|
|
11
32
|
function resolveProfitSettings(config) {
|
|
12
33
|
const extractProfit = config?.extractProfit !== false; // 默认 true(对齐 BSC)
|
|
13
|
-
|
|
34
|
+
// ✅ 对齐 BSC “捆绑换手模式”默认费率
|
|
35
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
|
|
14
36
|
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
15
37
|
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
16
38
|
return { extractProfit, profitBps, profitRecipient };
|
|
@@ -39,7 +61,7 @@ export class AAPortalSwapExecutor {
|
|
|
39
61
|
* AA 内盘单钱包换手签名
|
|
40
62
|
*/
|
|
41
63
|
async bundleSwapSign(params) {
|
|
42
|
-
const { tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
64
|
+
const { tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
43
65
|
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
44
66
|
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
45
67
|
const provider = this.aaManager.getProvider();
|
|
@@ -100,11 +122,18 @@ export class AAPortalSwapExecutor {
|
|
|
100
122
|
}
|
|
101
123
|
})();
|
|
102
124
|
const quotedSellOutWei = quoted;
|
|
103
|
-
|
|
125
|
+
// 3.1 利润提取(从卖出输出 OKB 里按比例刮取;但必须保证买入资金充足)
|
|
126
|
+
const requestedBuyWei = buyAmountOkb
|
|
104
127
|
? parseOkb(String(buyAmountOkb))
|
|
105
|
-
: (
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
: (quotedSellOutWei * BigInt(10000 - slippageBps)) / 10000n;
|
|
129
|
+
if (requestedBuyWei > quotedSellOutWei) {
|
|
130
|
+
throw new Error('AA 捆绑换手:buyAmountOkb 超过预估卖出输出(quotedSellOut)');
|
|
131
|
+
}
|
|
132
|
+
const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
133
|
+
const profitCap = quotedSellOutWei - requestedBuyWei;
|
|
134
|
+
const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
|
|
135
|
+
const finalBuyAmountWei = requestedBuyWei;
|
|
136
|
+
const hopCount = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
|
|
108
137
|
// 4. 构建 Ops
|
|
109
138
|
const outOps = [];
|
|
110
139
|
if (needApprove) {
|
|
@@ -144,6 +173,19 @@ export class AAPortalSwapExecutor {
|
|
|
144
173
|
});
|
|
145
174
|
outOps.push(signedProfit.userOp);
|
|
146
175
|
}
|
|
176
|
+
// ✅ 卖出所得 OKB 转给买方 AA(Sender),否则买方 UserOp 无法携带 value 执行 buy
|
|
177
|
+
if (sellerSender.toLowerCase() !== buyerSender.toLowerCase() && finalBuyAmountWei > 0n) {
|
|
178
|
+
const transferCallData = encodeExecute(buyerSender, finalBuyAmountWei, '0x');
|
|
179
|
+
const signedTransfer = await this.aaManager.buildUserOpWithState({
|
|
180
|
+
ownerWallet: sellerOwner,
|
|
181
|
+
sender: sellerSender,
|
|
182
|
+
nonce: nonceMap.next(sellerSender),
|
|
183
|
+
initCode: consumeInitCode(sellerSender),
|
|
184
|
+
callData: transferCallData,
|
|
185
|
+
signOnly: true,
|
|
186
|
+
});
|
|
187
|
+
outOps.push(signedTransfer.userOp);
|
|
188
|
+
}
|
|
147
189
|
// Buy op
|
|
148
190
|
const buySwapData = encodeBuyCall(tokenAddress, finalBuyAmountWei, 0n);
|
|
149
191
|
const buyCallData = encodeExecute(FLAP_PORTAL, finalBuyAmountWei, buySwapData);
|
|
@@ -195,6 +237,7 @@ export class AAPortalSwapExecutor {
|
|
|
195
237
|
profitRecipient,
|
|
196
238
|
profitWei: profitWei.toString(),
|
|
197
239
|
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
240
|
+
disperseHopCount: String(hopCount),
|
|
198
241
|
},
|
|
199
242
|
};
|
|
200
243
|
}
|
|
@@ -202,7 +245,7 @@ export class AAPortalSwapExecutor {
|
|
|
202
245
|
* AA 内盘批量换手签名 (一卖多买)
|
|
203
246
|
*/
|
|
204
247
|
async bundleBatchSwapSign(params) {
|
|
205
|
-
const { tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
248
|
+
const { tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
206
249
|
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
207
250
|
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
208
251
|
const provider = this.aaManager.getProvider();
|
|
@@ -272,7 +315,10 @@ export class AAPortalSwapExecutor {
|
|
|
272
315
|
signOnly: true,
|
|
273
316
|
});
|
|
274
317
|
outOps.push(signedSell.userOp);
|
|
275
|
-
//
|
|
318
|
+
// 先计算 buyAmountsWei(用于后续分发与校验)
|
|
319
|
+
const buyAmountsWei = buyAmountsOkb.map(a => parseOkb(a));
|
|
320
|
+
const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
|
|
321
|
+
// Profit op:用 previewSell/quoteExactInput 估算卖出输出,再按比例刮取(但必须保证分发/买入资金充足)
|
|
276
322
|
let quotedSellOutWei = 0n;
|
|
277
323
|
try {
|
|
278
324
|
quotedSellOutWei = await this.portalQuery.previewSell(tokenAddress, sellAmountWei);
|
|
@@ -280,7 +326,12 @@ export class AAPortalSwapExecutor {
|
|
|
280
326
|
catch {
|
|
281
327
|
quotedSellOutWei = await this.portalQuery.quoteExactInput(tokenAddress, ZERO_ADDRESS, sellAmountWei);
|
|
282
328
|
}
|
|
283
|
-
|
|
329
|
+
if (totalBuyWei > quotedSellOutWei) {
|
|
330
|
+
throw new Error('AA 批量换手:buyAmountsOkb 总和超过预估卖出输出(quotedSellOut)');
|
|
331
|
+
}
|
|
332
|
+
const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
333
|
+
const profitCap = quotedSellOutWei - totalBuyWei;
|
|
334
|
+
const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
|
|
284
335
|
if (extractProfit && profitWei > 0n) {
|
|
285
336
|
const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
|
|
286
337
|
const signedProfit = await this.aaManager.buildUserOpWithState({
|
|
@@ -293,8 +344,97 @@ export class AAPortalSwapExecutor {
|
|
|
293
344
|
});
|
|
294
345
|
outOps.push(signedProfit.userOp);
|
|
295
346
|
}
|
|
296
|
-
//
|
|
297
|
-
const
|
|
347
|
+
// ✅ 卖出所得 OKB 分发给多个买方 AA(Sender)(支持多跳)
|
|
348
|
+
const buyerSenders = buyerAis.map(ai => ai.sender);
|
|
349
|
+
const hopCountRaw = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
|
|
350
|
+
const hopCount = Math.min(hopCountRaw, buyerSenders.length); // 允许等于 buyerCount(最后一跳不再分发)
|
|
351
|
+
const maxPerOp = Math.max(1, Math.floor(Number(effectiveConfig.maxTransfersPerUserOpNative ?? 30)));
|
|
352
|
+
if (buyerSenders.length > 0 && totalBuyWei > 0n) {
|
|
353
|
+
if (hopCount <= 0) {
|
|
354
|
+
// 0 跳:卖方 AA(Sender) 直接用 multicall3 分发给所有买方 AA(Sender)
|
|
355
|
+
const items = buyerSenders.map((to, i) => ({ to, value: buyAmountsWei[i] ?? 0n })).filter(x => x.value > 0n);
|
|
356
|
+
const chunks = chunkArray(items, maxPerOp);
|
|
357
|
+
for (const ch of chunks) {
|
|
358
|
+
const { totalValue, data } = encodeNativeDisperseViaMulticall3({
|
|
359
|
+
to: ch.map(x => x.to),
|
|
360
|
+
values: ch.map(x => x.value),
|
|
361
|
+
});
|
|
362
|
+
const callData = encodeExecute(MULTICALL3, totalValue, data);
|
|
363
|
+
const signedDisperse = await this.aaManager.buildUserOpWithState({
|
|
364
|
+
ownerWallet: sellerOwner,
|
|
365
|
+
sender: sellerAi.sender,
|
|
366
|
+
nonce: nonceMap.next(sellerAi.sender),
|
|
367
|
+
initCode: consumeInitCode(sellerAi.sender),
|
|
368
|
+
callData,
|
|
369
|
+
signOnly: true,
|
|
370
|
+
});
|
|
371
|
+
outOps.push(signedDisperse.userOp);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
// 多跳:使用前 hopCount 个买方作为中转链(与 BSC 多跳语义独立,仅作用于 xlayer AA)
|
|
376
|
+
const hopSenders = buyerSenders.slice(0, hopCount);
|
|
377
|
+
const hopOwners = buyerOwners.slice(0, hopCount);
|
|
378
|
+
// 1) seller -> hop0(总额)
|
|
379
|
+
const hop0 = hopSenders[0];
|
|
380
|
+
const callData0 = encodeExecute(hop0, totalBuyWei, '0x');
|
|
381
|
+
const signedToHop0 = await this.aaManager.buildUserOpWithState({
|
|
382
|
+
ownerWallet: sellerOwner,
|
|
383
|
+
sender: sellerAi.sender,
|
|
384
|
+
nonce: nonceMap.next(sellerAi.sender),
|
|
385
|
+
initCode: consumeInitCode(sellerAi.sender),
|
|
386
|
+
callData: callData0,
|
|
387
|
+
signOnly: true,
|
|
388
|
+
});
|
|
389
|
+
outOps.push(signedToHop0.userOp);
|
|
390
|
+
// 2) hop j -> hop j+1(扣掉自己那份)
|
|
391
|
+
let prefixKept = 0n;
|
|
392
|
+
for (let j = 0; j < hopSenders.length - 1; j++) {
|
|
393
|
+
const sender = hopSenders[j];
|
|
394
|
+
const next = hopSenders[j + 1];
|
|
395
|
+
const keep = buyAmountsWei[j] ?? 0n;
|
|
396
|
+
prefixKept += keep;
|
|
397
|
+
const remaining = totalBuyWei - prefixKept;
|
|
398
|
+
if (remaining <= 0n)
|
|
399
|
+
break;
|
|
400
|
+
const callData = encodeExecute(next, remaining, '0x');
|
|
401
|
+
const signedHop = await this.aaManager.buildUserOpWithState({
|
|
402
|
+
ownerWallet: hopOwners[j],
|
|
403
|
+
sender,
|
|
404
|
+
nonce: nonceMap.next(sender),
|
|
405
|
+
initCode: consumeInitCode(sender),
|
|
406
|
+
callData,
|
|
407
|
+
signOnly: true,
|
|
408
|
+
});
|
|
409
|
+
outOps.push(signedHop.userOp);
|
|
410
|
+
}
|
|
411
|
+
// 3) lastHop 分发给剩余买方(不含 hop 自己)
|
|
412
|
+
const lastHopIdx = hopSenders.length - 1;
|
|
413
|
+
const lastHopSender = hopSenders[lastHopIdx];
|
|
414
|
+
const lastHopOwner = hopOwners[lastHopIdx];
|
|
415
|
+
const rest = buyerSenders.slice(hopSenders.length);
|
|
416
|
+
const restAmounts = buyAmountsWei.slice(hopSenders.length);
|
|
417
|
+
const restItems = rest.map((to, i) => ({ to, value: restAmounts[i] ?? 0n })).filter(x => x.value > 0n);
|
|
418
|
+
const restChunks = chunkArray(restItems, maxPerOp);
|
|
419
|
+
for (const ch of restChunks) {
|
|
420
|
+
const { totalValue, data } = encodeNativeDisperseViaMulticall3({
|
|
421
|
+
to: ch.map(x => x.to),
|
|
422
|
+
values: ch.map(x => x.value),
|
|
423
|
+
});
|
|
424
|
+
const callData = encodeExecute(MULTICALL3, totalValue, data);
|
|
425
|
+
const signedDisperse = await this.aaManager.buildUserOpWithState({
|
|
426
|
+
ownerWallet: lastHopOwner,
|
|
427
|
+
sender: lastHopSender,
|
|
428
|
+
nonce: nonceMap.next(lastHopSender),
|
|
429
|
+
initCode: consumeInitCode(lastHopSender),
|
|
430
|
+
callData,
|
|
431
|
+
signOnly: true,
|
|
432
|
+
});
|
|
433
|
+
outOps.push(signedDisperse.userOp);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Batch Buy ops(分发后再执行,确保每个买方 sender 先有 OKB)
|
|
298
438
|
for (let i = 0; i < buyerOwners.length; i++) {
|
|
299
439
|
const ai = buyerAis[i];
|
|
300
440
|
const buyWei = buyAmountsWei[i];
|
|
@@ -348,6 +488,7 @@ export class AAPortalSwapExecutor {
|
|
|
348
488
|
profitRecipient,
|
|
349
489
|
profitWei: profitWei.toString(),
|
|
350
490
|
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
491
|
+
disperseHopCount: String(hopCount),
|
|
351
492
|
},
|
|
352
493
|
};
|
|
353
494
|
}
|
package/dist/xlayer/types.d.ts
CHANGED
|
@@ -234,6 +234,14 @@ export interface BundleSwapParams {
|
|
|
234
234
|
buyAmountOkb?: string;
|
|
235
235
|
/** 预估卖出滑点(基点),默认 100 = 1%(用于 buyAmountOkb 未传入时) */
|
|
236
236
|
slippageBps?: number;
|
|
237
|
+
/**
|
|
238
|
+
* ✅ 转账多跳数(AA 专用,且与 BSC 的“多跳”逻辑相互独立)
|
|
239
|
+
*
|
|
240
|
+
* 用途:当卖出与买入不是同一个 AA(Sender) 时,需要把卖出得到的 OKB 先转到买方 AA(Sender) 才能执行 buy。
|
|
241
|
+
* - 0:卖方 AA(Sender) 直接转给买方 AA(Sender)
|
|
242
|
+
* - >0:按“多跳”方式转账(实现位于 xlayer/*,不会影响 BSC bundle/merkle)
|
|
243
|
+
*/
|
|
244
|
+
disperseHopCount?: number;
|
|
237
245
|
/** 配置覆盖 */
|
|
238
246
|
config?: Partial<XLayerConfig>;
|
|
239
247
|
}
|
|
@@ -263,6 +271,8 @@ export interface BundleSwapResult {
|
|
|
263
271
|
profitWei?: string;
|
|
264
272
|
/** 预估卖出输出(OKB wei) */
|
|
265
273
|
quotedSellOutWei?: string;
|
|
274
|
+
/** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
|
|
275
|
+
disperseHopCount?: string;
|
|
266
276
|
};
|
|
267
277
|
}
|
|
268
278
|
/**
|
|
@@ -312,6 +322,8 @@ export interface BundleSwapSignResult {
|
|
|
312
322
|
profitWei?: string;
|
|
313
323
|
/** 预估卖出输出(OKB wei) */
|
|
314
324
|
quotedSellOutWei?: string;
|
|
325
|
+
/** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
|
|
326
|
+
disperseHopCount?: string;
|
|
315
327
|
};
|
|
316
328
|
}
|
|
317
329
|
/**
|
|
@@ -336,6 +348,14 @@ export interface BundleBatchSwapParams {
|
|
|
336
348
|
sellAmount?: string;
|
|
337
349
|
/** 卖出比例(0-100,默认 100) */
|
|
338
350
|
sellPercent?: number;
|
|
351
|
+
/**
|
|
352
|
+
* ✅ 转账多跳数(AA 专用)
|
|
353
|
+
*
|
|
354
|
+
* 用途:卖出所得 OKB 需要在同一笔 handleOps 内分发给多个买方 AA(Sender)。
|
|
355
|
+
* - 0:卖方 AA(Sender) 直接分发给所有买方 AA(Sender)
|
|
356
|
+
* - >0:按“多跳”方式分发(实现位于 xlayer/*,不会影响 BSC bundle/merkle)
|
|
357
|
+
*/
|
|
358
|
+
disperseHopCount?: number;
|
|
339
359
|
/** 配置覆盖 */
|
|
340
360
|
config?: Partial<XLayerConfig>;
|
|
341
361
|
}
|
|
@@ -360,6 +380,8 @@ export interface BundleBatchSwapResult {
|
|
|
360
380
|
profitWei?: string;
|
|
361
381
|
/** 预估卖出输出(OKB wei) */
|
|
362
382
|
quotedSellOutWei?: string;
|
|
383
|
+
/** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
|
|
384
|
+
disperseHopCount?: string;
|
|
363
385
|
};
|
|
364
386
|
}
|
|
365
387
|
export interface BundleBatchSwapSignParams extends BundleBatchSwapParams {
|
|
@@ -392,6 +414,8 @@ export interface BundleBatchSwapSignResult {
|
|
|
392
414
|
profitWei?: string;
|
|
393
415
|
/** 预估卖出输出(OKB wei) */
|
|
394
416
|
quotedSellOutWei?: string;
|
|
417
|
+
/** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
|
|
418
|
+
disperseHopCount?: string;
|
|
395
419
|
};
|
|
396
420
|
}
|
|
397
421
|
/**
|
|
@@ -458,6 +482,16 @@ export interface BundleSellResult {
|
|
|
458
482
|
sellResult: HandleOpsResult;
|
|
459
483
|
/** 归集交易结果(如果 withdrawToOwner) */
|
|
460
484
|
withdrawResult?: HandleOpsResult;
|
|
485
|
+
/**
|
|
486
|
+
* 利润汇总(如果 withdrawToOwner=true 且实际归集金额>0)
|
|
487
|
+
* - 注意:AA 的 handleOps tx.value 永远是 0;利润是“从 AA Sender 余额里拆分转走”的金额
|
|
488
|
+
*/
|
|
489
|
+
profit?: {
|
|
490
|
+
extractProfit: boolean;
|
|
491
|
+
profitBps: number;
|
|
492
|
+
profitRecipient: string;
|
|
493
|
+
totalProfitWei: string;
|
|
494
|
+
};
|
|
461
495
|
}
|
|
462
496
|
/**
|
|
463
497
|
* 捆绑买卖结果
|
|
@@ -473,6 +507,13 @@ export interface BundleBuySellResult {
|
|
|
473
507
|
withdrawResult?: HandleOpsResult;
|
|
474
508
|
/** 各 sender 的最终 OKB 余额 */
|
|
475
509
|
finalBalances: Map<string, bigint>;
|
|
510
|
+
/** 利润汇总(如果 withdrawToOwner=true 且实际归集金额>0) */
|
|
511
|
+
profit?: {
|
|
512
|
+
extractProfit: boolean;
|
|
513
|
+
profitBps: number;
|
|
514
|
+
profitRecipient: string;
|
|
515
|
+
totalProfitWei: string;
|
|
516
|
+
};
|
|
476
517
|
}
|
|
477
518
|
/**
|
|
478
519
|
* 刷量结果
|