four-flap-meme-sdk 1.6.1 → 1.6.4
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 +29 -27
- package/dist/xlayer/dex-bundle-swap.d.ts +2 -0
- package/dist/xlayer/dex-bundle-swap.js +217 -126
- package/dist/xlayer/dex-bundle.js +6 -19
- package/dist/xlayer/dex-buy-first.js +8 -20
- package/dist/xlayer/dex.d.ts +3 -1
- package/dist/xlayer/dex.js +48 -30
- package/dist/xlayer/portal-bundle-swap.d.ts +1 -0
- package/dist/xlayer/portal-bundle-swap.js +147 -58
- package/dist/xlayer/portal-buy-first.js +6 -19
- package/dist/xlayer/portal-ops.d.ts +18 -0
- package/dist/xlayer/portal-ops.js +38 -0
- package/dist/xlayer/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/xlayer/bundle.js
CHANGED
|
@@ -1193,26 +1193,7 @@ export class BundleExecutor {
|
|
|
1193
1193
|
gasLimit: effConfig.gasLimit,
|
|
1194
1194
|
gasPrice: effConfig.minGasPriceGwei ? ethers.parseUnits(effConfig.minGasPriceGwei.toString(), 'gwei') : undefined
|
|
1195
1195
|
}) ?? undefined;
|
|
1196
|
-
|
|
1197
|
-
const totalProfitForTail = nativeBuyProfitAmount + totalSellWithdrawProfitWei;
|
|
1198
|
-
if (totalProfitForTail > 0n) {
|
|
1199
|
-
const provider = this.aaManager.getProvider();
|
|
1200
|
-
const feeData = await provider.getFeeData();
|
|
1201
|
-
const nonce = await provider.getTransactionCount(bundlerSigner.address, 'pending');
|
|
1202
|
-
const tailTx = await bundlerSigner.signTransaction({
|
|
1203
|
-
to: profitSettingsBuySell.profitRecipient,
|
|
1204
|
-
value: totalProfitForTail,
|
|
1205
|
-
data: '0x',
|
|
1206
|
-
nonce,
|
|
1207
|
-
gasLimit: this.config.profitTailGasLimit ?? 21000n,
|
|
1208
|
-
gasPrice: feeData.gasPrice ?? 100000000n,
|
|
1209
|
-
chainId: this.config.chainId ?? 196,
|
|
1210
|
-
type: 0,
|
|
1211
|
-
});
|
|
1212
|
-
const tx = await provider.broadcastTransaction(tailTx);
|
|
1213
|
-
await tx.wait();
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1196
|
+
// ✅ 利润已在 withdraw UserOp 内部通过 executeBatch 刮取,无需单独广播 tail tx
|
|
1216
1197
|
}
|
|
1217
1198
|
}
|
|
1218
1199
|
// 最终余额
|
|
@@ -1721,7 +1702,7 @@ export class BundleExecutor {
|
|
|
1721
1702
|
signedTransactions.push(signedDexTx);
|
|
1722
1703
|
currentNonce++;
|
|
1723
1704
|
}
|
|
1724
|
-
// --- 5. 利润提取 (
|
|
1705
|
+
// --- 5. 利润提取 (AA 内部 UserOp,不再使用 Tail Transaction) ---
|
|
1725
1706
|
let totalProfitWei = 0n;
|
|
1726
1707
|
if (profitSettings.extractProfit) {
|
|
1727
1708
|
totalProfitWei += calculateProfitWei(totalCurveBuyWei, profitSettings.profitBps);
|
|
@@ -1729,13 +1710,34 @@ export class BundleExecutor {
|
|
|
1729
1710
|
totalProfitWei += calculateProfitWei(totalDexBuyWei, profitSettings.profitBps);
|
|
1730
1711
|
}
|
|
1731
1712
|
if (totalProfitWei > 0n) {
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1713
|
+
// ✅ 使用 Payer AA 账户发起利润转账 UserOp(不再使用 Tail Transaction)
|
|
1714
|
+
const profitCallData = encodeExecute(profitSettings.profitRecipient, totalProfitWei, '0x');
|
|
1715
|
+
const payerInitCode = payerAccount.deployed ? '0x' : aaManager.generateInitCode(payerWallet.address);
|
|
1716
|
+
const { userOp: profitOp, prefundWei: profitPrefund } = await aaManager.buildUserOpWithFixedGas({
|
|
1717
|
+
ownerWallet: payerWallet,
|
|
1718
|
+
sender: payerAccount.sender,
|
|
1719
|
+
nonce: nonceMap.next(payerAccount.sender),
|
|
1720
|
+
initCode: payerInitCode,
|
|
1721
|
+
callData: profitCallData,
|
|
1722
|
+
deployed: payerAccount.deployed,
|
|
1723
|
+
fixedGas: {
|
|
1724
|
+
...(effConfig.fixedGas ?? {}),
|
|
1725
|
+
callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
|
|
1726
|
+
},
|
|
1727
|
+
});
|
|
1728
|
+
// 确保 Payer AA 账户有足够余额支付利润转账
|
|
1729
|
+
await aaManager.ensureSenderBalance(payerWallet, payerAccount.sender, totalProfitWei + profitPrefund + parseOkb('0.0001'), 'profit-transfer-fund');
|
|
1730
|
+
const signedProfitOp = await aaManager.signUserOp(profitOp, payerWallet);
|
|
1731
|
+
// 签名利润 handleOps 交易
|
|
1732
|
+
const signedProfitHandleOpsTx = await this.signHandleOpsTx({
|
|
1733
|
+
ops: [signedProfitOp.userOp],
|
|
1734
|
+
payerWallet: payerWallet,
|
|
1735
|
+
beneficiary: params.beneficiary ?? payerWallet.address,
|
|
1736
|
+
nonce: currentNonce,
|
|
1737
|
+
gasLimit: effConfig.gasLimit,
|
|
1738
|
+
gasPrice: effConfig.minGasPriceGwei ? ethers.parseUnits(effConfig.minGasPriceGwei.toString(), 'gwei') : undefined
|
|
1737
1739
|
});
|
|
1738
|
-
signedTransactions.push(
|
|
1740
|
+
signedTransactions.push(signedProfitHandleOpsTx);
|
|
1739
1741
|
}
|
|
1740
1742
|
}
|
|
1741
1743
|
return {
|
|
@@ -5,6 +5,7 @@ import { XLayerConfig, BundleSwapSignParams, BundleSwapSignResult, BundleBatchSw
|
|
|
5
5
|
/**
|
|
6
6
|
* XLayer AA 外盘换手执行器
|
|
7
7
|
* ✅ 支持 V2 和 V3 交易
|
|
8
|
+
* ✅ 支持 ERC20 稳定币(USDT/USDC/USDT0)
|
|
8
9
|
*/
|
|
9
10
|
export declare class AADexSwapExecutor {
|
|
10
11
|
private aaManager;
|
|
@@ -35,6 +36,7 @@ export declare class AADexSwapExecutor {
|
|
|
35
36
|
/**
|
|
36
37
|
* AA 外盘批量换手签名
|
|
37
38
|
* ✅ 支持 V2 和 V3 交易
|
|
39
|
+
* ✅ 支持 ERC20 稳定币(USDT/USDC/USDT0)
|
|
38
40
|
*/
|
|
39
41
|
bundleBatchSwapSign(params: BundleBatchSwapSignParams & {
|
|
40
42
|
skipApprovalCheck?: boolean;
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
import { Wallet, ethers } from 'ethers';
|
|
5
5
|
import { AANonceMap, } from './types.js';
|
|
6
6
|
import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, POTATOSWAP_V3_FACTORY, WOKB, MULTICALL3, } from './constants.js';
|
|
7
|
-
import { AAAccountManager, encodeExecute } from './aa-account.js';
|
|
7
|
+
import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
|
|
8
8
|
import { encodeApproveCall, lpFeeProfileToV3Fee, } from './portal-ops.js';
|
|
9
|
-
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, } from './dex.js';
|
|
9
|
+
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee, } from './dex.js';
|
|
10
10
|
import { BundleExecutor } from './bundle.js';
|
|
11
|
-
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
11
|
+
import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
|
|
12
12
|
const multicallIface = new ethers.Interface([
|
|
13
13
|
'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
|
|
14
14
|
]);
|
|
@@ -24,7 +24,7 @@ const V3_POOL_ABI = [
|
|
|
24
24
|
'function token1() view returns (address)',
|
|
25
25
|
];
|
|
26
26
|
const V3_FEE_DENOMINATOR = 1000000n;
|
|
27
|
-
|
|
27
|
+
// ✅ ZERO_ADDRESS 已从 ../utils/constants.js 导入
|
|
28
28
|
/**
|
|
29
29
|
* 使用 V3 Pool 的 slot0 获取 Token → WOKB 的报价
|
|
30
30
|
* 这是一个现货价计算,比 V3 Quoter 更简单但精度稍低
|
|
@@ -103,9 +103,16 @@ function calculateProfitWei(amountWei, profitBps) {
|
|
|
103
103
|
return 0n;
|
|
104
104
|
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* ✅ 判断是否使用原生代币(OKB)
|
|
108
|
+
*/
|
|
109
|
+
function isNativeToken(quoteToken) {
|
|
110
|
+
return !quoteToken || quoteToken === ZERO_ADDRESS || quoteToken.toLowerCase() === WOKB.toLowerCase();
|
|
111
|
+
}
|
|
106
112
|
/**
|
|
107
113
|
* XLayer AA 外盘换手执行器
|
|
108
114
|
* ✅ 支持 V2 和 V3 交易
|
|
115
|
+
* ✅ 支持 ERC20 稳定币(USDT/USDC/USDT0)
|
|
109
116
|
*/
|
|
110
117
|
export class AADexSwapExecutor {
|
|
111
118
|
constructor(config = {}) {
|
|
@@ -149,9 +156,12 @@ export class AADexSwapExecutor {
|
|
|
149
156
|
* ✅ 支持 V2 和 V3 交易
|
|
150
157
|
*/
|
|
151
158
|
async bundleSwapSign(params) {
|
|
152
|
-
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0,
|
|
159
|
+
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0, quoteToken, quoteTokenDecimals = 6, // XLayer USDT/USDC/USDT0 都是 6 位精度
|
|
160
|
+
} = params;
|
|
153
161
|
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
154
162
|
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
163
|
+
// ✅ ERC20 稳定币支持:判断是否使用原生代币
|
|
164
|
+
const useNativeToken = isNativeToken(quoteToken);
|
|
155
165
|
const isV3Trade = this.isV3(tradeType);
|
|
156
166
|
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn, tradeType });
|
|
157
167
|
const provider = this.aaManager.getProvider();
|
|
@@ -253,27 +263,34 @@ export class AADexSwapExecutor {
|
|
|
253
263
|
const signedApprove = await this.aaManager.signUserOp(userOp, sellerOwner);
|
|
254
264
|
outOps.push(signedApprove.userOp);
|
|
255
265
|
}
|
|
256
|
-
// Sell op - ✅ 支持 V2 和 V3
|
|
266
|
+
// Sell op - ✅ 支持 V2 和 V3,支持 ERC20 稳定币
|
|
257
267
|
const deadline = this.getDexDeadline();
|
|
258
268
|
let sellSwapData;
|
|
259
|
-
if (
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
if (useNativeToken) {
|
|
270
|
+
// ✅ 原生代币模式:Token → OKB
|
|
271
|
+
if (isV3Trade) {
|
|
272
|
+
// V3: 使用 multicall(exactInputSingle + unwrapWETH9)
|
|
273
|
+
sellSwapData = encodeSwapExactTokensForETHV3({
|
|
274
|
+
tokenIn: tokenAddress,
|
|
275
|
+
tokenOut: WOKB,
|
|
276
|
+
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
277
|
+
recipient: sellerSender,
|
|
278
|
+
deadline,
|
|
279
|
+
amountIn: sellAmountWei,
|
|
280
|
+
amountOutMinimum: 0n,
|
|
281
|
+
sqrtPriceLimitX96: 0n,
|
|
282
|
+
unwrapRecipient: sellerSender,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// V2: Token → OKB
|
|
287
|
+
sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellAmountWei, 0n, [tokenAddress, WOKB], sellerSender, deadline);
|
|
288
|
+
}
|
|
273
289
|
}
|
|
274
290
|
else {
|
|
275
|
-
//
|
|
276
|
-
|
|
291
|
+
// ✅ ERC20 模式:Token → quoteToken (USDT/USDC)
|
|
292
|
+
// V2/V3 都使用 Token → Token 路径
|
|
293
|
+
sellSwapData = encodeSwapExactTokensForTokensSupportingFee(sellAmountWei, 0n, [tokenAddress, quoteToken], sellerSender, deadline);
|
|
277
294
|
}
|
|
278
295
|
const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
|
|
279
296
|
const signedSell = await this.aaManager.buildUserOpWithState({
|
|
@@ -285,10 +302,32 @@ export class AADexSwapExecutor {
|
|
|
285
302
|
signOnly: true,
|
|
286
303
|
});
|
|
287
304
|
outOps.push(signedSell.userOp);
|
|
288
|
-
//
|
|
289
|
-
//
|
|
305
|
+
// ✅ 卖出所得资金转给买方 AA(Sender) + 利润刮取(AA 内部)
|
|
306
|
+
// 原生代币模式:直接转 OKB;ERC20 模式:调用 ERC20 transfer
|
|
290
307
|
if (sellerSender.toLowerCase() !== buyerSender.toLowerCase() && finalBuyAmountWei > 0n) {
|
|
291
|
-
|
|
308
|
+
let transferCallData;
|
|
309
|
+
if (useNativeToken) {
|
|
310
|
+
// ✅ 原生代币模式:使用 executeBatch 同时转账给买方和利润接收者
|
|
311
|
+
if (extractProfit && profitWei > 0n) {
|
|
312
|
+
transferCallData = encodeExecuteBatch([buyerSender, profitRecipient], [finalBuyAmountWei, profitWei], ['0x', '0x']);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
transferCallData = encodeExecute(buyerSender, finalBuyAmountWei, '0x');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// ✅ ERC20 模式:使用 ERC20 transfer 批量转账
|
|
320
|
+
const erc20Iface = new ethers.Interface(['function transfer(address to, uint256 amount) returns (bool)']);
|
|
321
|
+
if (extractProfit && profitWei > 0n) {
|
|
322
|
+
const transferToBuyer = erc20Iface.encodeFunctionData('transfer', [buyerSender, finalBuyAmountWei]);
|
|
323
|
+
const transferToProfit = erc20Iface.encodeFunctionData('transfer', [profitRecipient, profitWei]);
|
|
324
|
+
transferCallData = encodeExecuteBatch([quoteToken, quoteToken], [0n, 0n], [transferToBuyer, transferToProfit]);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
const transferData = erc20Iface.encodeFunctionData('transfer', [buyerSender, finalBuyAmountWei]);
|
|
328
|
+
transferCallData = encodeExecute(quoteToken, 0n, transferData);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
292
331
|
const signedTransfer = await this.aaManager.buildUserOpWithState({
|
|
293
332
|
ownerWallet: sellerOwner,
|
|
294
333
|
sender: sellerSender,
|
|
@@ -299,26 +338,34 @@ export class AADexSwapExecutor {
|
|
|
299
338
|
});
|
|
300
339
|
outOps.push(signedTransfer.userOp);
|
|
301
340
|
}
|
|
302
|
-
// Buy op - ✅ 支持 V2
|
|
303
|
-
let
|
|
304
|
-
if (
|
|
305
|
-
//
|
|
306
|
-
buySwapData
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
341
|
+
// Buy op - ✅ 支持 V2/V3,支持 ERC20 稳定币
|
|
342
|
+
let buyCallData;
|
|
343
|
+
if (useNativeToken) {
|
|
344
|
+
// ✅ 原生代币模式:OKB → Token
|
|
345
|
+
let buySwapData;
|
|
346
|
+
if (isV3Trade) {
|
|
347
|
+
buySwapData = encodeSwapExactETHForTokensV3({
|
|
348
|
+
tokenIn: WOKB,
|
|
349
|
+
tokenOut: tokenAddress,
|
|
350
|
+
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
351
|
+
recipient: buyerSender,
|
|
352
|
+
deadline,
|
|
353
|
+
amountIn: finalBuyAmountWei,
|
|
354
|
+
amountOutMinimum: 0n,
|
|
355
|
+
sqrtPriceLimitX96: 0n,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], buyerSender, deadline);
|
|
360
|
+
}
|
|
361
|
+
buyCallData = encodeExecute(effectiveRouter, finalBuyAmountWei, buySwapData);
|
|
316
362
|
}
|
|
317
363
|
else {
|
|
318
|
-
//
|
|
319
|
-
|
|
364
|
+
// ✅ ERC20 模式:quoteToken → Token(需要先 approve)
|
|
365
|
+
const approveData = encodeApproveCall(effectiveRouter, finalBuyAmountWei);
|
|
366
|
+
const swapData = encodeSwapExactTokensForTokensSupportingFee(finalBuyAmountWei, 0n, [quoteToken, tokenAddress], buyerSender, deadline);
|
|
367
|
+
buyCallData = encodeExecuteBatch([quoteToken, effectiveRouter], [0n, 0n], [approveData, swapData]);
|
|
320
368
|
}
|
|
321
|
-
const buyCallData = encodeExecute(effectiveRouter, finalBuyAmountWei, buySwapData);
|
|
322
369
|
const signedBuy = await this.aaManager.buildUserOpWithState({
|
|
323
370
|
ownerWallet: buyerOwner,
|
|
324
371
|
sender: buyerSender,
|
|
@@ -334,31 +381,8 @@ export class AADexSwapExecutor {
|
|
|
334
381
|
beneficiary,
|
|
335
382
|
nonce: payerStartNonce,
|
|
336
383
|
});
|
|
384
|
+
// ✅ 利润已在 AA 内部通过 executeBatch 刮取,不需要 Tail Tx
|
|
337
385
|
const signedTransactions = [signedHandleOps];
|
|
338
|
-
let currentNonce = Number(ethers.Transaction.from(signedHandleOps).nonce) + 1;
|
|
339
|
-
// 1. 处理 Route Tail Tx (如果存在)
|
|
340
|
-
if (routeAddress) {
|
|
341
|
-
const tailTx = await payerWallet.signTransaction({
|
|
342
|
-
to: routeAddress,
|
|
343
|
-
value: 0n,
|
|
344
|
-
nonce: currentNonce++,
|
|
345
|
-
gasLimit: 21000n,
|
|
346
|
-
gasPrice: ethers.Transaction.from(signedHandleOps).gasPrice,
|
|
347
|
-
chainId: this.config.chainId ?? 196,
|
|
348
|
-
type: 0,
|
|
349
|
-
});
|
|
350
|
-
signedTransactions.push(tailTx);
|
|
351
|
-
}
|
|
352
|
-
// 2. ✅ 处理 Profit Tail Tx (修复:补全缺失的利润签名)
|
|
353
|
-
if (extractProfit && profitWei > 0n) {
|
|
354
|
-
const tailTx = await this.bundleExecutor['signProfitTransaction']({
|
|
355
|
-
payerWallet,
|
|
356
|
-
profitWei,
|
|
357
|
-
recipient: profitRecipient,
|
|
358
|
-
nonce: currentNonce++,
|
|
359
|
-
});
|
|
360
|
-
signedTransactions.push(tailTx);
|
|
361
|
-
}
|
|
362
386
|
return {
|
|
363
387
|
signedTransactions,
|
|
364
388
|
metadata: {
|
|
@@ -384,11 +408,15 @@ export class AADexSwapExecutor {
|
|
|
384
408
|
/**
|
|
385
409
|
* AA 外盘批量换手签名
|
|
386
410
|
* ✅ 支持 V2 和 V3 交易
|
|
411
|
+
* ✅ 支持 ERC20 稳定币(USDT/USDC/USDT0)
|
|
387
412
|
*/
|
|
388
413
|
async bundleBatchSwapSign(params) {
|
|
389
|
-
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0,
|
|
414
|
+
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0, quoteToken, quoteTokenDecimals = 6, // XLayer USDT/USDC/USDT0 都是 6 位精度
|
|
415
|
+
} = params;
|
|
390
416
|
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
391
417
|
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
418
|
+
// ✅ ERC20 稳定币支持:判断是否使用原生代币
|
|
419
|
+
const useNativeToken = isNativeToken(quoteToken);
|
|
392
420
|
const isV3Trade = this.isV3(tradeType);
|
|
393
421
|
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn, tradeType });
|
|
394
422
|
const provider = this.aaManager.getProvider();
|
|
@@ -446,27 +474,31 @@ export class AADexSwapExecutor {
|
|
|
446
474
|
const signedApprove = await this.aaManager.signUserOp(userOp, sellerOwner);
|
|
447
475
|
outOps.push(signedApprove.userOp);
|
|
448
476
|
}
|
|
449
|
-
// Sell op - ✅ 支持 V2
|
|
477
|
+
// Sell op - ✅ 支持 V2/V3,支持 ERC20 稳定币
|
|
450
478
|
const deadline = this.getDexDeadline();
|
|
451
479
|
let sellSwapData;
|
|
452
|
-
if (
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
480
|
+
if (useNativeToken) {
|
|
481
|
+
// ✅ 原生代币模式:Token → OKB
|
|
482
|
+
if (isV3Trade) {
|
|
483
|
+
sellSwapData = encodeSwapExactTokensForETHV3({
|
|
484
|
+
tokenIn: tokenAddress,
|
|
485
|
+
tokenOut: WOKB,
|
|
486
|
+
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
487
|
+
recipient: sellerAi.sender,
|
|
488
|
+
deadline,
|
|
489
|
+
amountIn: sellAmountWei,
|
|
490
|
+
amountOutMinimum: 0n,
|
|
491
|
+
sqrtPriceLimitX96: 0n,
|
|
492
|
+
unwrapRecipient: sellerAi.sender,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellAmountWei, 0n, [tokenAddress, WOKB], sellerAi.sender, deadline);
|
|
497
|
+
}
|
|
466
498
|
}
|
|
467
499
|
else {
|
|
468
|
-
//
|
|
469
|
-
sellSwapData =
|
|
500
|
+
// ✅ ERC20 模式:Token → quoteToken (USDT/USDC)
|
|
501
|
+
sellSwapData = encodeSwapExactTokensForTokensSupportingFee(sellAmountWei, 0n, [tokenAddress, quoteToken], sellerAi.sender, deadline);
|
|
470
502
|
}
|
|
471
503
|
const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
|
|
472
504
|
const signedSell = await this.aaManager.buildUserOpWithState({
|
|
@@ -479,7 +511,10 @@ export class AADexSwapExecutor {
|
|
|
479
511
|
});
|
|
480
512
|
outOps.push(signedSell.userOp);
|
|
481
513
|
// 先计算 buyAmountsWei(用于后续分发与校验)
|
|
482
|
-
|
|
514
|
+
// 原生代币模式:使用 parseEther;ERC20 模式:使用 parseUnits
|
|
515
|
+
const buyAmountsWei = useNativeToken
|
|
516
|
+
? buyAmountsOkb.map(a => ethers.parseEther(a))
|
|
517
|
+
: buyAmountsOkb.map(a => ethers.parseUnits(a, quoteTokenDecimals));
|
|
483
518
|
const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
|
|
484
519
|
// Profit op:估算卖出输出,按比例刮取(但必须保证分发/买入资金充足)
|
|
485
520
|
// ✅ V3 模式:优先使用 slot0 报价
|
|
@@ -511,22 +546,41 @@ export class AADexSwapExecutor {
|
|
|
511
546
|
const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
512
547
|
const profitCap = quotedSellOutWei > totalBuyWei ? (quotedSellOutWei - totalBuyWei) : 0n;
|
|
513
548
|
const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
|
|
514
|
-
//
|
|
515
|
-
// ✅
|
|
549
|
+
// ✅ 利润在分发阶段通过 AA 内部刮取
|
|
550
|
+
// ✅ 卖出所得资金分发给多个买方 AA(Sender)(支持多跳)+ 利润刮取
|
|
551
|
+
// 原生代币模式:使用 Multicall3 批量转账
|
|
552
|
+
// ERC20 模式:使用 ERC20 transfer 批量转账
|
|
516
553
|
const buyerSenders = buyerAis.map(ai => ai.sender);
|
|
517
554
|
const hopCountRaw = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
|
|
518
555
|
const hopCount = Math.min(hopCountRaw, buyerSenders.length);
|
|
519
556
|
const maxPerOp = Math.max(1, Math.floor(Number(effectiveConfig.maxTransfersPerUserOpNative ?? 30)));
|
|
557
|
+
const erc20Iface = new ethers.Interface(['function transfer(address to, uint256 amount) returns (bool)']);
|
|
520
558
|
if (buyerSenders.length > 0 && totalBuyWei > 0n) {
|
|
521
559
|
if (hopCount <= 0) {
|
|
560
|
+
// ✅ 直接分发模式:将利润接收者加入分发列表(AA 内部刮取)
|
|
522
561
|
const items = buyerSenders.map((to, i) => ({ to, value: buyAmountsWei[i] ?? 0n })).filter(x => x.value > 0n);
|
|
562
|
+
// 添加利润接收者到分发列表
|
|
563
|
+
if (extractProfit && profitWei > 0n) {
|
|
564
|
+
items.push({ to: profitRecipient, value: profitWei });
|
|
565
|
+
}
|
|
523
566
|
const chunks = chunkArray(items, maxPerOp);
|
|
524
567
|
for (const ch of chunks) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
568
|
+
let callData;
|
|
569
|
+
if (useNativeToken) {
|
|
570
|
+
// ✅ 原生代币模式:使用 Multicall3 批量转账
|
|
571
|
+
const { totalValue, data } = encodeNativeDisperseViaMulticall3({
|
|
572
|
+
to: ch.map(x => x.to),
|
|
573
|
+
values: ch.map(x => x.value),
|
|
574
|
+
});
|
|
575
|
+
callData = encodeExecute(MULTICALL3, totalValue, data);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
// ✅ ERC20 模式:使用 executeBatch 批量调用 ERC20 transfer
|
|
579
|
+
const targets = ch.map(() => quoteToken);
|
|
580
|
+
const values = ch.map(() => 0n);
|
|
581
|
+
const datas = ch.map(x => erc20Iface.encodeFunctionData('transfer', [x.to, x.value]));
|
|
582
|
+
callData = encodeExecuteBatch(targets, values, datas);
|
|
583
|
+
}
|
|
530
584
|
const signedDisperse = await this.aaManager.buildUserOpWithState({
|
|
531
585
|
ownerWallet: sellerOwner,
|
|
532
586
|
sender: sellerAi.sender,
|
|
@@ -539,10 +593,32 @@ export class AADexSwapExecutor {
|
|
|
539
593
|
}
|
|
540
594
|
}
|
|
541
595
|
else {
|
|
596
|
+
// ✅ 多跳模式:在第一跳时同时将利润刮取到 profitRecipient
|
|
542
597
|
const hopSenders = buyerSenders.slice(0, hopCount);
|
|
543
598
|
const hopOwners = buyerOwners.slice(0, hopCount);
|
|
544
599
|
const hop0 = hopSenders[0];
|
|
545
|
-
|
|
600
|
+
let callData0;
|
|
601
|
+
if (useNativeToken) {
|
|
602
|
+
// ✅ 原生代币模式
|
|
603
|
+
if (extractProfit && profitWei > 0n) {
|
|
604
|
+
callData0 = encodeExecuteBatch([hop0, profitRecipient], [totalBuyWei, profitWei], ['0x', '0x']);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
callData0 = encodeExecute(hop0, totalBuyWei, '0x');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// ✅ ERC20 模式
|
|
612
|
+
if (extractProfit && profitWei > 0n) {
|
|
613
|
+
const transferToHop0 = erc20Iface.encodeFunctionData('transfer', [hop0, totalBuyWei]);
|
|
614
|
+
const transferToProfit = erc20Iface.encodeFunctionData('transfer', [profitRecipient, profitWei]);
|
|
615
|
+
callData0 = encodeExecuteBatch([quoteToken, quoteToken], [0n, 0n], [transferToHop0, transferToProfit]);
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
const transferData = erc20Iface.encodeFunctionData('transfer', [hop0, totalBuyWei]);
|
|
619
|
+
callData0 = encodeExecute(quoteToken, 0n, transferData);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
546
622
|
const signedToHop0 = await this.aaManager.buildUserOpWithState({
|
|
547
623
|
ownerWallet: sellerOwner,
|
|
548
624
|
sender: sellerAi.sender,
|
|
@@ -561,7 +637,14 @@ export class AADexSwapExecutor {
|
|
|
561
637
|
const remaining = totalBuyWei - prefixKept;
|
|
562
638
|
if (remaining <= 0n)
|
|
563
639
|
break;
|
|
564
|
-
|
|
640
|
+
let callData;
|
|
641
|
+
if (useNativeToken) {
|
|
642
|
+
callData = encodeExecute(next, remaining, '0x');
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
const transferData = erc20Iface.encodeFunctionData('transfer', [next, remaining]);
|
|
646
|
+
callData = encodeExecute(quoteToken, 0n, transferData);
|
|
647
|
+
}
|
|
565
648
|
const signedHop = await this.aaManager.buildUserOpWithState({
|
|
566
649
|
ownerWallet: hopOwners[j],
|
|
567
650
|
sender,
|
|
@@ -580,11 +663,20 @@ export class AADexSwapExecutor {
|
|
|
580
663
|
const restItems = rest.map((to, i) => ({ to, value: restAmounts[i] ?? 0n })).filter(x => x.value > 0n);
|
|
581
664
|
const restChunks = chunkArray(restItems, maxPerOp);
|
|
582
665
|
for (const ch of restChunks) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
666
|
+
let callData;
|
|
667
|
+
if (useNativeToken) {
|
|
668
|
+
const { totalValue, data } = encodeNativeDisperseViaMulticall3({
|
|
669
|
+
to: ch.map(x => x.to),
|
|
670
|
+
values: ch.map(x => x.value),
|
|
671
|
+
});
|
|
672
|
+
callData = encodeExecute(MULTICALL3, totalValue, data);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
const targets = ch.map(() => quoteToken);
|
|
676
|
+
const values = ch.map(() => 0n);
|
|
677
|
+
const datas = ch.map(x => erc20Iface.encodeFunctionData('transfer', [x.to, x.value]));
|
|
678
|
+
callData = encodeExecuteBatch(targets, values, datas);
|
|
679
|
+
}
|
|
588
680
|
const signedDisperse = await this.aaManager.buildUserOpWithState({
|
|
589
681
|
ownerWallet: lastHopOwner,
|
|
590
682
|
sender: lastHopSender,
|
|
@@ -597,29 +689,37 @@ export class AADexSwapExecutor {
|
|
|
597
689
|
}
|
|
598
690
|
}
|
|
599
691
|
}
|
|
600
|
-
// Batch Buy ops(分发后再执行)- ✅ 支持 V2
|
|
692
|
+
// Batch Buy ops(分发后再执行)- ✅ 支持 V2/V3,支持 ERC20 稳定币
|
|
601
693
|
for (let i = 0; i < buyerOwners.length; i++) {
|
|
602
694
|
const ai = buyerAis[i];
|
|
603
695
|
const buyWei = buyAmountsWei[i];
|
|
604
|
-
let
|
|
605
|
-
if (
|
|
606
|
-
//
|
|
607
|
-
buySwapData
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
696
|
+
let buyCallData;
|
|
697
|
+
if (useNativeToken) {
|
|
698
|
+
// ✅ 原生代币模式:OKB → Token
|
|
699
|
+
let buySwapData;
|
|
700
|
+
if (isV3Trade) {
|
|
701
|
+
buySwapData = encodeSwapExactETHForTokensV3({
|
|
702
|
+
tokenIn: WOKB,
|
|
703
|
+
tokenOut: tokenAddress,
|
|
704
|
+
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
705
|
+
recipient: ai.sender,
|
|
706
|
+
deadline,
|
|
707
|
+
amountIn: buyWei,
|
|
708
|
+
amountOutMinimum: 0n,
|
|
709
|
+
sqrtPriceLimitX96: 0n,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], ai.sender, deadline);
|
|
714
|
+
}
|
|
715
|
+
buyCallData = encodeExecute(effectiveRouter, buyWei, buySwapData);
|
|
617
716
|
}
|
|
618
717
|
else {
|
|
619
|
-
//
|
|
620
|
-
|
|
718
|
+
// ✅ ERC20 模式:quoteToken → Token(需要先 approve)
|
|
719
|
+
const approveData = encodeApproveCall(effectiveRouter, buyWei);
|
|
720
|
+
const swapData = encodeSwapExactTokensForTokensSupportingFee(buyWei, 0n, [quoteToken, tokenAddress], ai.sender, deadline);
|
|
721
|
+
buyCallData = encodeExecuteBatch([quoteToken, effectiveRouter], [0n, 0n], [approveData, swapData]);
|
|
621
722
|
}
|
|
622
|
-
const buyCallData = encodeExecute(effectiveRouter, buyWei, buySwapData);
|
|
623
723
|
const signedBuy = await this.aaManager.buildUserOpWithState({
|
|
624
724
|
ownerWallet: buyerOwners[i],
|
|
625
725
|
sender: ai.sender,
|
|
@@ -636,17 +736,8 @@ export class AADexSwapExecutor {
|
|
|
636
736
|
beneficiary,
|
|
637
737
|
nonce: payerStartNonce,
|
|
638
738
|
});
|
|
739
|
+
// ✅ 利润已在 AA 内部通过分发阶段刮取,不需要 Tail Tx
|
|
639
740
|
const signedTransactions = [signedHandleOps];
|
|
640
|
-
if (extractProfit && profitWei > 0n) {
|
|
641
|
-
const tx = ethers.Transaction.from(signedHandleOps);
|
|
642
|
-
const tailTx = await this.bundleExecutor['signProfitTransaction']({
|
|
643
|
-
payerWallet,
|
|
644
|
-
profitWei,
|
|
645
|
-
recipient: profitRecipient,
|
|
646
|
-
nonce: Number(tx.nonce) + 1,
|
|
647
|
-
});
|
|
648
|
-
signedTransactions.push(tailTx);
|
|
649
|
-
}
|
|
650
741
|
return {
|
|
651
742
|
signedTransactions,
|
|
652
743
|
metadata: {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Wallet, Contract, Interface, ethers } from 'ethers';
|
|
10
10
|
import { ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, WOKB, } from './constants.js';
|
|
11
|
-
import { AAAccountManager, encodeExecute } from './aa-account.js';
|
|
11
|
+
import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
|
|
12
12
|
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, } from './dex.js';
|
|
13
13
|
import { PortalQuery, encodeApproveCall, parseOkb, formatOkb, lpFeeProfileToV3Fee } from './portal-ops.js';
|
|
14
14
|
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
@@ -153,7 +153,10 @@ export class DexBundleExecutor {
|
|
|
153
153
|
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
|
|
154
154
|
const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
|
|
155
155
|
const toOwnerWei = withdrawAmount - profitWei;
|
|
156
|
-
|
|
156
|
+
// ✅ AA 内部刮取利润:使用 executeBatch 同时转账给利润接收者和 owner
|
|
157
|
+
const callData = extractProfit && profitWei > 0n
|
|
158
|
+
? encodeExecuteBatch([profitRecipient, params.ownerWallet.address], [profitWei, toOwnerWei], ['0x', '0x'])
|
|
159
|
+
: encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
|
|
157
160
|
const { userOp } = gasPolicy === 'fixed'
|
|
158
161
|
? await this.aaManager.buildUserOpWithFixedGas({
|
|
159
162
|
ownerWallet: params.ownerWallet,
|
|
@@ -380,23 +383,7 @@ export class DexBundleExecutor {
|
|
|
380
383
|
if (withdrawOps.length > 0) {
|
|
381
384
|
withdrawResult = await this.runHandleOps('dex-withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
|
|
382
385
|
}
|
|
383
|
-
|
|
384
|
-
const provider = this.aaManager.getProvider();
|
|
385
|
-
const feeData = await provider.getFeeData();
|
|
386
|
-
const nonce = await provider.getTransactionCount(bundlerSigner.address, 'pending');
|
|
387
|
-
const tailTx = await bundlerSigner.signTransaction({
|
|
388
|
-
to: profitSettings.profitRecipient,
|
|
389
|
-
value: totalProfitWei,
|
|
390
|
-
data: '0x',
|
|
391
|
-
nonce,
|
|
392
|
-
gasLimit: this.config.profitTailGasLimit ?? 21000n,
|
|
393
|
-
gasPrice: feeData.gasPrice ?? 100000000n,
|
|
394
|
-
chainId: this.config.chainId ?? 196,
|
|
395
|
-
type: 0,
|
|
396
|
-
});
|
|
397
|
-
const tx = await provider.broadcastTransaction(tailTx);
|
|
398
|
-
await tx.wait();
|
|
399
|
-
}
|
|
386
|
+
// ✅ 利润已在 withdraw UserOp 内部通过 executeBatch 刮取,无需单独广播 tail tx
|
|
400
387
|
}
|
|
401
388
|
const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
|
|
402
389
|
return {
|