four-flap-meme-sdk 1.6.95 → 1.6.97
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/contracts/tm-bundle-merkle/submit.d.ts +2 -0
- package/dist/contracts/tm-bundle-merkle/submit.js +31 -9
- package/dist/flap/portal-bundle-merkle/index.d.ts +1 -0
- package/dist/flap/portal-bundle-merkle/index.js +2 -0
- package/dist/flap/portal-bundle-merkle/swap.d.ts +40 -0
- package/dist/flap/portal-bundle-merkle/swap.js +133 -0
- package/dist/pancake/bundle-swap.js +9 -4
- package/dist/xlayer/aa-transfer-profit.js +210 -69
- package/package.json +1 -1
|
@@ -255,6 +255,8 @@ export declare function submitDirectToRpc(signedTransactions: string[], config:
|
|
|
255
255
|
*
|
|
256
256
|
* 每笔交易广播后会等待上链确认,确保后续交易能正确执行
|
|
257
257
|
*
|
|
258
|
+
* ⚠️ 多跳场景:如果某笔交易失败,会立即停止(因为后续交易肯定也会失败)
|
|
259
|
+
*
|
|
258
260
|
* @param signedTransactions 签名后的交易数组
|
|
259
261
|
* @param config 直接广播配置
|
|
260
262
|
* @returns 广播结果
|
|
@@ -397,6 +397,8 @@ export async function submitDirectToRpc(signedTransactions, config) {
|
|
|
397
397
|
*
|
|
398
398
|
* 每笔交易广播后会等待上链确认,确保后续交易能正确执行
|
|
399
399
|
*
|
|
400
|
+
* ⚠️ 多跳场景:如果某笔交易失败,会立即停止(因为后续交易肯定也会失败)
|
|
401
|
+
*
|
|
400
402
|
* @param signedTransactions 签名后的交易数组
|
|
401
403
|
* @param config 直接广播配置
|
|
402
404
|
* @returns 广播结果
|
|
@@ -427,7 +429,8 @@ export async function submitDirectToRpcSequential(signedTransactions, config) {
|
|
|
427
429
|
}
|
|
428
430
|
const chainId = config.chainId ?? 143;
|
|
429
431
|
const chainName = config.chainName ?? 'MONAD';
|
|
430
|
-
|
|
432
|
+
// ✅ XLayer 链默认 120 秒超时(网络可能较慢)
|
|
433
|
+
const confirmationTimeout = config.confirmationTimeout ?? (chainId === 196 ? 120000 : 60000);
|
|
431
434
|
const provider = new ethers.JsonRpcProvider(config.rpcUrl, {
|
|
432
435
|
chainId,
|
|
433
436
|
name: chainName
|
|
@@ -435,16 +438,19 @@ export async function submitDirectToRpcSequential(signedTransactions, config) {
|
|
|
435
438
|
const results = [];
|
|
436
439
|
const txHashes = [];
|
|
437
440
|
const errors = [];
|
|
441
|
+
console.log(`📤 [${chainName}] 开始顺序广播 ${totalTransactions} 笔交易(等待每笔确认)`);
|
|
438
442
|
// 顺序广播并等待确认
|
|
439
443
|
for (let i = 0; i < signedTransactions.length; i++) {
|
|
440
444
|
const signedTx = signedTransactions[i];
|
|
441
445
|
try {
|
|
442
446
|
// 广播交易
|
|
443
447
|
const txResponse = await provider.broadcastTransaction(signedTx);
|
|
448
|
+
console.log(`📡 [${chainName}] 交易 ${i + 1}/${totalTransactions} 广播成功: ${txResponse.hash},等待确认...`);
|
|
444
449
|
// ✅ 等待交易确认
|
|
445
450
|
const receipt = await provider.waitForTransaction(txResponse.hash, 1, // 等待1个确认
|
|
446
451
|
confirmationTimeout);
|
|
447
452
|
if (receipt && receipt.status === 1) {
|
|
453
|
+
console.log(`✅ [${chainName}] 交易 ${i + 1}/${totalTransactions} 已确认: ${txResponse.hash}`);
|
|
448
454
|
results.push({
|
|
449
455
|
index: i,
|
|
450
456
|
success: true,
|
|
@@ -454,6 +460,7 @@ export async function submitDirectToRpcSequential(signedTransactions, config) {
|
|
|
454
460
|
}
|
|
455
461
|
else {
|
|
456
462
|
const errorMsg = `交易执行失败(status=${receipt?.status})`;
|
|
463
|
+
console.error(`❌ [${chainName}] 交易 ${i + 1}/${totalTransactions} 上链失败: ${errorMsg}`);
|
|
457
464
|
results.push({
|
|
458
465
|
index: i,
|
|
459
466
|
success: false,
|
|
@@ -461,32 +468,44 @@ export async function submitDirectToRpcSequential(signedTransactions, config) {
|
|
|
461
468
|
error: errorMsg
|
|
462
469
|
});
|
|
463
470
|
errors.push(`交易 ${i + 1}: ${errorMsg}`);
|
|
464
|
-
// ✅
|
|
471
|
+
// ✅ 多跳场景:如果某笔失败,后续交易肯定也会失败,立即停止
|
|
472
|
+
console.warn(`⚠️ [${chainName}] 多跳模式检测到失败,停止后续 ${totalTransactions - i - 1} 笔交易`);
|
|
473
|
+
break;
|
|
465
474
|
}
|
|
466
475
|
}
|
|
467
476
|
catch (error) {
|
|
468
477
|
const errorMessage = error?.message || String(error);
|
|
478
|
+
console.error(`❌ [${chainName}] 交易 ${i + 1}/${totalTransactions} 广播/确认失败:`, errorMessage);
|
|
469
479
|
// ✅ 兼容:nonce too low / already known 等情况下,交易可能已经被接收
|
|
470
480
|
const recovered = await recoverTxHashIfAlreadySeen(provider, signedTx, errorMessage);
|
|
471
481
|
if (recovered) {
|
|
482
|
+
console.log(`🔄 [${chainName}] 交易 ${i + 1} 可能已广播,尝试等待确认: ${recovered}`);
|
|
472
483
|
// 顺序模式依赖确认:尽量等待一下
|
|
473
484
|
try {
|
|
474
485
|
const receipt = await provider.waitForTransaction(recovered, 1, confirmationTimeout);
|
|
475
486
|
if (receipt && receipt.status === 1) {
|
|
487
|
+
console.log(`✅ [${chainName}] 交易 ${i + 1}/${totalTransactions} 恢复成功: ${recovered}`);
|
|
476
488
|
results.push({ index: i, success: true, txHash: recovered, error: errorMessage });
|
|
477
489
|
txHashes.push(recovered);
|
|
478
490
|
continue;
|
|
479
491
|
}
|
|
480
492
|
const errorMsg = `交易执行失败(status=${receipt?.status})`;
|
|
493
|
+
console.error(`❌ [${chainName}] 交易 ${i + 1} 恢复失败: ${errorMsg}`);
|
|
481
494
|
results.push({ index: i, success: false, txHash: recovered, error: `${errorMessage} | ${errorMsg}` });
|
|
482
495
|
errors.push(`交易 ${i + 1}: ${errorMessage} | ${errorMsg}`);
|
|
483
|
-
|
|
496
|
+
// ✅ 多跳场景:失败则停止
|
|
497
|
+
console.warn(`⚠️ [${chainName}] 多跳模式检测到失败,停止后续 ${totalTransactions - i - 1} 笔交易`);
|
|
498
|
+
break;
|
|
484
499
|
}
|
|
485
|
-
catch {
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
500
|
+
catch (waitError) {
|
|
501
|
+
// 等待超时:对于多跳场景,不能假设成功,需要停止
|
|
502
|
+
const waitErrorMsg = waitError?.message || String(waitError);
|
|
503
|
+
console.error(`⏱️ [${chainName}] 交易 ${i + 1} 等待确认超时: ${waitErrorMsg}`);
|
|
504
|
+
results.push({ index: i, success: false, txHash: recovered, error: `${errorMessage} | 等待确认超时: ${waitErrorMsg}` });
|
|
505
|
+
errors.push(`交易 ${i + 1}: ${errorMessage} | 等待确认超时`);
|
|
506
|
+
// ✅ 多跳场景:超时也停止
|
|
507
|
+
console.warn(`⚠️ [${chainName}] 多跳模式检测到超时,停止后续 ${totalTransactions - i - 1} 笔交易`);
|
|
508
|
+
break;
|
|
490
509
|
}
|
|
491
510
|
}
|
|
492
511
|
results.push({
|
|
@@ -495,11 +514,14 @@ export async function submitDirectToRpcSequential(signedTransactions, config) {
|
|
|
495
514
|
error: errorMessage
|
|
496
515
|
});
|
|
497
516
|
errors.push(`交易 ${i + 1}: ${errorMessage}`);
|
|
498
|
-
// ✅
|
|
517
|
+
// ✅ 多跳场景:如果某笔失败,后续交易肯定也会失败,立即停止
|
|
518
|
+
console.warn(`⚠️ [${chainName}] 多跳模式检测到失败,停止后续 ${totalTransactions - i - 1} 笔交易`);
|
|
519
|
+
break;
|
|
499
520
|
}
|
|
500
521
|
}
|
|
501
522
|
const successCount = txHashes.length;
|
|
502
523
|
const failedCount = totalTransactions - successCount;
|
|
524
|
+
console.log(`📊 [${chainName}] 顺序广播完成: 成功 ${successCount}/${totalTransactions}`);
|
|
503
525
|
return {
|
|
504
526
|
code: successCount > 0,
|
|
505
527
|
totalTransactions,
|
|
@@ -11,3 +11,4 @@ export { pancakeProxyBatchBuyMerkle, pancakeProxyBatchSellMerkle, approvePancake
|
|
|
11
11
|
export { flapDisperseWithBundleMerkle, flapSweepWithBundleMerkle, type FlapDisperseSignParams, type FlapDisperseMerkleResult, type FlapSweepSignParams, type FlapSweepMerkleResult } from './utils.js';
|
|
12
12
|
export { flapBundleCurveToDex, type CurveToDexChain, type DexPoolType, type CurveToDexSignConfig, type CurveBuyerConfig, type DexBuyerConfig, type FlapCurveToDexParams, type FlapCurveToDexResult } from './curve-to-dex.js';
|
|
13
13
|
export { flapBundleCreateToDex, type CreateToDexChain, type CreateToDexSignConfig, type CreateTokenInfo, type FlapCreateToDexParams, type FlapCreateToDexResult, type CurveBuyerConfig as CreateCurveBuyerConfig, type DexBuyerConfig as CreateDexBuyerConfig, type DexPoolType as CreateDexPoolType } from './create-to-dex.js';
|
|
14
|
+
export { flapBundleSwapMerkle, flapBatchSwapMerkle, flapQuickBatchSwapMerkle, flapCrossSwapMerkle, type FlapSwapSignConfig, type FlapSwapConfig, type FlapBundleSwapSignParams, type FlapBundleSwapParams, type FlapSwapResult, type FlapBatchSwapSignParams, type FlapBatchSwapResult, type FlapQuickBatchSwapSignParams, type FlapQuickBatchSwapResult, type FlapCrossSwapParams, type FlapCrossSwapResult, type UserType } from './swap.js';
|
|
@@ -19,3 +19,5 @@ export { flapDisperseWithBundleMerkle, flapSweepWithBundleMerkle } from './utils
|
|
|
19
19
|
export { flapBundleCurveToDex } from './curve-to-dex.js';
|
|
20
20
|
// ✅ 发币 + 一键买到外盘(Create → Curve → DEX)- 新代币
|
|
21
21
|
export { flapBundleCreateToDex } from './create-to-dex.js';
|
|
22
|
+
// ✅ 内盘换手方法(一卖一买、一卖多买、快捷资金利用率、交叉换手)
|
|
23
|
+
export { flapBundleSwapMerkle, flapBatchSwapMerkle, flapQuickBatchSwapMerkle, flapCrossSwapMerkle } from './swap.js';
|
|
@@ -144,6 +144,38 @@ export interface FlapQuickBatchSwapResult {
|
|
|
144
144
|
disperseHopCount?: number;
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
|
+
/** 交叉换手参数 */
|
|
148
|
+
export interface FlapCrossSwapParams {
|
|
149
|
+
chain: FlapChain;
|
|
150
|
+
/** 卖方私钥列表(按顺序轮流卖出) */
|
|
151
|
+
sellerPrivateKeys: string[];
|
|
152
|
+
/** 每个卖方的卖出数量(与 sellerPrivateKeys 长度一致) */
|
|
153
|
+
sellAmounts: string[];
|
|
154
|
+
/** 买方私钥列表(按顺序轮流分配,每笔平分) */
|
|
155
|
+
buyerPrivateKeys: string[];
|
|
156
|
+
tokenAddress: string;
|
|
157
|
+
config: FlapSwapSignConfig;
|
|
158
|
+
quoteToken?: string;
|
|
159
|
+
quoteTokenDecimals?: number;
|
|
160
|
+
/** 每次卖出分配给多少个买方(默认使用全部买方平分) */
|
|
161
|
+
buyersPerSell?: number;
|
|
162
|
+
/** 转账多跳数(可选) */
|
|
163
|
+
disperseHopCount?: number;
|
|
164
|
+
}
|
|
165
|
+
/** 交叉换手结果 */
|
|
166
|
+
export interface FlapCrossSwapResult {
|
|
167
|
+
signedTransactions: string[];
|
|
168
|
+
/** 每轮的元数据列表 */
|
|
169
|
+
rounds: Array<{
|
|
170
|
+
sellerAddress: string;
|
|
171
|
+
buyerAddresses: string[];
|
|
172
|
+
sellAmount: string;
|
|
173
|
+
bundleHash?: string;
|
|
174
|
+
}>;
|
|
175
|
+
/** 汇总的中间钱包信息(转账/利润) */
|
|
176
|
+
disperseHopWallets?: GeneratedWallet[];
|
|
177
|
+
profitHopWallets?: GeneratedWallet[];
|
|
178
|
+
}
|
|
147
179
|
/**
|
|
148
180
|
* Flap 内盘快捷批量换手(资金利用率模式)
|
|
149
181
|
*
|
|
@@ -159,3 +191,11 @@ export interface FlapQuickBatchSwapResult {
|
|
|
159
191
|
* 限制:根据多跳数动态计算最大买方数量
|
|
160
192
|
*/
|
|
161
193
|
export declare function flapQuickBatchSwapMerkle(params: FlapQuickBatchSwapSignParams): Promise<FlapQuickBatchSwapResult>;
|
|
194
|
+
/**
|
|
195
|
+
* Flap 交叉换手(多卖多买,循环执行)
|
|
196
|
+
* - 每个卖方单独卖出,买入金额由卖出所得等分分配给当轮买方
|
|
197
|
+
* - 利润刮取、转账多跳逻辑继承 flapQuickBatchSwapMerkle
|
|
198
|
+
* - 结果按顺序拼接 signedTransactions(可交由前端/后端顺序广播)
|
|
199
|
+
* - ✅ 正确处理 nonce:同一钱包在多轮中使用时 nonce 自动递增
|
|
200
|
+
*/
|
|
201
|
+
export declare function flapCrossSwapMerkle(params: FlapCrossSwapParams): Promise<FlapCrossSwapResult>;
|
|
@@ -1222,3 +1222,136 @@ export async function flapQuickBatchSwapMerkle(params) {
|
|
|
1222
1222
|
}
|
|
1223
1223
|
};
|
|
1224
1224
|
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Flap 交叉换手(多卖多买,循环执行)
|
|
1227
|
+
* - 每个卖方单独卖出,买入金额由卖出所得等分分配给当轮买方
|
|
1228
|
+
* - 利润刮取、转账多跳逻辑继承 flapQuickBatchSwapMerkle
|
|
1229
|
+
* - 结果按顺序拼接 signedTransactions(可交由前端/后端顺序广播)
|
|
1230
|
+
* - ✅ 正确处理 nonce:同一钱包在多轮中使用时 nonce 自动递增
|
|
1231
|
+
*/
|
|
1232
|
+
export async function flapCrossSwapMerkle(params) {
|
|
1233
|
+
const { chain, sellerPrivateKeys, sellAmounts, buyerPrivateKeys, tokenAddress, config, quoteToken, quoteTokenDecimals = 18, buyersPerSell, disperseHopCount = 0 } = params;
|
|
1234
|
+
if (sellerPrivateKeys.length === 0) {
|
|
1235
|
+
throw new Error('至少需要一个卖方');
|
|
1236
|
+
}
|
|
1237
|
+
if (sellerPrivateKeys.length !== sellAmounts.length) {
|
|
1238
|
+
throw new Error(`sellAmounts 长度 (${sellAmounts.length}) 必须与卖方数量 (${sellerPrivateKeys.length}) 一致`);
|
|
1239
|
+
}
|
|
1240
|
+
if (buyerPrivateKeys.length === 0) {
|
|
1241
|
+
throw new Error('至少需要一个买方');
|
|
1242
|
+
}
|
|
1243
|
+
// ✅ 创建 Provider 和 NonceManager
|
|
1244
|
+
const chainContext = createChainContext(chain, config);
|
|
1245
|
+
const nonceManager = new NonceManager(chainContext.provider);
|
|
1246
|
+
// ✅ 预先获取所有钱包的初始 nonce
|
|
1247
|
+
const allSellerWallets = sellerPrivateKeys.map(pk => new Wallet(pk, chainContext.provider));
|
|
1248
|
+
const allBuyerWallets = buyerPrivateKeys.map(pk => new Wallet(pk, chainContext.provider));
|
|
1249
|
+
// 使用 Map 去重(同一私钥可能出现多次)
|
|
1250
|
+
const addressToNonce = new Map();
|
|
1251
|
+
// 获取所有卖方 nonce
|
|
1252
|
+
for (const wallet of allSellerWallets) {
|
|
1253
|
+
if (!addressToNonce.has(wallet.address)) {
|
|
1254
|
+
const nonce = await nonceManager.getNextNonce(wallet);
|
|
1255
|
+
addressToNonce.set(wallet.address, nonce);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
// 获取所有买方 nonce
|
|
1259
|
+
for (const wallet of allBuyerWallets) {
|
|
1260
|
+
if (!addressToNonce.has(wallet.address)) {
|
|
1261
|
+
const nonce = await nonceManager.getNextNonce(wallet);
|
|
1262
|
+
addressToNonce.set(wallet.address, nonce);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
console.log(`[flapCrossSwapMerkle] 初始化完成: ${sellerPrivateKeys.length} 卖方, ${buyerPrivateKeys.length} 买方`);
|
|
1266
|
+
const allSigned = [];
|
|
1267
|
+
const allDisperse = [];
|
|
1268
|
+
const allProfit = [];
|
|
1269
|
+
const rounds = [];
|
|
1270
|
+
// 轮流分配买方:每轮取 buyersPerSell 个(默认所有买方),不足则从头继续
|
|
1271
|
+
let buyerCursor = 0;
|
|
1272
|
+
const buyerBatchSize = Math.max(1, buyersPerSell ?? buyerPrivateKeys.length);
|
|
1273
|
+
for (let i = 0; i < sellerPrivateKeys.length; i++) {
|
|
1274
|
+
const sellerPk = sellerPrivateKeys[i];
|
|
1275
|
+
const sellAmount = sellAmounts[i];
|
|
1276
|
+
const sellerWallet = allSellerWallets[i];
|
|
1277
|
+
// 选取本轮买方私钥和钱包
|
|
1278
|
+
const roundBuyerPks = [];
|
|
1279
|
+
const roundBuyerWallets = [];
|
|
1280
|
+
for (let k = 0; k < buyerBatchSize; k++) {
|
|
1281
|
+
const idx = buyerCursor % buyerPrivateKeys.length;
|
|
1282
|
+
roundBuyerPks.push(buyerPrivateKeys[idx]);
|
|
1283
|
+
roundBuyerWallets.push(allBuyerWallets[idx]);
|
|
1284
|
+
buyerCursor++;
|
|
1285
|
+
}
|
|
1286
|
+
// ✅ 获取卖方当前 nonce
|
|
1287
|
+
const sellerNonce = addressToNonce.get(sellerWallet.address);
|
|
1288
|
+
// ✅ 获取并更新买方 nonces(每个买方本轮需要 1 个 nonce)
|
|
1289
|
+
const roundBuyerNonces = roundBuyerWallets.map(w => {
|
|
1290
|
+
const nonce = addressToNonce.get(w.address);
|
|
1291
|
+
addressToNonce.set(w.address, nonce + 1); // 每个买方用 1 个 nonce
|
|
1292
|
+
return nonce;
|
|
1293
|
+
});
|
|
1294
|
+
// 平分买入金额(使用比例模式,避免固定金额超出可分配金额)
|
|
1295
|
+
const ratio = 1 / roundBuyerPks.length;
|
|
1296
|
+
const buyerRatios = roundBuyerPks.map(() => ratio);
|
|
1297
|
+
// ✅ 构建 startNonces: [sellerNonce, buyer1Nonce, buyer2Nonce, ...]
|
|
1298
|
+
const startNonces = [sellerNonce, ...roundBuyerNonces];
|
|
1299
|
+
// ✅ 预先计算卖方 nonce 消耗(精确计算)
|
|
1300
|
+
// 卖方 nonce 消耗 = 贿赂(0/1) + 卖出(1) + 转账(N 或 N*2) + 利润(1)
|
|
1301
|
+
const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
|
|
1302
|
+
const hasBribe = getBribeAmount(config) > 0n;
|
|
1303
|
+
const hasProfit = true; // 默认有利润刮取
|
|
1304
|
+
let sellerNonceConsumed = 0;
|
|
1305
|
+
if (hasBribe)
|
|
1306
|
+
sellerNonceConsumed += 1; // 贿赂
|
|
1307
|
+
sellerNonceConsumed += 1; // 卖出
|
|
1308
|
+
if (disperseHopCount === 0) {
|
|
1309
|
+
sellerNonceConsumed += roundBuyerPks.length; // 直接转账
|
|
1310
|
+
}
|
|
1311
|
+
else if (useNativeToken) {
|
|
1312
|
+
sellerNonceConsumed += roundBuyerPks.length; // 原生多跳:seller→hop1
|
|
1313
|
+
}
|
|
1314
|
+
else {
|
|
1315
|
+
sellerNonceConsumed += roundBuyerPks.length * 2; // ERC20 多跳:BNB + ERC20 两批
|
|
1316
|
+
}
|
|
1317
|
+
if (hasProfit)
|
|
1318
|
+
sellerNonceConsumed += 1; // 利润
|
|
1319
|
+
// 调用单卖多买的签名函数
|
|
1320
|
+
const res = await flapQuickBatchSwapMerkle({
|
|
1321
|
+
chain,
|
|
1322
|
+
sellerPrivateKey: sellerPk,
|
|
1323
|
+
sellAmount,
|
|
1324
|
+
buyerPrivateKeys: roundBuyerPks,
|
|
1325
|
+
buyerRatios,
|
|
1326
|
+
tokenAddress,
|
|
1327
|
+
config,
|
|
1328
|
+
quoteToken,
|
|
1329
|
+
quoteTokenDecimals,
|
|
1330
|
+
disperseHopCount,
|
|
1331
|
+
startNonces // ✅ 传入预计算的 nonces
|
|
1332
|
+
});
|
|
1333
|
+
// ✅ 更新卖方 nonce(使用精确计算的值)
|
|
1334
|
+
addressToNonce.set(sellerWallet.address, sellerNonce + sellerNonceConsumed);
|
|
1335
|
+
console.log(`[flapCrossSwapMerkle] 轮次 ${i + 1}: 卖方=${sellerWallet.address.slice(0, 10)}..., 买方=${roundBuyerPks.length}, 交易=${res.signedTransactions.length}`);
|
|
1336
|
+
// 累积签名与中间钱包
|
|
1337
|
+
allSigned.push(...res.signedTransactions);
|
|
1338
|
+
if (res.disperseHopWallets)
|
|
1339
|
+
allDisperse.push(...res.disperseHopWallets);
|
|
1340
|
+
if (res.profitHopWallets)
|
|
1341
|
+
allProfit.push(...res.profitHopWallets);
|
|
1342
|
+
rounds.push({
|
|
1343
|
+
sellerAddress: res.metadata?.sellerAddress || '',
|
|
1344
|
+
buyerAddresses: res.metadata?.buyerAddresses || roundBuyerPks.map(() => ''),
|
|
1345
|
+
sellAmount,
|
|
1346
|
+
bundleHash: undefined
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
nonceManager.clearTemp();
|
|
1350
|
+
console.log(`[flapCrossSwapMerkle] 完成: ${rounds.length} 轮, ${allSigned.length} 笔交易`);
|
|
1351
|
+
return {
|
|
1352
|
+
signedTransactions: allSigned,
|
|
1353
|
+
rounds,
|
|
1354
|
+
disperseHopWallets: allDisperse.length > 0 ? allDisperse : undefined,
|
|
1355
|
+
profitHopWallets: allProfit.length > 0 ? allProfit : undefined
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
@@ -1051,7 +1051,11 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1051
1051
|
} = params;
|
|
1052
1052
|
// ✅ 判断是否使用原生代币
|
|
1053
1053
|
const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
|
|
1054
|
-
|
|
1054
|
+
// ✅ 根据 chainId 获取正确的 Wrapped Native Token 地址
|
|
1055
|
+
const chainId = config.chainId ?? 56;
|
|
1056
|
+
const WRAPPED_NATIVE = chainId === 196
|
|
1057
|
+
? ADDRESSES.XLAYER.WOKB.toLowerCase() // XLayer: WOKB
|
|
1058
|
+
: ADDRESSES.BSC.WBNB.toLowerCase(); // BSC: WBNB
|
|
1055
1059
|
// ✅ 动态计算最大买方数量(根据多跳数)
|
|
1056
1060
|
// 固定开销: 贿赂(1) + 卖出(1) + 利润多跳(PROFIT_HOP_COUNT + 1)
|
|
1057
1061
|
// BNB 模式(无多跳): 转账(N) + 买入(N) = 2N
|
|
@@ -1108,10 +1112,11 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1108
1112
|
}
|
|
1109
1113
|
}
|
|
1110
1114
|
// 校验输出代币
|
|
1115
|
+
const wrappedNativeName = chainId === 196 ? 'WOKB' : 'WBNB';
|
|
1111
1116
|
if (useNativeToken) {
|
|
1112
|
-
// 原生代币模式:输出必须是 WBNB
|
|
1113
|
-
if (!sellOutputToken || sellOutputToken.toLowerCase() !==
|
|
1114
|
-
throw new Error(`原生代币模式要求卖出路径以
|
|
1117
|
+
// 原生代币模式:输出必须是 Wrapped Native Token(BSC: WBNB, XLayer: WOKB)
|
|
1118
|
+
if (!sellOutputToken || sellOutputToken.toLowerCase() !== WRAPPED_NATIVE) {
|
|
1119
|
+
throw new Error(`原生代币模式要求卖出路径以 ${wrappedNativeName} 结尾(当前输出: ${sellOutputToken || '未知'})。` +
|
|
1115
1120
|
`请切换交易对或使用 ERC20 模式。`);
|
|
1116
1121
|
}
|
|
1117
1122
|
}
|
|
@@ -1,34 +1,72 @@
|
|
|
1
|
-
import { ethers } from 'ethers';
|
|
1
|
+
import { ethers, JsonRpcProvider } from 'ethers';
|
|
2
2
|
import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
|
|
3
3
|
import { createAAAccountManager, createWallet, encodeExecute, encodeExecuteBatch, encodeExecuteViaMulticall3 } from './aa-account.js';
|
|
4
4
|
import { quoteTokenToOkb } from './dex.js';
|
|
5
|
-
import { PortalQuery } from './portal-ops.js';
|
|
5
|
+
import { PortalQuery, lpFeeProfileToV3Fee } from './portal-ops.js';
|
|
6
|
+
import { WOKB, DEFAULT_RPC_URL, XLAYER_CHAIN_ID } from './constants.js';
|
|
7
|
+
import { quoteV3 } from '../utils/quote-helpers.js';
|
|
6
8
|
/**
|
|
7
9
|
* ✅ 获取 Token → OKB 报价(带回退逻辑)
|
|
8
|
-
*
|
|
10
|
+
* 改进版:V2 → V3 → Flap Portal,确保尽可能获取报价
|
|
9
11
|
*/
|
|
10
12
|
async function quoteTokenToOkbWithFallback(tokenAmount, tokenAddress, config) {
|
|
11
13
|
if (tokenAmount <= 0n)
|
|
12
14
|
return 0n;
|
|
15
|
+
const rpcUrl = config.rpcUrl || DEFAULT_RPC_URL;
|
|
16
|
+
const chainId = config.chainId ?? XLAYER_CHAIN_ID;
|
|
13
17
|
// 1. 先尝试 V2 报价
|
|
14
18
|
try {
|
|
15
|
-
const okbOut = await quoteTokenToOkb(tokenAmount, tokenAddress, { rpcUrl
|
|
16
|
-
if (okbOut > 0n)
|
|
19
|
+
const okbOut = await quoteTokenToOkb(tokenAmount, tokenAddress, { rpcUrl });
|
|
20
|
+
if (okbOut > 0n) {
|
|
21
|
+
console.log(`[quoteTokenToOkbWithFallback] V2 报价成功: ${ethers.formatEther(okbOut)} OKB`);
|
|
17
22
|
return okbOut;
|
|
23
|
+
}
|
|
18
24
|
}
|
|
19
|
-
catch {
|
|
20
|
-
// V2 报价失败,继续尝试
|
|
25
|
+
catch (e) {
|
|
26
|
+
// V2 报价失败,继续尝试 V3
|
|
27
|
+
console.log(`[quoteTokenToOkbWithFallback] V2 报价失败,尝试 V3...`);
|
|
21
28
|
}
|
|
22
|
-
// 2.
|
|
29
|
+
// 2. 尝试 V3 报价
|
|
23
30
|
try {
|
|
24
|
-
|
|
31
|
+
// 先尝试从 Portal 获取代币的 lpFeeProfile 来确定正确的 fee
|
|
32
|
+
let preferredFee;
|
|
33
|
+
try {
|
|
34
|
+
const portalQuery = new PortalQuery({ rpcUrl, chainId });
|
|
35
|
+
const tokenState = await portalQuery.getTokenV7(tokenAddress);
|
|
36
|
+
preferredFee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
|
|
37
|
+
console.log(`[quoteTokenToOkbWithFallback] 从 Portal 获取 lpFeeProfile=${tokenState.lpFeeProfile} → fee=${preferredFee}`);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// 无法获取 lpFeeProfile(可能是纯外盘代币),让 quoteV3 自动尝试所有费率
|
|
41
|
+
console.log(`[quoteTokenToOkbWithFallback] 无法获取 lpFeeProfile,将尝试所有 V3 费率档位`);
|
|
42
|
+
}
|
|
43
|
+
// 使用 V3 Quoter 报价(Token → WOKB)
|
|
44
|
+
// 如果有 preferredFee 则优先尝试,否则自动尝试所有费率 [100, 500, 2500, 10000]
|
|
45
|
+
const provider = new JsonRpcProvider(rpcUrl, { chainId, name: 'xlayer', ensAddress: undefined });
|
|
46
|
+
const quoteResult = await quoteV3(provider, tokenAddress, WOKB, tokenAmount, 'XLAYER', preferredFee);
|
|
47
|
+
if (quoteResult.amountOut > 0n) {
|
|
48
|
+
console.log(`[quoteTokenToOkbWithFallback] V3 报价成功 (fee=${quoteResult.fee}): ${ethers.formatEther(quoteResult.amountOut)} OKB`);
|
|
49
|
+
return quoteResult.amountOut;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// V3 报价失败,继续尝试 Flap
|
|
54
|
+
console.log(`[quoteTokenToOkbWithFallback] V3 报价失败,尝试 Flap Portal...`);
|
|
55
|
+
}
|
|
56
|
+
// 3. 回退到 Flap Portal 报价
|
|
57
|
+
try {
|
|
58
|
+
const portalQuery = new PortalQuery({ rpcUrl, chainId });
|
|
25
59
|
const okbOut = await portalQuery.quoteExactInput(tokenAddress, ZERO_ADDRESS, tokenAmount);
|
|
26
|
-
if (okbOut > 0n)
|
|
60
|
+
if (okbOut > 0n) {
|
|
61
|
+
console.log(`[quoteTokenToOkbWithFallback] Flap Portal 报价成功: ${ethers.formatEther(okbOut)} OKB`);
|
|
27
62
|
return okbOut;
|
|
63
|
+
}
|
|
28
64
|
}
|
|
29
|
-
catch {
|
|
65
|
+
catch (e) {
|
|
30
66
|
// Flap 报价也失败
|
|
67
|
+
console.log(`[quoteTokenToOkbWithFallback] Flap Portal 报价失败`);
|
|
31
68
|
}
|
|
69
|
+
console.warn(`[quoteTokenToOkbWithFallback] 所有报价方式都失败,无法获取 ${tokenAddress} 的 OKB 价值`);
|
|
32
70
|
return 0n;
|
|
33
71
|
}
|
|
34
72
|
function clampBps(bps) {
|
|
@@ -88,10 +126,11 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
88
126
|
]);
|
|
89
127
|
// 计算总金额与利润
|
|
90
128
|
// ✅ Native:利润从总额中扣除,保留在 AA 钱包
|
|
91
|
-
// ✅ ERC20
|
|
129
|
+
// ✅ ERC20:优先以 OKB 形式收取利润,报价失败时直接刮取 ERC20 代币
|
|
92
130
|
let totalWei = 0n;
|
|
93
|
-
let profitWei = 0n; // ✅
|
|
94
|
-
let
|
|
131
|
+
let profitWei = 0n; // ✅ OKB 利润(当报价成功时)
|
|
132
|
+
let profitTokenWei = 0n; // ✅ ERC20 代币利润(当报价失败时)
|
|
133
|
+
let profitIsOkb = false; // ✅ 标记利润是否为 OKB
|
|
95
134
|
if (params.kind === 'native') {
|
|
96
135
|
const values = amtList.map((x) => {
|
|
97
136
|
const v = ethers.parseEther(String(x || '0'));
|
|
@@ -113,29 +152,43 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
113
152
|
return v > 0n ? v : 0n;
|
|
114
153
|
});
|
|
115
154
|
totalWei = values.reduce((a, b) => a + b, 0n);
|
|
116
|
-
// ✅ ERC20
|
|
117
|
-
//
|
|
155
|
+
// ✅ ERC20:尝试报价获取 OKB 价值
|
|
156
|
+
// 报价成功 → 收取 OKB 利润
|
|
157
|
+
// 报价失败 → 直接刮取 ERC20 代币作为利润(不跳过!)
|
|
118
158
|
if (totalWei > 0n) {
|
|
119
159
|
const okbValueWei = await quoteTokenToOkbWithFallback(totalWei, token, { rpcUrl: effConfig.rpcUrl, chainId: effConfig.chainId });
|
|
120
160
|
if (okbValueWei > 0n) {
|
|
121
161
|
profitWei = calcProfitWei(okbValueWei, PROFIT_CONFIG.RATE_BPS);
|
|
162
|
+
profitTokenWei = 0n;
|
|
122
163
|
profitIsOkb = true;
|
|
123
|
-
console.log(`[AA 分发] ERC20
|
|
164
|
+
console.log(`[AA 分发] ERC20 报价成功: ${ethers.formatUnits(totalWei, dec)} Token = ${ethers.formatEther(okbValueWei)} OKB, 利润: ${ethers.formatEther(profitWei)} OKB`);
|
|
124
165
|
}
|
|
125
166
|
else {
|
|
126
|
-
|
|
167
|
+
// ✅ 报价失败:直接从代币金额中扣除利润(ERC20 代币形式)
|
|
168
|
+
profitTokenWei = calcProfitWei(totalWei, PROFIT_CONFIG.RATE_BPS);
|
|
127
169
|
profitWei = 0n;
|
|
128
170
|
profitIsOkb = false;
|
|
171
|
+
console.warn(`[AA 分发] ERC20 报价失败,改为刮取 ERC20 代币: ${ethers.formatUnits(profitTokenWei, dec)} Token`);
|
|
129
172
|
}
|
|
130
173
|
}
|
|
131
174
|
}
|
|
132
|
-
// ✅
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
// ✅ 计算可分发金额
|
|
176
|
+
// Native:扣除利润后分发
|
|
177
|
+
// ERC20(报价成功):全额分发代币,利润另外以 OKB 转账
|
|
178
|
+
// ERC20(报价失败):扣除代币利润后分发
|
|
179
|
+
let distributableWei;
|
|
180
|
+
if (params.kind === 'native') {
|
|
181
|
+
distributableWei = totalWei > profitWei ? (totalWei - profitWei) : 0n;
|
|
182
|
+
}
|
|
183
|
+
else if (profitIsOkb) {
|
|
184
|
+
// 报价成功:全额分发代币
|
|
185
|
+
distributableWei = totalWei;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// 报价失败:扣除代币利润后分发
|
|
189
|
+
distributableWei = totalWei > profitTokenWei ? (totalWei - profitTokenWei) : 0n;
|
|
190
|
+
}
|
|
136
191
|
// 按比例缩放每个收款金额
|
|
137
|
-
// ✅ Native:使"总分发=总额-利润";利润保留在 AA 钱包
|
|
138
|
-
// ✅ ERC20:全额分发代币,利润以 OKB 单独转账
|
|
139
192
|
const scaled = [];
|
|
140
193
|
if (totalWei > 0n && distributableWei > 0n) {
|
|
141
194
|
if (params.kind === 'native') {
|
|
@@ -162,11 +215,28 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
162
215
|
}
|
|
163
216
|
}
|
|
164
217
|
else {
|
|
165
|
-
// ✅ ERC20
|
|
218
|
+
// ✅ ERC20:按比例缩放分发金额
|
|
166
219
|
const dec = Number(params.tokenDecimals);
|
|
167
|
-
|
|
220
|
+
const raw = amtList.map((x) => {
|
|
168
221
|
const v = ethers.parseUnits(String(x || '0'), dec);
|
|
169
|
-
|
|
222
|
+
return v > 0n ? v : 0n;
|
|
223
|
+
});
|
|
224
|
+
let acc = 0n;
|
|
225
|
+
for (let i = 0; i < raw.length; i++) {
|
|
226
|
+
const v = raw[i];
|
|
227
|
+
const out = (v * distributableWei) / totalWei;
|
|
228
|
+
scaled.push(out);
|
|
229
|
+
acc += out;
|
|
230
|
+
}
|
|
231
|
+
// 把余数补到最后一个非 0 位置
|
|
232
|
+
const rem = distributableWei - acc;
|
|
233
|
+
if (rem > 0n) {
|
|
234
|
+
for (let i = scaled.length - 1; i >= 0; i--) {
|
|
235
|
+
if (scaled[i] > 0n || raw[i] > 0n) {
|
|
236
|
+
scaled[i] = scaled[i] + rem;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
170
240
|
}
|
|
171
241
|
}
|
|
172
242
|
}
|
|
@@ -244,9 +314,13 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
244
314
|
}
|
|
245
315
|
else {
|
|
246
316
|
// ERC20 转账:executeBatch 或 Multicall3
|
|
247
|
-
// ✅ 如果有 OKB 利润转账,必须使用 Multicall3(SimpleAccount 不支持带 values 的 executeBatch)
|
|
248
317
|
const token = String(params.tokenAddress || '').trim();
|
|
318
|
+
// ✅ 利润处理:
|
|
319
|
+
// - 报价成功(profitIsOkb):OKB 利润转账
|
|
320
|
+
// - 报价失败(!profitIsOkb && profitTokenWei > 0n):ERC20 代币利润转账
|
|
249
321
|
const hasOkbProfit = i === 0 && profitIsOkb && profitWei > 0n;
|
|
322
|
+
const hasTokenProfit = i === 0 && !profitIsOkb && profitTokenWei > 0n;
|
|
323
|
+
const hasAnyProfit = hasOkbProfit || hasTokenProfit;
|
|
250
324
|
let callData;
|
|
251
325
|
if (hasOkbProfit) {
|
|
252
326
|
// ✅ 使用 Multicall3:ERC20 转账 + OKB 利润转账
|
|
@@ -266,8 +340,24 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
266
340
|
});
|
|
267
341
|
callData = encodeExecuteViaMulticall3(calls);
|
|
268
342
|
}
|
|
343
|
+
else if (hasTokenProfit) {
|
|
344
|
+
// ✅ 报价失败:ERC20 转账 + ERC20 代币利润转账
|
|
345
|
+
const dests = [];
|
|
346
|
+
const values = [];
|
|
347
|
+
const datas = [];
|
|
348
|
+
for (const it of list) {
|
|
349
|
+
dests.push(token);
|
|
350
|
+
values.push(0n);
|
|
351
|
+
datas.push(erc20Iface.encodeFunctionData('transfer', [it.to, it.amountWei]));
|
|
352
|
+
}
|
|
353
|
+
// 添加 ERC20 代币利润转账
|
|
354
|
+
dests.push(token);
|
|
355
|
+
values.push(0n);
|
|
356
|
+
datas.push(erc20Iface.encodeFunctionData('transfer', [PROFIT_CONFIG.RECIPIENT, profitTokenWei]));
|
|
357
|
+
callData = encodeExecuteBatch(dests, values, datas);
|
|
358
|
+
}
|
|
269
359
|
else {
|
|
270
|
-
// 无利润:使用 executeBatch
|
|
360
|
+
// 无利润:使用 executeBatch
|
|
271
361
|
const dests = [];
|
|
272
362
|
const values = [];
|
|
273
363
|
const datas = [];
|
|
@@ -281,8 +371,8 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
281
371
|
const nonce = nonce0 + BigInt(i);
|
|
282
372
|
const initCode = i === 0 ? String(initCode0) : '0x';
|
|
283
373
|
const deployed = i === 0 ? deployed0 : true;
|
|
284
|
-
// ✅ 计算 gas
|
|
285
|
-
const transferCount = list.length + (
|
|
374
|
+
// ✅ 计算 gas,包含利润转账
|
|
375
|
+
const transferCount = list.length + (hasAnyProfit ? 1 : 0);
|
|
286
376
|
const base = hasOkbProfit ? 250000n : 220000n; // Multicall3 需要更多基础 gas
|
|
287
377
|
const per = 65000n;
|
|
288
378
|
const g = base + per * BigInt(transferCount);
|
|
@@ -300,7 +390,9 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
300
390
|
ops.push(signed.userOp);
|
|
301
391
|
}
|
|
302
392
|
}
|
|
303
|
-
|
|
393
|
+
// ✅ 返回利润:OKB 利润或 ERC20 代币利润(用 profitWei 表示,方便上层处理)
|
|
394
|
+
const finalProfitWei = profitIsOkb ? profitWei : profitTokenWei;
|
|
395
|
+
return { ops, sender, profitWei: finalProfitWei };
|
|
304
396
|
}
|
|
305
397
|
/**
|
|
306
398
|
* ✅ XLayer AA:多 owner 归集/转账(SDK 安全版,内置利润)
|
|
@@ -382,7 +474,7 @@ export async function buildTransfersWithProfit(params) {
|
|
|
382
474
|
return (totalGas * gasPrice * 130n) / 100n;
|
|
383
475
|
};
|
|
384
476
|
// ✅ 第一步:计算每个钱包的归集金额和总利润
|
|
385
|
-
//
|
|
477
|
+
// 利润从每个钱包的归集金额中扣除
|
|
386
478
|
const transferInfos = [];
|
|
387
479
|
for (let i = 0; i < ownerWallets.length; i++) {
|
|
388
480
|
const sender = senders[i];
|
|
@@ -406,55 +498,66 @@ export async function buildTransfersWithProfit(params) {
|
|
|
406
498
|
continue;
|
|
407
499
|
const deployed = accountInfos[i]?.deployed ?? false;
|
|
408
500
|
const prefundWei = params.kind === 'native' ? estimateTransferPrefund(deployed) : 0n;
|
|
409
|
-
// ✅
|
|
410
|
-
// ✅ ERC20:代币全额分发,利润以 OKB 形式收取(需报价)
|
|
501
|
+
// ✅ 利润处理
|
|
411
502
|
let profitWei = 0n;
|
|
503
|
+
let profitTokenWei = 0n;
|
|
504
|
+
let profitIsOkb = false;
|
|
412
505
|
let toWei = amountWei;
|
|
413
506
|
if (params.kind === 'native') {
|
|
414
507
|
profitWei = calcProfitWei(amountWei, PROFIT_CONFIG.RATE_BPS);
|
|
508
|
+
profitIsOkb = false; // Native 模式下利润就是 OKB,但走 Native 逻辑
|
|
415
509
|
// ✅ 关键修复:toWei 需要扣除 max(profitWei, prefundWei),确保留足够的 gas
|
|
416
510
|
const reserveWei = profitWei > prefundWei ? profitWei : prefundWei;
|
|
417
511
|
toWei = amountWei > reserveWei ? (amountWei - reserveWei) : 0n;
|
|
418
512
|
totalProfitWei += profitWei;
|
|
419
513
|
}
|
|
420
514
|
else {
|
|
421
|
-
// ✅ ERC20
|
|
422
|
-
// 和 bundle.ts/wash-ops.ts 保持一致:先尝试 V2,失败后回退到 Flap Portal
|
|
423
|
-
toWei = amountWei;
|
|
515
|
+
// ✅ ERC20:尝试报价,报价失败则直接刮取 ERC20 代币
|
|
424
516
|
const token = String(params.tokenAddress || '').trim();
|
|
425
|
-
// ✅ 关键修复:检查 AA 钱包是否有足够的 OKB 余额支付利润
|
|
426
|
-
// 如果 AA 钱包 OKB 余额不足,跳过利润收取,只转 ERC20 代币
|
|
427
517
|
const aaOkbBalance = accountInfos[i]?.okbBalance ?? 0n;
|
|
428
518
|
const okbValueWei = await quoteTokenToOkbWithFallback(amountWei, token, { rpcUrl: effConfig.rpcUrl, chainId: effConfig.chainId });
|
|
429
519
|
if (okbValueWei > 0n) {
|
|
430
520
|
const calculatedProfit = calcProfitWei(okbValueWei, PROFIT_CONFIG.RATE_BPS);
|
|
431
|
-
// ✅ 只有当 AA 钱包 OKB 余额 >= 利润 + prefund 时才收取利润
|
|
432
|
-
// prefund 预留 0.0001 OKB (100_000_000_000_000 wei)
|
|
433
521
|
const minReserve = 100000000000000n; // 0.0001 OKB
|
|
434
522
|
if (aaOkbBalance >= calculatedProfit + minReserve) {
|
|
523
|
+
// ✅ 报价成功且余额充足:收取 OKB 利润
|
|
435
524
|
profitWei = calculatedProfit;
|
|
525
|
+
profitTokenWei = 0n;
|
|
526
|
+
profitIsOkb = true;
|
|
527
|
+
toWei = amountWei; // ERC20 全额转账
|
|
436
528
|
totalProfitWei += profitWei;
|
|
529
|
+
console.log(`[AA 归集] 钱包 ${i} 报价成功: 收取 ${ethers.formatEther(profitWei)} OKB 利润`);
|
|
437
530
|
}
|
|
438
531
|
else {
|
|
439
|
-
|
|
532
|
+
// ✅ 余额不足:改为刮取 ERC20 代币
|
|
533
|
+
profitTokenWei = calcProfitWei(amountWei, PROFIT_CONFIG.RATE_BPS);
|
|
440
534
|
profitWei = 0n;
|
|
535
|
+
profitIsOkb = false;
|
|
536
|
+
toWei = amountWei > profitTokenWei ? (amountWei - profitTokenWei) : 0n;
|
|
537
|
+
totalProfitWei += profitTokenWei;
|
|
538
|
+
console.warn(`[AA 归集] 钱包 ${i} OKB 余额不足,改为刮取 ERC20 代币: ${ethers.formatUnits(profitTokenWei, Number(params.tokenDecimals))} Token`);
|
|
441
539
|
}
|
|
442
540
|
}
|
|
443
541
|
else {
|
|
444
|
-
|
|
542
|
+
// ✅ 报价失败:直接刮取 ERC20 代币(不跳过!)
|
|
543
|
+
profitTokenWei = calcProfitWei(amountWei, PROFIT_CONFIG.RATE_BPS);
|
|
445
544
|
profitWei = 0n;
|
|
545
|
+
profitIsOkb = false;
|
|
546
|
+
toWei = amountWei > profitTokenWei ? (amountWei - profitTokenWei) : 0n;
|
|
547
|
+
totalProfitWei += profitTokenWei;
|
|
548
|
+
console.warn(`[AA 归集] 钱包 ${i} 报价失败,改为刮取 ERC20 代币: ${ethers.formatUnits(profitTokenWei, Number(params.tokenDecimals))} Token`);
|
|
446
549
|
}
|
|
447
550
|
}
|
|
448
|
-
transferInfos.push({ walletIndex: i, sender, to, amountWei, profitWei, toWei, prefundWei });
|
|
551
|
+
transferInfos.push({ walletIndex: i, sender, to, amountWei, profitWei, profitTokenWei, profitIsOkb, toWei, prefundWei });
|
|
449
552
|
}
|
|
450
|
-
console.log(`[AA 归集] 钱包数量: ${transferInfos.length}, 总利润: ${ethers.formatEther(totalProfitWei)}
|
|
553
|
+
console.log(`[AA 归集] 钱包数量: ${transferInfos.length}, 总利润: ${ethers.formatEther(totalProfitWei)}`);
|
|
451
554
|
const unsigned = [];
|
|
452
555
|
const opOwnerIndex = [];
|
|
453
556
|
// ✅ 第二步:构建 UserOps
|
|
454
557
|
// Native: 单个 UserOp,转 toWei 给目标地址,利润保留在 AA 钱包
|
|
455
|
-
// ERC20:
|
|
456
|
-
// - UserOp 1
|
|
457
|
-
// - UserOp
|
|
558
|
+
// ERC20:
|
|
559
|
+
// - 报价成功(profitIsOkb):UserOp 1 转代币 + UserOp 2 转 OKB 利润
|
|
560
|
+
// - 报价失败(!profitIsOkb):单个 UserOp 用 executeBatch 转代币 + 代币利润
|
|
458
561
|
for (let idx = 0; idx < transferInfos.length; idx++) {
|
|
459
562
|
const info = transferInfos[idx];
|
|
460
563
|
const i = info.walletIndex;
|
|
@@ -478,38 +581,76 @@ export async function buildTransfersWithProfit(params) {
|
|
|
478
581
|
opOwnerIndex.push(i);
|
|
479
582
|
}
|
|
480
583
|
else {
|
|
481
|
-
// ✅ ERC20:两个 UserOp
|
|
482
584
|
const token = String(params.tokenAddress || '').trim();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
585
|
+
if (info.profitIsOkb && info.profitWei > 0n) {
|
|
586
|
+
// ✅ 报价成功:两个 UserOp(ERC20 转账 + OKB 利润转账)
|
|
587
|
+
// UserOp 1: 转 ERC20 代币给目标地址
|
|
588
|
+
const transfer = erc20Iface.encodeFunctionData('transfer', [info.to, info.toWei]);
|
|
589
|
+
const erc20CallData = encodeExecute(token, 0n, transfer);
|
|
590
|
+
const erc20Built = await aaManager.buildUserOpWithFixedGas({
|
|
591
|
+
ownerWallet: w,
|
|
592
|
+
sender: info.sender,
|
|
593
|
+
callData: erc20CallData,
|
|
594
|
+
nonce: currentNonce,
|
|
595
|
+
initCode,
|
|
596
|
+
deployed: initCode === '0x',
|
|
597
|
+
fixedGas: { callGasLimit: 200000n },
|
|
598
|
+
});
|
|
599
|
+
unsigned.push(erc20Built.userOp);
|
|
600
|
+
opOwnerIndex.push(i);
|
|
601
|
+
currentNonce += 1n;
|
|
602
|
+
// UserOp 2: 转 OKB 利润给利润接收者
|
|
500
603
|
const profitCallData = encodeExecute(PROFIT_CONFIG.RECIPIENT, info.profitWei, '0x');
|
|
501
604
|
const profitBuilt = await aaManager.buildUserOpWithFixedGas({
|
|
502
605
|
ownerWallet: w,
|
|
503
606
|
sender: info.sender,
|
|
504
607
|
callData: profitCallData,
|
|
505
608
|
nonce: currentNonce,
|
|
506
|
-
initCode: '0x',
|
|
609
|
+
initCode: '0x',
|
|
507
610
|
deployed: true,
|
|
508
611
|
fixedGas: { callGasLimit: 120000n },
|
|
509
612
|
});
|
|
510
613
|
unsigned.push(profitBuilt.userOp);
|
|
511
614
|
opOwnerIndex.push(i);
|
|
512
|
-
console.log(`[AA 归集] 钱包 ${i}
|
|
615
|
+
console.log(`[AA 归集] 钱包 ${i} 创建 OKB 利润转账 UserOp: ${ethers.formatEther(info.profitWei)} OKB → ${PROFIT_CONFIG.RECIPIENT}`);
|
|
616
|
+
}
|
|
617
|
+
else if (!info.profitIsOkb && info.profitTokenWei > 0n) {
|
|
618
|
+
// ✅ 报价失败:单个 UserOp,使用 executeBatch 同时转代币和代币利润
|
|
619
|
+
const dests = [token, token];
|
|
620
|
+
const values = [0n, 0n];
|
|
621
|
+
const datas = [
|
|
622
|
+
erc20Iface.encodeFunctionData('transfer', [info.to, info.toWei]),
|
|
623
|
+
erc20Iface.encodeFunctionData('transfer', [PROFIT_CONFIG.RECIPIENT, info.profitTokenWei]),
|
|
624
|
+
];
|
|
625
|
+
const batchCallData = encodeExecuteBatch(dests, values, datas);
|
|
626
|
+
const batchBuilt = await aaManager.buildUserOpWithFixedGas({
|
|
627
|
+
ownerWallet: w,
|
|
628
|
+
sender: info.sender,
|
|
629
|
+
callData: batchCallData,
|
|
630
|
+
nonce: currentNonce,
|
|
631
|
+
initCode,
|
|
632
|
+
deployed: initCode === '0x',
|
|
633
|
+
fixedGas: { callGasLimit: 300000n }, // 批量操作需要更多 gas
|
|
634
|
+
});
|
|
635
|
+
unsigned.push(batchBuilt.userOp);
|
|
636
|
+
opOwnerIndex.push(i);
|
|
637
|
+
console.log(`[AA 归集] 钱包 ${i} 创建 ERC20 代币利润转账: ${ethers.formatUnits(info.profitTokenWei, Number(params.tokenDecimals))} Token → ${PROFIT_CONFIG.RECIPIENT}`);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// ✅ 无利润:只转代币
|
|
641
|
+
const transfer = erc20Iface.encodeFunctionData('transfer', [info.to, info.toWei]);
|
|
642
|
+
const erc20CallData = encodeExecute(token, 0n, transfer);
|
|
643
|
+
const erc20Built = await aaManager.buildUserOpWithFixedGas({
|
|
644
|
+
ownerWallet: w,
|
|
645
|
+
sender: info.sender,
|
|
646
|
+
callData: erc20CallData,
|
|
647
|
+
nonce: currentNonce,
|
|
648
|
+
initCode,
|
|
649
|
+
deployed: initCode === '0x',
|
|
650
|
+
fixedGas: { callGasLimit: 200000n },
|
|
651
|
+
});
|
|
652
|
+
unsigned.push(erc20Built.userOp);
|
|
653
|
+
opOwnerIndex.push(i);
|
|
513
654
|
}
|
|
514
655
|
}
|
|
515
656
|
}
|