four-flap-meme-sdk 1.5.31 → 1.5.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/xlayer/bundle.js +80 -7
- package/dist/xlayer/dex-bundle-swap.js +217 -12
- package/dist/xlayer/dex-bundle.d.ts +42 -0
- package/dist/xlayer/dex-bundle.js +378 -0
- package/dist/xlayer/dex-volume.d.ts +30 -0
- package/dist/xlayer/dex-volume.js +80 -0
- package/dist/xlayer/index.d.ts +2 -0
- package/dist/xlayer/index.js +8 -0
- package/dist/xlayer/portal-bundle-swap.js +213 -6
- package/dist/xlayer/types.d.ts +96 -0
- package/package.json +1 -1
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,21 @@ 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
|
+
// ✅ 对齐 bundle/刷量模式默认费率(与 BSC bundle-buy-first 语义一致)
|
|
79
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
|
|
80
|
+
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
81
|
+
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
82
|
+
return { extractProfit, profitBps, profitRecipient };
|
|
83
|
+
}
|
|
84
|
+
function calculateProfitWei(amountWei, profitBps) {
|
|
85
|
+
if (amountWei <= 0n)
|
|
86
|
+
return 0n;
|
|
87
|
+
if (!Number.isFinite(profitBps) || profitBps <= 0)
|
|
88
|
+
return 0n;
|
|
89
|
+
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
90
|
+
}
|
|
75
91
|
/**
|
|
76
92
|
* XLayer 捆绑交易执行器
|
|
77
93
|
*
|
|
@@ -418,7 +434,14 @@ export class BundleExecutor {
|
|
|
418
434
|
console.log(`\n[${params.ownerName ?? 'owner'}] 归集后可转出=0(余额不足以覆盖 prefund+reserve)`);
|
|
419
435
|
return null;
|
|
420
436
|
}
|
|
421
|
-
|
|
437
|
+
// ✅ 利润提取:从“归集金额”里按 bps 刮取一部分转到 profitRecipient
|
|
438
|
+
const effConfig = { ...(this.config ?? {}), ...(params.configOverride ?? {}) };
|
|
439
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
|
|
440
|
+
const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
|
|
441
|
+
const toOwnerWei = withdrawAmount - profitWei;
|
|
442
|
+
const callData = extractProfit && profitWei > 0n
|
|
443
|
+
? encodeExecuteBatch([profitRecipient, params.ownerWallet.address], [profitWei, toOwnerWei], ['0x', '0x'])
|
|
444
|
+
: encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
|
|
422
445
|
const { userOp } = gasPolicy === 'fixed'
|
|
423
446
|
? await this.aaManager.buildUserOpWithFixedGas({
|
|
424
447
|
ownerWallet: params.ownerWallet,
|
|
@@ -447,7 +470,12 @@ export class BundleExecutor {
|
|
|
447
470
|
nonce: params.nonce,
|
|
448
471
|
initCode: params.initCode,
|
|
449
472
|
});
|
|
450
|
-
|
|
473
|
+
if (extractProfit && profitWei > 0n) {
|
|
474
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(toOwnerWei)} OKB (profit: ${formatOkb(profitWei)} OKB -> ${profitRecipient})`);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
|
|
478
|
+
}
|
|
451
479
|
const signed = await this.aaManager.signUserOp(userOp, params.ownerWallet);
|
|
452
480
|
return { ...signed, prefundWei, ownerName: params.ownerName };
|
|
453
481
|
}
|
|
@@ -596,7 +624,7 @@ export class BundleExecutor {
|
|
|
596
624
|
* 多个地址同一笔 handleOps 卖出代币
|
|
597
625
|
*/
|
|
598
626
|
async bundleSell(params) {
|
|
599
|
-
const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
|
|
627
|
+
const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, } = params;
|
|
600
628
|
const sharedProvider = this.aaManager.getProvider();
|
|
601
629
|
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
602
630
|
const bundlerSigner = wallets[0];
|
|
@@ -679,6 +707,9 @@ export class BundleExecutor {
|
|
|
679
707
|
}
|
|
680
708
|
// 3. 可选:归集 OKB
|
|
681
709
|
let withdrawResult;
|
|
710
|
+
let totalProfitWei = 0n;
|
|
711
|
+
const effProfitCfg = { ...(this.config ?? {}), ...(config ?? {}) };
|
|
712
|
+
const profitSettings = resolveProfitSettings(effProfitCfg);
|
|
682
713
|
if (withdrawToOwner) {
|
|
683
714
|
const withdrawOps = [];
|
|
684
715
|
// 批量获取 sender OKB 余额
|
|
@@ -702,7 +733,14 @@ export class BundleExecutor {
|
|
|
702
733
|
senderBalance: it.senderBalance,
|
|
703
734
|
reserveWei,
|
|
704
735
|
ownerName: `owner${it.i + 1}`,
|
|
736
|
+
configOverride: config,
|
|
705
737
|
});
|
|
738
|
+
if (signed && profitSettings.extractProfit) {
|
|
739
|
+
const withdrawAmount = it.senderBalance > signed.prefundWei + reserveWei
|
|
740
|
+
? it.senderBalance - signed.prefundWei - reserveWei
|
|
741
|
+
: 0n;
|
|
742
|
+
totalProfitWei += calculateProfitWei(withdrawAmount, profitSettings.profitBps);
|
|
743
|
+
}
|
|
706
744
|
if (signed?.userOp)
|
|
707
745
|
nonceMap.commit(it.sender, it.nonce);
|
|
708
746
|
return signed?.userOp ?? null;
|
|
@@ -715,7 +753,19 @@ export class BundleExecutor {
|
|
|
715
753
|
withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
|
|
716
754
|
}
|
|
717
755
|
}
|
|
718
|
-
return {
|
|
756
|
+
return {
|
|
757
|
+
approveResult,
|
|
758
|
+
sellResult,
|
|
759
|
+
withdrawResult,
|
|
760
|
+
profit: withdrawToOwner
|
|
761
|
+
? {
|
|
762
|
+
extractProfit: profitSettings.extractProfit,
|
|
763
|
+
profitBps: profitSettings.profitBps,
|
|
764
|
+
profitRecipient: profitSettings.profitRecipient,
|
|
765
|
+
totalProfitWei: totalProfitWei.toString(),
|
|
766
|
+
}
|
|
767
|
+
: undefined,
|
|
768
|
+
};
|
|
719
769
|
}
|
|
720
770
|
/**
|
|
721
771
|
* 捆绑买卖(先买后卖)
|
|
@@ -723,7 +773,7 @@ export class BundleExecutor {
|
|
|
723
773
|
* 完整流程:买入 -> 授权 -> 卖出 -> 归集
|
|
724
774
|
*/
|
|
725
775
|
async bundleBuySell(params) {
|
|
726
|
-
const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
|
|
776
|
+
const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, } = params;
|
|
727
777
|
if (privateKeys.length !== buyAmounts.length) {
|
|
728
778
|
throw new Error('私钥数量和购买金额数量必须一致');
|
|
729
779
|
}
|
|
@@ -810,6 +860,9 @@ export class BundleExecutor {
|
|
|
810
860
|
}
|
|
811
861
|
// 3. 可选:归集 OKB
|
|
812
862
|
let withdrawResult;
|
|
863
|
+
let totalProfitWei = 0n;
|
|
864
|
+
const effProfitCfg = { ...(this.config ?? {}), ...(config ?? {}) };
|
|
865
|
+
const profitSettings = resolveProfitSettings(effProfitCfg);
|
|
813
866
|
if (withdrawToOwner) {
|
|
814
867
|
const withdrawOps = [];
|
|
815
868
|
// 批量获取 OKB 余额(sell 后状态)
|
|
@@ -831,7 +884,14 @@ export class BundleExecutor {
|
|
|
831
884
|
senderBalance: it.senderBalance,
|
|
832
885
|
reserveWei,
|
|
833
886
|
ownerName: `owner${it.i + 1}`,
|
|
887
|
+
configOverride: config,
|
|
834
888
|
});
|
|
889
|
+
if (signed && profitSettings.extractProfit) {
|
|
890
|
+
const withdrawAmount = it.senderBalance > signed.prefundWei + reserveWei
|
|
891
|
+
? it.senderBalance - signed.prefundWei - reserveWei
|
|
892
|
+
: 0n;
|
|
893
|
+
totalProfitWei += calculateProfitWei(withdrawAmount, profitSettings.profitBps);
|
|
894
|
+
}
|
|
835
895
|
if (signed?.userOp)
|
|
836
896
|
nonceMap.commit(it.sender, it.nonce);
|
|
837
897
|
return signed?.userOp ?? null;
|
|
@@ -846,7 +906,20 @@ export class BundleExecutor {
|
|
|
846
906
|
}
|
|
847
907
|
// 最终余额
|
|
848
908
|
const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
|
|
849
|
-
return {
|
|
909
|
+
return {
|
|
910
|
+
buyResult,
|
|
911
|
+
sellResult,
|
|
912
|
+
withdrawResult,
|
|
913
|
+
finalBalances,
|
|
914
|
+
profit: withdrawToOwner
|
|
915
|
+
? {
|
|
916
|
+
extractProfit: profitSettings.extractProfit,
|
|
917
|
+
profitBps: profitSettings.profitBps,
|
|
918
|
+
profitRecipient: profitSettings.profitRecipient,
|
|
919
|
+
totalProfitWei: totalProfitWei.toString(),
|
|
920
|
+
}
|
|
921
|
+
: undefined,
|
|
922
|
+
};
|
|
850
923
|
}
|
|
851
924
|
}
|
|
852
925
|
// ============================================================================
|
|
@@ -3,11 +3,48 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Wallet, ethers } from 'ethers';
|
|
5
5
|
import { AANonceMap, } from './types.js';
|
|
6
|
-
import { POTATOSWAP_V2_ROUTER, WOKB, } from './constants.js';
|
|
6
|
+
import { POTATOSWAP_V2_ROUTER, WOKB, MULTICALL3, } from './constants.js';
|
|
7
7
|
import { AAAccountManager, encodeExecute } from './aa-account.js';
|
|
8
8
|
import { encodeApproveCall, } from './portal-ops.js';
|
|
9
9
|
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, } from './dex.js';
|
|
10
10
|
import { BundleExecutor } from './bundle.js';
|
|
11
|
+
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
12
|
+
const multicallIface = new ethers.Interface([
|
|
13
|
+
'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
|
|
14
|
+
]);
|
|
15
|
+
function chunkArray(arr, size) {
|
|
16
|
+
const out = [];
|
|
17
|
+
const n = Math.max(1, Math.floor(size));
|
|
18
|
+
for (let i = 0; i < arr.length; i += n)
|
|
19
|
+
out.push(arr.slice(i, i + n));
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function encodeNativeDisperseViaMulticall3(params) {
|
|
23
|
+
const calls = params.to.map((target, idx) => ({
|
|
24
|
+
target,
|
|
25
|
+
allowFailure: false,
|
|
26
|
+
value: params.values[idx] ?? 0n,
|
|
27
|
+
callData: '0x',
|
|
28
|
+
}));
|
|
29
|
+
const totalValue = params.values.reduce((a, b) => a + (b ?? 0n), 0n);
|
|
30
|
+
const data = multicallIface.encodeFunctionData('aggregate3Value', [calls]);
|
|
31
|
+
return { totalValue, data };
|
|
32
|
+
}
|
|
33
|
+
function resolveProfitSettings(config) {
|
|
34
|
+
const extractProfit = config?.extractProfit !== false; // 默认 true(对齐 BSC)
|
|
35
|
+
// ✅ 对齐 BSC “捆绑换手模式”默认费率
|
|
36
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
|
|
37
|
+
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
38
|
+
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
39
|
+
return { extractProfit, profitBps, profitRecipient };
|
|
40
|
+
}
|
|
41
|
+
function calculateProfitWei(amountWei, profitBps) {
|
|
42
|
+
if (amountWei <= 0n)
|
|
43
|
+
return 0n;
|
|
44
|
+
if (!Number.isFinite(profitBps) || profitBps <= 0)
|
|
45
|
+
return 0n;
|
|
46
|
+
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
47
|
+
}
|
|
11
48
|
function getDexDeadline(minutes = 20) {
|
|
12
49
|
return Math.floor(Date.now() / 1000) + minutes * 60;
|
|
13
50
|
}
|
|
@@ -39,7 +76,9 @@ export class AADexSwapExecutor {
|
|
|
39
76
|
* AA 外盘单钱包换手签名
|
|
40
77
|
*/
|
|
41
78
|
async bundleSwapSign(params) {
|
|
42
|
-
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
79
|
+
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
80
|
+
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
81
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
43
82
|
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
|
|
44
83
|
const provider = this.aaManager.getProvider();
|
|
45
84
|
const sellerOwner = new Wallet(sellerPrivateKey, provider);
|
|
@@ -84,15 +123,27 @@ export class AADexSwapExecutor {
|
|
|
84
123
|
const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerSender, effectiveRouter);
|
|
85
124
|
needApprove = allowance < sellAmountWei;
|
|
86
125
|
}
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
// 估算卖出输出(用于利润提取与 buy 预算)
|
|
127
|
+
const quotedSellOutWei = await (async () => {
|
|
128
|
+
try {
|
|
129
|
+
return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return 0n;
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
// buyAmount:若未传则按 quotedSellOut 计算;利润从 quotedSellOut 中刮取,但要保证买入资金充足
|
|
136
|
+
const requestedBuyWei = buyAmountOkb
|
|
137
|
+
? ethers.parseEther(String(buyAmountOkb))
|
|
138
|
+
: (quotedSellOutWei * BigInt(10000 - slippageBps)) / 10000n;
|
|
139
|
+
if (quotedSellOutWei > 0n && requestedBuyWei > quotedSellOutWei) {
|
|
140
|
+
throw new Error('AA 捆绑换手:buyAmountOkb 超过预估卖出输出(quotedSellOut)');
|
|
95
141
|
}
|
|
142
|
+
const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
143
|
+
const profitCap = quotedSellOutWei > requestedBuyWei ? (quotedSellOutWei - requestedBuyWei) : 0n;
|
|
144
|
+
const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
|
|
145
|
+
const finalBuyAmountWei = requestedBuyWei;
|
|
146
|
+
const hopCount = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
|
|
96
147
|
const outOps = [];
|
|
97
148
|
if (needApprove) {
|
|
98
149
|
const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
@@ -118,6 +169,32 @@ export class AADexSwapExecutor {
|
|
|
118
169
|
signOnly: true,
|
|
119
170
|
});
|
|
120
171
|
outOps.push(signedSell.userOp);
|
|
172
|
+
// Profit op(紧跟 sell 之后)
|
|
173
|
+
if (extractProfit && profitWei > 0n) {
|
|
174
|
+
const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
|
|
175
|
+
const signedProfit = await this.aaManager.buildUserOpWithState({
|
|
176
|
+
ownerWallet: sellerOwner,
|
|
177
|
+
sender: sellerSender,
|
|
178
|
+
nonce: nonceMap.next(sellerSender),
|
|
179
|
+
initCode: consumeInitCode(sellerSender),
|
|
180
|
+
callData: profitCallData,
|
|
181
|
+
signOnly: true,
|
|
182
|
+
});
|
|
183
|
+
outOps.push(signedProfit.userOp);
|
|
184
|
+
}
|
|
185
|
+
// ✅ 卖出所得 OKB 转给买方 AA(Sender)
|
|
186
|
+
if (sellerSender.toLowerCase() !== buyerSender.toLowerCase() && finalBuyAmountWei > 0n) {
|
|
187
|
+
const transferCallData = encodeExecute(buyerSender, finalBuyAmountWei, '0x');
|
|
188
|
+
const signedTransfer = await this.aaManager.buildUserOpWithState({
|
|
189
|
+
ownerWallet: sellerOwner,
|
|
190
|
+
sender: sellerSender,
|
|
191
|
+
nonce: nonceMap.next(sellerSender),
|
|
192
|
+
initCode: consumeInitCode(sellerSender),
|
|
193
|
+
callData: transferCallData,
|
|
194
|
+
signOnly: true,
|
|
195
|
+
});
|
|
196
|
+
outOps.push(signedTransfer.userOp);
|
|
197
|
+
}
|
|
121
198
|
// Buy op
|
|
122
199
|
const buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], buyerSender, getDexDeadline());
|
|
123
200
|
const buyCallData = encodeExecute(effectiveRouter, finalBuyAmountWei, buySwapData);
|
|
@@ -163,6 +240,12 @@ export class AADexSwapExecutor {
|
|
|
163
240
|
buyAmountWei: finalBuyAmountWei.toString(),
|
|
164
241
|
hasApprove: needApprove,
|
|
165
242
|
routeAddress,
|
|
243
|
+
extractProfit,
|
|
244
|
+
profitBps,
|
|
245
|
+
profitRecipient,
|
|
246
|
+
profitWei: profitWei.toString(),
|
|
247
|
+
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
248
|
+
disperseHopCount: String(hopCount),
|
|
166
249
|
},
|
|
167
250
|
};
|
|
168
251
|
}
|
|
@@ -170,7 +253,9 @@ export class AADexSwapExecutor {
|
|
|
170
253
|
* AA 外盘批量换手签名
|
|
171
254
|
*/
|
|
172
255
|
async bundleBatchSwapSign(params) {
|
|
173
|
-
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
256
|
+
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
|
|
257
|
+
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
258
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
174
259
|
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
|
|
175
260
|
const provider = this.aaManager.getProvider();
|
|
176
261
|
const sellerOwner = new Wallet(sellerPrivateKey, provider);
|
|
@@ -239,8 +324,122 @@ export class AADexSwapExecutor {
|
|
|
239
324
|
signOnly: true,
|
|
240
325
|
});
|
|
241
326
|
outOps.push(signedSell.userOp);
|
|
242
|
-
//
|
|
327
|
+
// 先计算 buyAmountsWei(用于后续分发与校验)
|
|
243
328
|
const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
|
|
329
|
+
const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
|
|
330
|
+
// Profit op:估算卖出输出,按比例刮取(但必须保证分发/买入资金充足)
|
|
331
|
+
const quotedSellOutWei = await (async () => {
|
|
332
|
+
try {
|
|
333
|
+
return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return 0n;
|
|
337
|
+
}
|
|
338
|
+
})();
|
|
339
|
+
if (quotedSellOutWei > 0n && totalBuyWei > quotedSellOutWei) {
|
|
340
|
+
throw new Error('AA 批量换手:buyAmountsOkb 总和超过预估卖出输出(quotedSellOut)');
|
|
341
|
+
}
|
|
342
|
+
const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
343
|
+
const profitCap = quotedSellOutWei > totalBuyWei ? (quotedSellOutWei - totalBuyWei) : 0n;
|
|
344
|
+
const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
|
|
345
|
+
if (extractProfit && profitWei > 0n) {
|
|
346
|
+
const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
|
|
347
|
+
const signedProfit = await this.aaManager.buildUserOpWithState({
|
|
348
|
+
ownerWallet: sellerOwner,
|
|
349
|
+
sender: sellerAi.sender,
|
|
350
|
+
nonce: nonceMap.next(sellerAi.sender),
|
|
351
|
+
initCode: consumeInitCode(sellerAi.sender),
|
|
352
|
+
callData: profitCallData,
|
|
353
|
+
signOnly: true,
|
|
354
|
+
});
|
|
355
|
+
outOps.push(signedProfit.userOp);
|
|
356
|
+
}
|
|
357
|
+
// ✅ 卖出所得 OKB 分发给多个买方 AA(Sender)(支持多跳)
|
|
358
|
+
const buyerSenders = buyerAis.map(ai => ai.sender);
|
|
359
|
+
const hopCountRaw = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
|
|
360
|
+
const hopCount = Math.min(hopCountRaw, buyerSenders.length);
|
|
361
|
+
const maxPerOp = Math.max(1, Math.floor(Number(effectiveConfig.maxTransfersPerUserOpNative ?? 30)));
|
|
362
|
+
if (buyerSenders.length > 0 && totalBuyWei > 0n) {
|
|
363
|
+
if (hopCount <= 0) {
|
|
364
|
+
const items = buyerSenders.map((to, i) => ({ to, value: buyAmountsWei[i] ?? 0n })).filter(x => x.value > 0n);
|
|
365
|
+
const chunks = chunkArray(items, maxPerOp);
|
|
366
|
+
for (const ch of chunks) {
|
|
367
|
+
const { totalValue, data } = encodeNativeDisperseViaMulticall3({
|
|
368
|
+
to: ch.map(x => x.to),
|
|
369
|
+
values: ch.map(x => x.value),
|
|
370
|
+
});
|
|
371
|
+
const callData = encodeExecute(MULTICALL3, totalValue, data);
|
|
372
|
+
const signedDisperse = await this.aaManager.buildUserOpWithState({
|
|
373
|
+
ownerWallet: sellerOwner,
|
|
374
|
+
sender: sellerAi.sender,
|
|
375
|
+
nonce: nonceMap.next(sellerAi.sender),
|
|
376
|
+
initCode: consumeInitCode(sellerAi.sender),
|
|
377
|
+
callData,
|
|
378
|
+
signOnly: true,
|
|
379
|
+
});
|
|
380
|
+
outOps.push(signedDisperse.userOp);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
const hopSenders = buyerSenders.slice(0, hopCount);
|
|
385
|
+
const hopOwners = buyerOwners.slice(0, hopCount);
|
|
386
|
+
const hop0 = hopSenders[0];
|
|
387
|
+
const callData0 = encodeExecute(hop0, totalBuyWei, '0x');
|
|
388
|
+
const signedToHop0 = await this.aaManager.buildUserOpWithState({
|
|
389
|
+
ownerWallet: sellerOwner,
|
|
390
|
+
sender: sellerAi.sender,
|
|
391
|
+
nonce: nonceMap.next(sellerAi.sender),
|
|
392
|
+
initCode: consumeInitCode(sellerAi.sender),
|
|
393
|
+
callData: callData0,
|
|
394
|
+
signOnly: true,
|
|
395
|
+
});
|
|
396
|
+
outOps.push(signedToHop0.userOp);
|
|
397
|
+
let prefixKept = 0n;
|
|
398
|
+
for (let j = 0; j < hopSenders.length - 1; j++) {
|
|
399
|
+
const sender = hopSenders[j];
|
|
400
|
+
const next = hopSenders[j + 1];
|
|
401
|
+
const keep = buyAmountsWei[j] ?? 0n;
|
|
402
|
+
prefixKept += keep;
|
|
403
|
+
const remaining = totalBuyWei - prefixKept;
|
|
404
|
+
if (remaining <= 0n)
|
|
405
|
+
break;
|
|
406
|
+
const callData = encodeExecute(next, remaining, '0x');
|
|
407
|
+
const signedHop = await this.aaManager.buildUserOpWithState({
|
|
408
|
+
ownerWallet: hopOwners[j],
|
|
409
|
+
sender,
|
|
410
|
+
nonce: nonceMap.next(sender),
|
|
411
|
+
initCode: consumeInitCode(sender),
|
|
412
|
+
callData,
|
|
413
|
+
signOnly: true,
|
|
414
|
+
});
|
|
415
|
+
outOps.push(signedHop.userOp);
|
|
416
|
+
}
|
|
417
|
+
const lastHopIdx = hopSenders.length - 1;
|
|
418
|
+
const lastHopSender = hopSenders[lastHopIdx];
|
|
419
|
+
const lastHopOwner = hopOwners[lastHopIdx];
|
|
420
|
+
const rest = buyerSenders.slice(hopSenders.length);
|
|
421
|
+
const restAmounts = buyAmountsWei.slice(hopSenders.length);
|
|
422
|
+
const restItems = rest.map((to, i) => ({ to, value: restAmounts[i] ?? 0n })).filter(x => x.value > 0n);
|
|
423
|
+
const restChunks = chunkArray(restItems, maxPerOp);
|
|
424
|
+
for (const ch of restChunks) {
|
|
425
|
+
const { totalValue, data } = encodeNativeDisperseViaMulticall3({
|
|
426
|
+
to: ch.map(x => x.to),
|
|
427
|
+
values: ch.map(x => x.value),
|
|
428
|
+
});
|
|
429
|
+
const callData = encodeExecute(MULTICALL3, totalValue, data);
|
|
430
|
+
const signedDisperse = await this.aaManager.buildUserOpWithState({
|
|
431
|
+
ownerWallet: lastHopOwner,
|
|
432
|
+
sender: lastHopSender,
|
|
433
|
+
nonce: nonceMap.next(lastHopSender),
|
|
434
|
+
initCode: consumeInitCode(lastHopSender),
|
|
435
|
+
callData,
|
|
436
|
+
signOnly: true,
|
|
437
|
+
});
|
|
438
|
+
outOps.push(signedDisperse.userOp);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Batch Buy ops(分发后再执行)
|
|
244
443
|
for (let i = 0; i < buyerOwners.length; i++) {
|
|
245
444
|
const ai = buyerAis[i];
|
|
246
445
|
const buyWei = buyAmountsWei[i];
|
|
@@ -289,6 +488,12 @@ export class AADexSwapExecutor {
|
|
|
289
488
|
buyAmountsWei: buyAmountsWei.map(w => w.toString()),
|
|
290
489
|
hasApprove: needApprove,
|
|
291
490
|
routeAddress,
|
|
491
|
+
extractProfit,
|
|
492
|
+
profitBps,
|
|
493
|
+
profitRecipient,
|
|
494
|
+
profitWei: profitWei.toString(),
|
|
495
|
+
quotedSellOutWei: quotedSellOutWei.toString(),
|
|
496
|
+
disperseHopCount: String(hopCount),
|
|
292
497
|
},
|
|
293
498
|
};
|
|
294
499
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer 外盘(PotatoSwap/DEX)捆绑交易 SDK(AA / ERC-4337)
|
|
3
|
+
*
|
|
4
|
+
* 目标:对齐内盘 `bundle.ts` 的能力,但 Router/报价/授权逻辑走 DEX。
|
|
5
|
+
* - 捆绑买卖(买入 ->(可选授权)-> 卖出 ->(可选归集))
|
|
6
|
+
* - 自动归集 OKB 到 owner
|
|
7
|
+
* - ✅ 支持“刮取利润”:在归集时从可转出金额中按 bps 拆分转到 PROFIT_CONFIG.RECIPIENT
|
|
8
|
+
*/
|
|
9
|
+
import type { XLayerConfig, BundleBuySellResult, BundleBuySellParams } from './types.js';
|
|
10
|
+
export interface DexBundleConfig extends XLayerConfig {
|
|
11
|
+
dexKey?: string;
|
|
12
|
+
routerAddress?: string;
|
|
13
|
+
deadlineMinutes?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DexBundleBuySellParams extends BundleBuySellParams {
|
|
16
|
+
dexKey?: string;
|
|
17
|
+
routerAddress?: string;
|
|
18
|
+
deadlineMinutes?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 外盘捆绑交易执行器
|
|
22
|
+
*/
|
|
23
|
+
export declare class DexBundleExecutor {
|
|
24
|
+
private aaManager;
|
|
25
|
+
private portalQuery;
|
|
26
|
+
private dexQuery;
|
|
27
|
+
private config;
|
|
28
|
+
constructor(config?: DexBundleConfig);
|
|
29
|
+
private getEffectiveRouter;
|
|
30
|
+
private getDeadlineMinutes;
|
|
31
|
+
/**
|
|
32
|
+
* 执行 handleOps 并解析结果
|
|
33
|
+
*/
|
|
34
|
+
private runHandleOps;
|
|
35
|
+
private buildWithdrawUserOpWithState;
|
|
36
|
+
/**
|
|
37
|
+
* 外盘捆绑买卖(先买后卖)
|
|
38
|
+
*/
|
|
39
|
+
bundleBuySell(params: DexBundleBuySellParams): Promise<BundleBuySellResult>;
|
|
40
|
+
}
|
|
41
|
+
export declare function createDexBundleExecutor(config?: DexBundleConfig): DexBundleExecutor;
|
|
42
|
+
export declare function dexBundleBuySell(params: DexBundleBuySellParams): Promise<BundleBuySellResult>;
|