four-flap-meme-sdk 1.5.30 → 1.5.32
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/utils/wallet.js
CHANGED
|
@@ -15,6 +15,13 @@ const MULTICALL3_BY_CHAIN = {
|
|
|
15
15
|
2818: DEFAULT_MULTICALL3, // Morph
|
|
16
16
|
143: DEFAULT_MULTICALL3, // Monad
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* ✅ Multicall3 每批最多查询的地址数量
|
|
20
|
+
* - 考虑因素:RPC 节点 gas 限制(通常 50M)、返回数据大小限制、超时风险
|
|
21
|
+
* - 每个地址的调用数 = 1 (native) + N (tokens)
|
|
22
|
+
* - 安全值:100 个地址,即使查询 10 个代币也只有 ~1100 个 calls
|
|
23
|
+
*/
|
|
24
|
+
const MULTICALL_BATCH_SIZE = 100;
|
|
18
25
|
// ============================================================================
|
|
19
26
|
// 钱包生成与验证
|
|
20
27
|
// ============================================================================
|
|
@@ -66,8 +73,28 @@ function normalizePrivateKey(input) {
|
|
|
66
73
|
// ============================================================================
|
|
67
74
|
/**
|
|
68
75
|
* ✅ 内部:批量查询多代币余额(统一逻辑)
|
|
76
|
+
* ✅ 自动分批:如果地址数量超过 MULTICALL_BATCH_SIZE,自动分批查询并合并结果
|
|
69
77
|
*/
|
|
70
78
|
async function _queryMultiTokenBalances(provider, multicallAddress, tokenAddresses, holders) {
|
|
79
|
+
if (!holders?.length)
|
|
80
|
+
return [];
|
|
81
|
+
// ✅ 如果地址数量超过批次上限,分批处理
|
|
82
|
+
if (holders.length > MULTICALL_BATCH_SIZE) {
|
|
83
|
+
const allResults = [];
|
|
84
|
+
for (let i = 0; i < holders.length; i += MULTICALL_BATCH_SIZE) {
|
|
85
|
+
const batchHolders = holders.slice(i, i + MULTICALL_BATCH_SIZE);
|
|
86
|
+
const batchResults = await _queryMultiTokenBalancesSingle(provider, multicallAddress, tokenAddresses, batchHolders);
|
|
87
|
+
allResults.push(...batchResults);
|
|
88
|
+
}
|
|
89
|
+
return allResults;
|
|
90
|
+
}
|
|
91
|
+
// ✅ 地址数量在限制内,直接查询
|
|
92
|
+
return _queryMultiTokenBalancesSingle(provider, multicallAddress, tokenAddresses, holders);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* ✅ 内部:单批次查询多代币余额(不超过 MULTICALL_BATCH_SIZE 个地址)
|
|
96
|
+
*/
|
|
97
|
+
async function _queryMultiTokenBalancesSingle(provider, multicallAddress, tokenAddresses, holders) {
|
|
71
98
|
if (!holders?.length)
|
|
72
99
|
return [];
|
|
73
100
|
const erc20Iface = new Interface(ERC20_ABI);
|
|
@@ -153,8 +180,27 @@ async function _queryMultiTokenBalances(provider, multicallAddress, tokenAddress
|
|
|
153
180
|
}
|
|
154
181
|
/**
|
|
155
182
|
* ✅ 内部:查询单代币余额(旧接口兼容)
|
|
183
|
+
* ✅ 自动分批:如果地址数量超过 MULTICALL_BATCH_SIZE,自动分批查询并合并结果
|
|
156
184
|
*/
|
|
157
185
|
async function _querySingleTokenBalances(provider, multicallAddress, token, holders) {
|
|
186
|
+
if (!holders?.length)
|
|
187
|
+
return [];
|
|
188
|
+
// ✅ 如果地址数量超过批次上限,分批处理
|
|
189
|
+
if (holders.length > MULTICALL_BATCH_SIZE) {
|
|
190
|
+
const allResults = [];
|
|
191
|
+
for (let i = 0; i < holders.length; i += MULTICALL_BATCH_SIZE) {
|
|
192
|
+
const batchHolders = holders.slice(i, i + MULTICALL_BATCH_SIZE);
|
|
193
|
+
const batchResults = await _querySingleTokenBalancesSingle(provider, multicallAddress, token, batchHolders);
|
|
194
|
+
allResults.push(...batchResults);
|
|
195
|
+
}
|
|
196
|
+
return allResults;
|
|
197
|
+
}
|
|
198
|
+
return _querySingleTokenBalancesSingle(provider, multicallAddress, token, holders);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* ✅ 内部:单批次查询单代币余额(不超过 MULTICALL_BATCH_SIZE 个地址)
|
|
202
|
+
*/
|
|
203
|
+
async function _querySingleTokenBalancesSingle(provider, multicallAddress, token, holders) {
|
|
158
204
|
if (!holders?.length)
|
|
159
205
|
return [];
|
|
160
206
|
const erc20Iface = new Interface(ERC20_ABI);
|
package/dist/xlayer/bundle.js
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { Wallet, Interface, Contract } from 'ethers';
|
|
11
11
|
import { FLAP_PORTAL, ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, } from './constants.js';
|
|
12
|
-
import { AAAccountManager, encodeExecute } from './aa-account.js';
|
|
12
|
+
import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
|
|
13
13
|
import { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, parseOkb, formatOkb, } from './portal-ops.js';
|
|
14
14
|
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
15
|
+
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
15
16
|
// ============================================================================
|
|
16
17
|
// AA Nonce(EntryPoint nonce)本地分配器
|
|
17
18
|
// ============================================================================
|
|
@@ -72,6 +73,20 @@ const DEFAULT_CALL_GAS_LIMIT_BUY = DEFAULT_CALL_GAS_LIMIT_SELL; // buy 与 sell
|
|
|
72
73
|
const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
|
|
73
74
|
const DEFAULT_CALL_GAS_LIMIT_TRANSFER = 150000n;
|
|
74
75
|
const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
|
|
76
|
+
function resolveProfitSettings(config) {
|
|
77
|
+
const extractProfit = config?.extractProfit !== false; // 默认 true
|
|
78
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS;
|
|
79
|
+
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
80
|
+
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
81
|
+
return { extractProfit, profitBps, profitRecipient };
|
|
82
|
+
}
|
|
83
|
+
function calculateProfitWei(amountWei, profitBps) {
|
|
84
|
+
if (amountWei <= 0n)
|
|
85
|
+
return 0n;
|
|
86
|
+
if (!Number.isFinite(profitBps) || profitBps <= 0)
|
|
87
|
+
return 0n;
|
|
88
|
+
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
89
|
+
}
|
|
75
90
|
/**
|
|
76
91
|
* XLayer 捆绑交易执行器
|
|
77
92
|
*
|
|
@@ -418,7 +433,14 @@ export class BundleExecutor {
|
|
|
418
433
|
console.log(`\n[${params.ownerName ?? 'owner'}] 归集后可转出=0(余额不足以覆盖 prefund+reserve)`);
|
|
419
434
|
return null;
|
|
420
435
|
}
|
|
421
|
-
|
|
436
|
+
// ✅ 利润提取:从“归集金额”里按 bps 刮取一部分转到 profitRecipient
|
|
437
|
+
const effConfig = { ...(this.config ?? {}), ...(params.configOverride ?? {}) };
|
|
438
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
|
|
439
|
+
const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
|
|
440
|
+
const toOwnerWei = withdrawAmount - profitWei;
|
|
441
|
+
const callData = extractProfit && profitWei > 0n
|
|
442
|
+
? encodeExecuteBatch([profitRecipient, params.ownerWallet.address], [profitWei, toOwnerWei], ['0x', '0x'])
|
|
443
|
+
: encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
|
|
422
444
|
const { userOp } = gasPolicy === 'fixed'
|
|
423
445
|
? await this.aaManager.buildUserOpWithFixedGas({
|
|
424
446
|
ownerWallet: params.ownerWallet,
|
|
@@ -447,7 +469,12 @@ export class BundleExecutor {
|
|
|
447
469
|
nonce: params.nonce,
|
|
448
470
|
initCode: params.initCode,
|
|
449
471
|
});
|
|
450
|
-
|
|
472
|
+
if (extractProfit && profitWei > 0n) {
|
|
473
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(toOwnerWei)} OKB (profit: ${formatOkb(profitWei)} OKB -> ${profitRecipient})`);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
|
|
477
|
+
}
|
|
451
478
|
const signed = await this.aaManager.signUserOp(userOp, params.ownerWallet);
|
|
452
479
|
return { ...signed, prefundWei, ownerName: params.ownerName };
|
|
453
480
|
}
|
|
@@ -596,7 +623,7 @@ export class BundleExecutor {
|
|
|
596
623
|
* 多个地址同一笔 handleOps 卖出代币
|
|
597
624
|
*/
|
|
598
625
|
async bundleSell(params) {
|
|
599
|
-
const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
|
|
626
|
+
const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, } = params;
|
|
600
627
|
const sharedProvider = this.aaManager.getProvider();
|
|
601
628
|
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
602
629
|
const bundlerSigner = wallets[0];
|
|
@@ -702,6 +729,7 @@ export class BundleExecutor {
|
|
|
702
729
|
senderBalance: it.senderBalance,
|
|
703
730
|
reserveWei,
|
|
704
731
|
ownerName: `owner${it.i + 1}`,
|
|
732
|
+
configOverride: config,
|
|
705
733
|
});
|
|
706
734
|
if (signed?.userOp)
|
|
707
735
|
nonceMap.commit(it.sender, it.nonce);
|
|
@@ -723,7 +751,7 @@ export class BundleExecutor {
|
|
|
723
751
|
* 完整流程:买入 -> 授权 -> 卖出 -> 归集
|
|
724
752
|
*/
|
|
725
753
|
async bundleBuySell(params) {
|
|
726
|
-
const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
|
|
754
|
+
const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, } = params;
|
|
727
755
|
if (privateKeys.length !== buyAmounts.length) {
|
|
728
756
|
throw new Error('私钥数量和购买金额数量必须一致');
|
|
729
757
|
}
|
|
@@ -831,6 +859,7 @@ export class BundleExecutor {
|
|
|
831
859
|
senderBalance: it.senderBalance,
|
|
832
860
|
reserveWei,
|
|
833
861
|
ownerName: `owner${it.i + 1}`,
|
|
862
|
+
configOverride: config,
|
|
834
863
|
});
|
|
835
864
|
if (signed?.userOp)
|
|
836
865
|
nonceMap.commit(it.sender, it.nonce);
|
|
@@ -8,6 +8,21 @@ 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
|
+
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
12
|
+
function resolveProfitSettings(config) {
|
|
13
|
+
const extractProfit = config?.extractProfit !== false; // 默认 true(对齐 BSC)
|
|
14
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS;
|
|
15
|
+
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
16
|
+
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
17
|
+
return { extractProfit, profitBps, profitRecipient };
|
|
18
|
+
}
|
|
19
|
+
function calculateProfitWei(amountWei, profitBps) {
|
|
20
|
+
if (amountWei <= 0n)
|
|
21
|
+
return 0n;
|
|
22
|
+
if (!Number.isFinite(profitBps) || profitBps <= 0)
|
|
23
|
+
return 0n;
|
|
24
|
+
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
25
|
+
}
|
|
11
26
|
function getDexDeadline(minutes = 20) {
|
|
12
27
|
return Math.floor(Date.now() / 1000) + minutes * 60;
|
|
13
28
|
}
|
|
@@ -40,6 +55,8 @@ export class AADexSwapExecutor {
|
|
|
40
55
|
*/
|
|
41
56
|
async bundleSwapSign(params) {
|
|
42
57
|
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
58
|
+
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
59
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
43
60
|
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
|
|
44
61
|
const provider = this.aaManager.getProvider();
|
|
45
62
|
const sellerOwner = new Wallet(sellerPrivateKey, provider);
|
|
@@ -93,6 +110,16 @@ export class AADexSwapExecutor {
|
|
|
93
110
|
const quoted = await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
94
111
|
finalBuyAmountWei = (quoted * BigInt(10000 - slippageBps)) / 10000n;
|
|
95
112
|
}
|
|
113
|
+
// 用 quoteTokenToOkb 估算卖出输出(用于利润提取)
|
|
114
|
+
const quotedSellOutWei = await (async () => {
|
|
115
|
+
try {
|
|
116
|
+
return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return 0n;
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
96
123
|
const outOps = [];
|
|
97
124
|
if (needApprove) {
|
|
98
125
|
const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
@@ -118,6 +145,19 @@ export class AADexSwapExecutor {
|
|
|
118
145
|
signOnly: true,
|
|
119
146
|
});
|
|
120
147
|
outOps.push(signedSell.userOp);
|
|
148
|
+
// Profit op(紧跟 sell 之后)
|
|
149
|
+
if (extractProfit && profitWei > 0n) {
|
|
150
|
+
const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
|
|
151
|
+
const signedProfit = await this.aaManager.buildUserOpWithState({
|
|
152
|
+
ownerWallet: sellerOwner,
|
|
153
|
+
sender: sellerSender,
|
|
154
|
+
nonce: nonceMap.next(sellerSender),
|
|
155
|
+
initCode: consumeInitCode(sellerSender),
|
|
156
|
+
callData: profitCallData,
|
|
157
|
+
signOnly: true,
|
|
158
|
+
});
|
|
159
|
+
outOps.push(signedProfit.userOp);
|
|
160
|
+
}
|
|
121
161
|
// Buy op
|
|
122
162
|
const buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], buyerSender, getDexDeadline());
|
|
123
163
|
const buyCallData = encodeExecute(effectiveRouter, finalBuyAmountWei, buySwapData);
|
|
@@ -163,6 +203,11 @@ export class AADexSwapExecutor {
|
|
|
163
203
|
buyAmountWei: finalBuyAmountWei.toString(),
|
|
164
204
|
hasApprove: needApprove,
|
|
165
205
|
routeAddress,
|
|
206
|
+
extractProfit,
|
|
207
|
+
profitBps,
|
|
208
|
+
profitRecipient,
|
|
209
|
+
profitWei: profitWei.toString(),
|
|
210
|
+
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
166
211
|
},
|
|
167
212
|
};
|
|
168
213
|
}
|
|
@@ -171,6 +216,8 @@ export class AADexSwapExecutor {
|
|
|
171
216
|
*/
|
|
172
217
|
async bundleBatchSwapSign(params) {
|
|
173
218
|
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
219
|
+
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
220
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
174
221
|
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
|
|
175
222
|
const provider = this.aaManager.getProvider();
|
|
176
223
|
const sellerOwner = new Wallet(sellerPrivateKey, provider);
|
|
@@ -239,6 +286,28 @@ export class AADexSwapExecutor {
|
|
|
239
286
|
signOnly: true,
|
|
240
287
|
});
|
|
241
288
|
outOps.push(signedSell.userOp);
|
|
289
|
+
// Profit op:估算卖出输出,按比例刮取
|
|
290
|
+
const quotedSellOutWei = await (async () => {
|
|
291
|
+
try {
|
|
292
|
+
return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return 0n;
|
|
296
|
+
}
|
|
297
|
+
})();
|
|
298
|
+
const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
299
|
+
if (extractProfit && profitWei > 0n) {
|
|
300
|
+
const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
|
|
301
|
+
const signedProfit = await this.aaManager.buildUserOpWithState({
|
|
302
|
+
ownerWallet: sellerOwner,
|
|
303
|
+
sender: sellerAi.sender,
|
|
304
|
+
nonce: nonceMap.next(sellerAi.sender),
|
|
305
|
+
initCode: consumeInitCode(sellerAi.sender),
|
|
306
|
+
callData: profitCallData,
|
|
307
|
+
signOnly: true,
|
|
308
|
+
});
|
|
309
|
+
outOps.push(signedProfit.userOp);
|
|
310
|
+
}
|
|
242
311
|
// Batch Buy ops
|
|
243
312
|
const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
|
|
244
313
|
for (let i = 0; i < buyerOwners.length; i++) {
|
|
@@ -289,6 +358,11 @@ export class AADexSwapExecutor {
|
|
|
289
358
|
buyAmountsWei: buyAmountsWei.map(w => w.toString()),
|
|
290
359
|
hasApprove: needApprove,
|
|
291
360
|
routeAddress,
|
|
361
|
+
extractProfit,
|
|
362
|
+
profitBps,
|
|
363
|
+
profitRecipient,
|
|
364
|
+
profitWei: profitWei.toString(),
|
|
365
|
+
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
292
366
|
},
|
|
293
367
|
};
|
|
294
368
|
}
|
|
@@ -7,6 +7,21 @@ import { FLAP_PORTAL, ZERO_ADDRESS, } 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
|
+
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
11
|
+
function resolveProfitSettings(config) {
|
|
12
|
+
const extractProfit = config?.extractProfit !== false; // 默认 true(对齐 BSC)
|
|
13
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS;
|
|
14
|
+
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
15
|
+
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
16
|
+
return { extractProfit, profitBps, profitRecipient };
|
|
17
|
+
}
|
|
18
|
+
function calculateProfitWei(amountWei, profitBps) {
|
|
19
|
+
if (amountWei <= 0n)
|
|
20
|
+
return 0n;
|
|
21
|
+
if (!Number.isFinite(profitBps) || profitBps <= 0)
|
|
22
|
+
return 0n;
|
|
23
|
+
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* XLayer AA 内盘换手执行器
|
|
12
27
|
*/
|
|
@@ -25,6 +40,8 @@ export class AAPortalSwapExecutor {
|
|
|
25
40
|
*/
|
|
26
41
|
async bundleSwapSign(params) {
|
|
27
42
|
const { tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
43
|
+
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
44
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
28
45
|
const provider = this.aaManager.getProvider();
|
|
29
46
|
const sellerOwner = new Wallet(sellerPrivateKey, provider);
|
|
30
47
|
const buyerOwner = new Wallet(buyerPrivateKey, provider);
|
|
@@ -82,9 +99,12 @@ export class AAPortalSwapExecutor {
|
|
|
82
99
|
return await this.portalQuery.quoteExactInput(tokenAddress, ZERO_ADDRESS, sellAmountWei);
|
|
83
100
|
}
|
|
84
101
|
})();
|
|
102
|
+
const quotedSellOutWei = quoted;
|
|
85
103
|
const finalBuyAmountWei = buyAmountOkb
|
|
86
104
|
? parseOkb(String(buyAmountOkb))
|
|
87
105
|
: (quoted * BigInt(10000 - slippageBps)) / 10000n;
|
|
106
|
+
// 3.1 利润提取(从卖出输出 OKB 里按比例刮取)
|
|
107
|
+
const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
88
108
|
// 4. 构建 Ops
|
|
89
109
|
const outOps = [];
|
|
90
110
|
if (needApprove) {
|
|
@@ -111,6 +131,19 @@ export class AAPortalSwapExecutor {
|
|
|
111
131
|
signOnly: true,
|
|
112
132
|
});
|
|
113
133
|
outOps.push(signedSell.userOp);
|
|
134
|
+
// Profit op(紧跟 sell 之后,确保 sellerSender 先收到 OKB)
|
|
135
|
+
if (extractProfit && profitWei > 0n) {
|
|
136
|
+
const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
|
|
137
|
+
const signedProfit = await this.aaManager.buildUserOpWithState({
|
|
138
|
+
ownerWallet: sellerOwner,
|
|
139
|
+
sender: sellerSender,
|
|
140
|
+
nonce: nonceMap.next(sellerSender),
|
|
141
|
+
initCode: consumeInitCode(sellerSender),
|
|
142
|
+
callData: profitCallData,
|
|
143
|
+
signOnly: true,
|
|
144
|
+
});
|
|
145
|
+
outOps.push(signedProfit.userOp);
|
|
146
|
+
}
|
|
114
147
|
// Buy op
|
|
115
148
|
const buySwapData = encodeBuyCall(tokenAddress, finalBuyAmountWei, 0n);
|
|
116
149
|
const buyCallData = encodeExecute(FLAP_PORTAL, finalBuyAmountWei, buySwapData);
|
|
@@ -157,6 +190,11 @@ export class AAPortalSwapExecutor {
|
|
|
157
190
|
buyAmountWei: finalBuyAmountWei.toString(),
|
|
158
191
|
hasApprove: needApprove,
|
|
159
192
|
routeAddress,
|
|
193
|
+
extractProfit,
|
|
194
|
+
profitBps,
|
|
195
|
+
profitRecipient,
|
|
196
|
+
profitWei: profitWei.toString(),
|
|
197
|
+
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
160
198
|
},
|
|
161
199
|
};
|
|
162
200
|
}
|
|
@@ -165,6 +203,8 @@ export class AAPortalSwapExecutor {
|
|
|
165
203
|
*/
|
|
166
204
|
async bundleBatchSwapSign(params) {
|
|
167
205
|
const { tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
206
|
+
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
207
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
168
208
|
const provider = this.aaManager.getProvider();
|
|
169
209
|
const sellerOwner = new Wallet(sellerPrivateKey, provider);
|
|
170
210
|
const buyerOwners = buyerPrivateKeys.map(pk => new Wallet(pk, provider));
|
|
@@ -232,6 +272,27 @@ export class AAPortalSwapExecutor {
|
|
|
232
272
|
signOnly: true,
|
|
233
273
|
});
|
|
234
274
|
outOps.push(signedSell.userOp);
|
|
275
|
+
// Profit op:用 previewSell/quoteExactInput 估算卖出输出,再按比例刮取
|
|
276
|
+
let quotedSellOutWei = 0n;
|
|
277
|
+
try {
|
|
278
|
+
quotedSellOutWei = await this.portalQuery.previewSell(tokenAddress, sellAmountWei);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
quotedSellOutWei = await this.portalQuery.quoteExactInput(tokenAddress, ZERO_ADDRESS, sellAmountWei);
|
|
282
|
+
}
|
|
283
|
+
const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
284
|
+
if (extractProfit && profitWei > 0n) {
|
|
285
|
+
const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
|
|
286
|
+
const signedProfit = await this.aaManager.buildUserOpWithState({
|
|
287
|
+
ownerWallet: sellerOwner,
|
|
288
|
+
sender: sellerAi.sender,
|
|
289
|
+
nonce: nonceMap.next(sellerAi.sender),
|
|
290
|
+
initCode: consumeInitCode(sellerAi.sender),
|
|
291
|
+
callData: profitCallData,
|
|
292
|
+
signOnly: true,
|
|
293
|
+
});
|
|
294
|
+
outOps.push(signedProfit.userOp);
|
|
295
|
+
}
|
|
235
296
|
// Batch Buy ops
|
|
236
297
|
const buyAmountsWei = buyAmountsOkb.map(a => parseOkb(a));
|
|
237
298
|
for (let i = 0; i < buyerOwners.length; i++) {
|
|
@@ -282,6 +343,11 @@ export class AAPortalSwapExecutor {
|
|
|
282
343
|
buyAmountsWei: buyAmountsWei.map(w => w.toString()),
|
|
283
344
|
hasApprove: needApprove,
|
|
284
345
|
routeAddress,
|
|
346
|
+
extractProfit,
|
|
347
|
+
profitBps,
|
|
348
|
+
profitRecipient,
|
|
349
|
+
profitWei: profitWei.toString(),
|
|
350
|
+
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
285
351
|
},
|
|
286
352
|
};
|
|
287
353
|
}
|
package/dist/xlayer/types.d.ts
CHANGED
|
@@ -85,6 +85,21 @@ export interface XLayerConfig {
|
|
|
85
85
|
gasPolicy?: GasPolicy;
|
|
86
86
|
/** fixed 策略的默认 gas 配置(可被每次调用覆盖) */
|
|
87
87
|
fixedGas?: FixedGasConfig;
|
|
88
|
+
/**
|
|
89
|
+
* 是否提取利润(默认 true)
|
|
90
|
+
* - 对齐 BSC:默认会从“卖出得到的 OKB / 归集金额”中按比例刮取利润
|
|
91
|
+
*/
|
|
92
|
+
extractProfit?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* 利润比例(基点 bps,1 bps = 0.01%)
|
|
95
|
+
* - 默认使用公共常量 PROFIT_CONFIG.RATE_BPS(当前为 30 bps)
|
|
96
|
+
*/
|
|
97
|
+
profitBps?: number;
|
|
98
|
+
/**
|
|
99
|
+
* 利润接收地址
|
|
100
|
+
* - 默认使用公共常量 PROFIT_CONFIG.RECIPIENT
|
|
101
|
+
*/
|
|
102
|
+
profitRecipient?: string;
|
|
88
103
|
}
|
|
89
104
|
/**
|
|
90
105
|
* AA 账户信息
|
|
@@ -238,6 +253,16 @@ export interface BundleSwapResult {
|
|
|
238
253
|
buyAmountWei: string;
|
|
239
254
|
hasApprove: boolean;
|
|
240
255
|
routeAddress?: string;
|
|
256
|
+
/** 是否提取利润 */
|
|
257
|
+
extractProfit?: boolean;
|
|
258
|
+
/** 利润比例 bps */
|
|
259
|
+
profitBps?: number;
|
|
260
|
+
/** 利润接收地址 */
|
|
261
|
+
profitRecipient?: string;
|
|
262
|
+
/** 预估利润(OKB wei) */
|
|
263
|
+
profitWei?: string;
|
|
264
|
+
/** 预估卖出输出(OKB wei) */
|
|
265
|
+
quotedSellOutWei?: string;
|
|
241
266
|
};
|
|
242
267
|
}
|
|
243
268
|
/**
|
|
@@ -277,6 +302,16 @@ export interface BundleSwapSignResult {
|
|
|
277
302
|
buyAmountWei: string;
|
|
278
303
|
hasApprove: boolean;
|
|
279
304
|
routeAddress?: string;
|
|
305
|
+
/** 是否提取利润 */
|
|
306
|
+
extractProfit?: boolean;
|
|
307
|
+
/** 利润比例 bps */
|
|
308
|
+
profitBps?: number;
|
|
309
|
+
/** 利润接收地址 */
|
|
310
|
+
profitRecipient?: string;
|
|
311
|
+
/** 预估利润(OKB wei) */
|
|
312
|
+
profitWei?: string;
|
|
313
|
+
/** 预估卖出输出(OKB wei) */
|
|
314
|
+
quotedSellOutWei?: string;
|
|
280
315
|
};
|
|
281
316
|
}
|
|
282
317
|
/**
|
|
@@ -315,6 +350,16 @@ export interface BundleBatchSwapResult {
|
|
|
315
350
|
buyAmountsWei: string[];
|
|
316
351
|
hasApprove: boolean;
|
|
317
352
|
routeAddress?: string;
|
|
353
|
+
/** 是否提取利润 */
|
|
354
|
+
extractProfit?: boolean;
|
|
355
|
+
/** 利润比例 bps */
|
|
356
|
+
profitBps?: number;
|
|
357
|
+
/** 利润接收地址 */
|
|
358
|
+
profitRecipient?: string;
|
|
359
|
+
/** 预估利润(OKB wei) */
|
|
360
|
+
profitWei?: string;
|
|
361
|
+
/** 预估卖出输出(OKB wei) */
|
|
362
|
+
quotedSellOutWei?: string;
|
|
318
363
|
};
|
|
319
364
|
}
|
|
320
365
|
export interface BundleBatchSwapSignParams extends BundleBatchSwapParams {
|
|
@@ -337,6 +382,16 @@ export interface BundleBatchSwapSignResult {
|
|
|
337
382
|
buyAmountsWei: string[];
|
|
338
383
|
hasApprove: boolean;
|
|
339
384
|
routeAddress?: string;
|
|
385
|
+
/** 是否提取利润 */
|
|
386
|
+
extractProfit?: boolean;
|
|
387
|
+
/** 利润比例 bps */
|
|
388
|
+
profitBps?: number;
|
|
389
|
+
/** 利润接收地址 */
|
|
390
|
+
profitRecipient?: string;
|
|
391
|
+
/** 预估利润(OKB wei) */
|
|
392
|
+
profitWei?: string;
|
|
393
|
+
/** 预估卖出输出(OKB wei) */
|
|
394
|
+
quotedSellOutWei?: string;
|
|
340
395
|
};
|
|
341
396
|
}
|
|
342
397
|
/**
|