four-flap-meme-sdk 4.0.13 → 4.1.0
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/bundle/four-meme/core/sell-flow.js +3 -6
- package/dist/bundle/four-meme/private/sell.js +3 -3
- package/dist/chains/bsc/pancake/bundle-buy-first-build.d.ts +5 -3
- package/dist/chains/bsc/pancake/bundle-buy-first-build.js +25 -23
- package/dist/chains/bsc/pancake/bundle-buy-first-multi-wallet.js +6 -0
- package/dist/chains/bsc/pancake/bundle-buy-first-types.d.ts +4 -0
- package/dist/chains/bsc/pancake/bundle-buy-first.js +6 -0
- package/dist/chains/bsc/pancake/bundle-swap-types.d.ts +4 -0
- package/dist/chains/bsc/pancake/hop-chain.js +2 -0
- package/dist/chains/bsc/pancake/quote.d.ts +10 -1
- package/dist/chains/bsc/pancake/quote.js +30 -0
- package/dist/chains/bsc/pancake/quote.test.d.ts +1 -0
- package/dist/chains/bsc/pancake/quote.test.js +16 -0
- package/dist/chains/bsc/pancake/swap-batch.js +16 -7
- package/dist/chains/bsc/pancake/swap-build.d.ts +1 -1
- package/dist/chains/bsc/pancake/swap-build.js +9 -18
- package/dist/chains/bsc/pancake/swap-quick.js +31 -38
- package/dist/chains/bsc/pancake/swap-single.js +10 -1
- package/dist/chains/eni/platforms/daoaas/portal-direct-submit.js +2 -6
- package/dist/domains/flap/portal-bundle-merkle/core-helpers.d.ts +1 -1
- package/dist/domains/flap/portal-bundle-merkle/core-helpers.js +5 -3
- package/dist/domains/flap/portal-bundle-merkle/private/helpers.js +1 -1
- package/dist/domains/flap/portal-bundle-merkle/private/sell.js +16 -19
- package/dist/domains/flap/portal-bundle-merkle/swap-helpers.js +2 -0
- package/dist/domains/flap/portal-bundle-merkle/swap-quick-batch.js +12 -29
- package/package.json +1 -1
|
@@ -131,8 +131,7 @@ async function resolveSellOutputs(params) {
|
|
|
131
131
|
try {
|
|
132
132
|
const result = await trySell('BSC', rpcUrl, tokenAddress, amountsWei[0]);
|
|
133
133
|
const quotedOutputs = [result.funds];
|
|
134
|
-
|
|
135
|
-
const minOuts = [0n];
|
|
134
|
+
const minOuts = [(result.funds * 9900n) / 10000n];
|
|
136
135
|
return { minOuts, quotedOutputs };
|
|
137
136
|
}
|
|
138
137
|
catch {
|
|
@@ -165,8 +164,7 @@ async function resolveSellOutputs(params) {
|
|
|
165
164
|
}
|
|
166
165
|
return 0n;
|
|
167
166
|
});
|
|
168
|
-
|
|
169
|
-
const minOuts = quotedOutputs.map(() => 0n);
|
|
167
|
+
const minOuts = quotedOutputs.map((q) => (q * 9900n) / 10000n);
|
|
170
168
|
return { minOuts, quotedOutputs };
|
|
171
169
|
}
|
|
172
170
|
catch {
|
|
@@ -180,8 +178,7 @@ async function resolveSellOutputs(params) {
|
|
|
180
178
|
return 0n;
|
|
181
179
|
}
|
|
182
180
|
}));
|
|
183
|
-
|
|
184
|
-
const minOuts = quotedOutputs.map(() => 0n);
|
|
181
|
+
const minOuts = quotedOutputs.map((q) => (q * 9900n) / 10000n);
|
|
185
182
|
return { minOuts, quotedOutputs };
|
|
186
183
|
}
|
|
187
184
|
}
|
|
@@ -31,7 +31,7 @@ export async function fourPrivateSellMerkle(params) {
|
|
|
31
31
|
const [gasPrice, sellNonce, sellUnsigned] = await Promise.all([
|
|
32
32
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
33
33
|
wallet.getNonce(),
|
|
34
|
-
tm2.sellToken.populateTransaction(0n, tokenAddress, amountWei,
|
|
34
|
+
tm2.sellToken.populateTransaction(0n, tokenAddress, amountWei, minOut),
|
|
35
35
|
]);
|
|
36
36
|
// ✅ 已移除授权检查(前端负责确保授权)
|
|
37
37
|
// ✅ 利润计算(同步)
|
|
@@ -133,8 +133,8 @@ export async function fourBatchPrivateSellMerkle(params) {
|
|
|
133
133
|
}
|
|
134
134
|
})),
|
|
135
135
|
]);
|
|
136
|
-
//
|
|
137
|
-
const minOuts =
|
|
136
|
+
// 报价 × (1 - 100bps)
|
|
137
|
+
const minOuts = quotedOutputs.map((q) => (q * 9900n) / 10000n);
|
|
138
138
|
// ✅ 计算总利润和找出收益最高的钱包(同步计算)
|
|
139
139
|
const extractProfit = shouldExtractProfit();
|
|
140
140
|
let totalProfit = 0n;
|
|
@@ -6,20 +6,22 @@ import type { MultiSwapTxParams, MultiSwapUnsignedResult, RouteParams } from './
|
|
|
6
6
|
/**
|
|
7
7
|
* ✅ 构建多笔买入和卖出交易
|
|
8
8
|
*/
|
|
9
|
-
export declare function buildMultiRouteTransactions({ routeParams, buyAmountsWei, sellAmountsWei, buyer, seller, useNativeToken, v2RouterAddress, }: MultiSwapTxParams): Promise<MultiSwapUnsignedResult>;
|
|
10
|
-
export declare function buildSingleBuyTx({ routeParams, buyAmount, buyer, useNativeToken, deadline, v2RouterAddress, }: {
|
|
9
|
+
export declare function buildMultiRouteTransactions({ routeParams, buyAmountsWei, sellAmountsWei, buyer, seller, useNativeToken, v2RouterAddress, buyAmountOutMins, sellAmountOutMins, }: MultiSwapTxParams): Promise<MultiSwapUnsignedResult>;
|
|
10
|
+
export declare function buildSingleBuyTx({ routeParams, buyAmount, buyer, useNativeToken, deadline, v2RouterAddress, amountOutMin, }: {
|
|
11
11
|
routeParams: RouteParams;
|
|
12
12
|
buyAmount: bigint;
|
|
13
13
|
buyer: Wallet;
|
|
14
14
|
useNativeToken: boolean;
|
|
15
15
|
deadline: bigint;
|
|
16
16
|
v2RouterAddress?: string;
|
|
17
|
+
amountOutMin?: bigint;
|
|
17
18
|
}): Promise<ethers.TransactionLike>;
|
|
18
|
-
export declare function buildSingleSellTx({ routeParams, sellAmount, seller, useNativeToken, deadline, v2RouterAddress, }: {
|
|
19
|
+
export declare function buildSingleSellTx({ routeParams, sellAmount, seller, useNativeToken, deadline, v2RouterAddress, amountOutMin, }: {
|
|
19
20
|
routeParams: RouteParams;
|
|
20
21
|
sellAmount: bigint;
|
|
21
22
|
seller: Wallet;
|
|
22
23
|
useNativeToken: boolean;
|
|
23
24
|
deadline: bigint;
|
|
24
25
|
v2RouterAddress?: string;
|
|
26
|
+
amountOutMin?: bigint;
|
|
25
27
|
}): Promise<ethers.TransactionLike>;
|
|
@@ -7,8 +7,10 @@ import { FLAT_FEE, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_
|
|
|
7
7
|
/**
|
|
8
8
|
* ✅ 构建多笔买入和卖出交易
|
|
9
9
|
*/
|
|
10
|
-
export async function buildMultiRouteTransactions({ routeParams, buyAmountsWei, sellAmountsWei, buyer, seller, useNativeToken = true, v2RouterAddress, }) {
|
|
10
|
+
export async function buildMultiRouteTransactions({ routeParams, buyAmountsWei, sellAmountsWei, buyer, seller, useNativeToken = true, v2RouterAddress, buyAmountOutMins, sellAmountOutMins, }) {
|
|
11
11
|
const deadline = getDeadline();
|
|
12
|
+
const buyMinAt = (i) => buyAmountOutMins?.[i] ?? 0n;
|
|
13
|
+
const sellMinAt = (i) => sellAmountOutMins?.[i] ?? 0n;
|
|
12
14
|
if (routeParams.routeType === 'v2') {
|
|
13
15
|
const { v2Path } = routeParams;
|
|
14
16
|
const reversePath = [...v2Path].reverse();
|
|
@@ -18,17 +20,17 @@ export async function buildMultiRouteTransactions({ routeParams, buyAmountsWei,
|
|
|
18
20
|
// ✅ 根据是否使用原生代币选择不同的函数
|
|
19
21
|
if (useNativeToken) {
|
|
20
22
|
// BNB 池子:使用 swapExactETHForTokens / swapExactTokensForETH
|
|
21
|
-
const buyUnsignedArray = await Promise.all(buyAmountsWei.map((amount) => {
|
|
23
|
+
const buyUnsignedArray = await Promise.all(buyAmountsWei.map((amount, i) => {
|
|
22
24
|
const buyValue = amount + FLAT_FEE;
|
|
23
|
-
return v2RouterBuyer.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(
|
|
25
|
+
return v2RouterBuyer.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(buyMinAt(i), v2Path, buyer.address, deadline, { value: buyValue });
|
|
24
26
|
}));
|
|
25
|
-
const sellUnsignedArray = await Promise.all(sellAmountsWei.map((amount) => v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(amount,
|
|
27
|
+
const sellUnsignedArray = await Promise.all(sellAmountsWei.map((amount, i) => v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(amount, sellMinAt(i), reversePath, seller.address, deadline)));
|
|
26
28
|
return { buyUnsignedArray, sellUnsignedArray };
|
|
27
29
|
}
|
|
28
30
|
else {
|
|
29
31
|
// ✅ ERC20 池子(如 USDT):使用 swapExactTokensForTokens
|
|
30
|
-
const buyUnsignedArray = await Promise.all(buyAmountsWei.map((amount) => v2RouterBuyer.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(amount,
|
|
31
|
-
const sellUnsignedArray = await Promise.all(sellAmountsWei.map((amount) => v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(amount,
|
|
32
|
+
const buyUnsignedArray = await Promise.all(buyAmountsWei.map((amount, i) => v2RouterBuyer.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(amount, buyMinAt(i), v2Path, buyer.address, deadline)));
|
|
33
|
+
const sellUnsignedArray = await Promise.all(sellAmountsWei.map((amount, i) => v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(amount, sellMinAt(i), reversePath, seller.address, deadline)));
|
|
32
34
|
return { buyUnsignedArray, sellUnsignedArray };
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -38,7 +40,7 @@ export async function buildMultiRouteTransactions({ routeParams, buyAmountsWei,
|
|
|
38
40
|
const v3RouterBuyer = new Contract(PANCAKE_V3_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, buyer);
|
|
39
41
|
const v3RouterSeller = new Contract(PANCAKE_V3_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, seller);
|
|
40
42
|
// 构建多笔买入交易(ERC20 模式 value = 0,因为通过代币授权支付)
|
|
41
|
-
const buyUnsignedArray = await Promise.all(buyAmountsWei.map((amount) => {
|
|
43
|
+
const buyUnsignedArray = await Promise.all(buyAmountsWei.map((amount, i) => {
|
|
42
44
|
const buyValue = useNativeToken ? amount + FLAT_FEE : 0n;
|
|
43
45
|
const buySwapData = v3RouterIface.encodeFunctionData('exactInputSingle', [
|
|
44
46
|
{
|
|
@@ -47,7 +49,7 @@ export async function buildMultiRouteTransactions({ routeParams, buyAmountsWei,
|
|
|
47
49
|
fee: v3Fee,
|
|
48
50
|
recipient: buyer.address,
|
|
49
51
|
amountIn: amount,
|
|
50
|
-
amountOutMinimum:
|
|
52
|
+
amountOutMinimum: buyMinAt(i),
|
|
51
53
|
sqrtPriceLimitX96: 0n,
|
|
52
54
|
},
|
|
53
55
|
]);
|
|
@@ -56,7 +58,7 @@ export async function buildMultiRouteTransactions({ routeParams, buyAmountsWei,
|
|
|
56
58
|
// ✅ 根据是否使用原生代币构建不同的卖出交易
|
|
57
59
|
if (useNativeToken) {
|
|
58
60
|
// BNB 池子:卖出后 unwrap WBNB 为 BNB
|
|
59
|
-
const sellUnsignedArray = await Promise.all(sellAmountsWei.map((amount) => {
|
|
61
|
+
const sellUnsignedArray = await Promise.all(sellAmountsWei.map((amount, i) => {
|
|
60
62
|
const sellSwapData = v3RouterIface.encodeFunctionData('exactInputSingle', [
|
|
61
63
|
{
|
|
62
64
|
tokenIn: v3TokenOut,
|
|
@@ -64,18 +66,18 @@ export async function buildMultiRouteTransactions({ routeParams, buyAmountsWei,
|
|
|
64
66
|
fee: v3Fee,
|
|
65
67
|
recipient: PANCAKE_V3_ROUTER_ADDRESS,
|
|
66
68
|
amountIn: amount,
|
|
67
|
-
amountOutMinimum:
|
|
69
|
+
amountOutMinimum: sellMinAt(i),
|
|
68
70
|
sqrtPriceLimitX96: 0n,
|
|
69
71
|
},
|
|
70
72
|
]);
|
|
71
|
-
const sellUnwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [
|
|
73
|
+
const sellUnwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [sellMinAt(i), seller.address]);
|
|
72
74
|
return v3RouterSeller.multicall.populateTransaction(deadline, [sellSwapData, sellUnwrapData]);
|
|
73
75
|
}));
|
|
74
76
|
return { buyUnsignedArray, sellUnsignedArray };
|
|
75
77
|
}
|
|
76
78
|
else {
|
|
77
79
|
// ✅ ERC20 池子:卖出后直接获得 ERC20
|
|
78
|
-
const sellUnsignedArray = await Promise.all(sellAmountsWei.map((amount) => {
|
|
80
|
+
const sellUnsignedArray = await Promise.all(sellAmountsWei.map((amount, i) => {
|
|
79
81
|
const sellSwapData = v3RouterIface.encodeFunctionData('exactInputSingle', [
|
|
80
82
|
{
|
|
81
83
|
tokenIn: v3TokenOut,
|
|
@@ -83,7 +85,7 @@ export async function buildMultiRouteTransactions({ routeParams, buyAmountsWei,
|
|
|
83
85
|
fee: v3Fee,
|
|
84
86
|
recipient: seller.address, // 直接发送到卖方地址
|
|
85
87
|
amountIn: amount,
|
|
86
|
-
amountOutMinimum:
|
|
88
|
+
amountOutMinimum: sellMinAt(i),
|
|
87
89
|
sqrtPriceLimitX96: 0n,
|
|
88
90
|
},
|
|
89
91
|
]);
|
|
@@ -95,15 +97,15 @@ export async function buildMultiRouteTransactions({ routeParams, buyAmountsWei,
|
|
|
95
97
|
throw new Error('V3 多跳路由暂不支持官方合约,请使用 V2 路由或 V3 单跳');
|
|
96
98
|
}
|
|
97
99
|
// ✅ 构建单笔买入交易
|
|
98
|
-
export async function buildSingleBuyTx({ routeParams, buyAmount, buyer, useNativeToken, deadline, v2RouterAddress, }) {
|
|
100
|
+
export async function buildSingleBuyTx({ routeParams, buyAmount, buyer, useNativeToken, deadline, v2RouterAddress, amountOutMin = 0n, }) {
|
|
99
101
|
if (routeParams.routeType === 'v2') {
|
|
100
102
|
const { v2Path } = routeParams;
|
|
101
103
|
const v2Router = new Contract(v2RouterAddress || PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, buyer);
|
|
102
104
|
if (useNativeToken) {
|
|
103
|
-
return await v2Router.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(
|
|
105
|
+
return await v2Router.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(amountOutMin, v2Path, buyer.address, deadline, { value: buyAmount });
|
|
104
106
|
}
|
|
105
107
|
else {
|
|
106
|
-
return await v2Router.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(buyAmount,
|
|
108
|
+
return await v2Router.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(buyAmount, amountOutMin, v2Path, buyer.address, deadline);
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
if (routeParams.routeType === 'v3-single') {
|
|
@@ -118,7 +120,7 @@ export async function buildSingleBuyTx({ routeParams, buyAmount, buyer, useNativ
|
|
|
118
120
|
fee: v3Fee,
|
|
119
121
|
recipient: buyer.address,
|
|
120
122
|
amountIn: buyAmount,
|
|
121
|
-
amountOutMinimum:
|
|
123
|
+
amountOutMinimum: amountOutMin,
|
|
122
124
|
sqrtPriceLimitX96: 0n,
|
|
123
125
|
},
|
|
124
126
|
]);
|
|
@@ -127,16 +129,16 @@ export async function buildSingleBuyTx({ routeParams, buyAmount, buyer, useNativ
|
|
|
127
129
|
throw new Error('V3 多跳路由暂不支持');
|
|
128
130
|
}
|
|
129
131
|
// ✅ 构建单笔卖出交易
|
|
130
|
-
export async function buildSingleSellTx({ routeParams, sellAmount, seller, useNativeToken, deadline, v2RouterAddress, }) {
|
|
132
|
+
export async function buildSingleSellTx({ routeParams, sellAmount, seller, useNativeToken, deadline, v2RouterAddress, amountOutMin = 0n, }) {
|
|
131
133
|
if (routeParams.routeType === 'v2') {
|
|
132
134
|
const { v2Path } = routeParams;
|
|
133
135
|
const reversePath = [...v2Path].reverse();
|
|
134
136
|
const v2Router = new Contract(v2RouterAddress || PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, seller);
|
|
135
137
|
if (useNativeToken) {
|
|
136
|
-
return await v2Router.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmount,
|
|
138
|
+
return await v2Router.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmount, amountOutMin, reversePath, seller.address, deadline);
|
|
137
139
|
}
|
|
138
140
|
else {
|
|
139
|
-
return await v2Router.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmount,
|
|
141
|
+
return await v2Router.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmount, amountOutMin, reversePath, seller.address, deadline);
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
if (routeParams.routeType === 'v3-single') {
|
|
@@ -151,11 +153,11 @@ export async function buildSingleSellTx({ routeParams, sellAmount, seller, useNa
|
|
|
151
153
|
fee: v3Fee,
|
|
152
154
|
recipient: PANCAKE_V3_ROUTER_ADDRESS,
|
|
153
155
|
amountIn: sellAmount,
|
|
154
|
-
amountOutMinimum:
|
|
156
|
+
amountOutMinimum: amountOutMin,
|
|
155
157
|
sqrtPriceLimitX96: 0n,
|
|
156
158
|
},
|
|
157
159
|
]);
|
|
158
|
-
const sellUnwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [
|
|
160
|
+
const sellUnwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [amountOutMin, seller.address]);
|
|
159
161
|
return await v3Router.multicall.populateTransaction(deadline, [sellSwapData, sellUnwrapData]);
|
|
160
162
|
}
|
|
161
163
|
else {
|
|
@@ -166,7 +168,7 @@ export async function buildSingleSellTx({ routeParams, sellAmount, seller, useNa
|
|
|
166
168
|
fee: v3Fee,
|
|
167
169
|
recipient: seller.address,
|
|
168
170
|
amountIn: sellAmount,
|
|
169
|
-
amountOutMinimum:
|
|
171
|
+
amountOutMinimum: amountOutMin,
|
|
170
172
|
sqrtPriceLimitX96: 0n,
|
|
171
173
|
},
|
|
172
174
|
]);
|
|
@@ -11,6 +11,7 @@ import { createPancakeContext, getGasPrice, quoteTokenOutput } from './bundle-bu
|
|
|
11
11
|
import { validateSwapCounts } from './bundle-buy-first-plan.js';
|
|
12
12
|
import { buildSingleBuyTx, buildSingleSellTx } from './bundle-buy-first-build.js';
|
|
13
13
|
import { buildProfitTransaction } from './bundle-buy-first-profit.js';
|
|
14
|
+
import { splitQuotedMins } from './quote.js';
|
|
14
15
|
/**
|
|
15
16
|
* ✅ 多钱包捆绑换手
|
|
16
17
|
* - 多个买方钱包执行买入(每个钱包1笔)
|
|
@@ -88,6 +89,9 @@ export async function pancakeBundleBuyFirstMultiWallet(params) {
|
|
|
88
89
|
}
|
|
89
90
|
// ✅ 将代币平均分配给卖方
|
|
90
91
|
const sellAmountsWei = splitAmount(actualTokenOut, sellCount);
|
|
92
|
+
const slippageBps = config.slippageBps ?? 100;
|
|
93
|
+
const buyAmountOutMins = splitQuotedMins(quoteResult.quotedTokenOut, buyAmountsWei, slippageBps);
|
|
94
|
+
const sellAmountOutMins = splitQuotedMins(totalFundsWei, sellAmountsWei, slippageBps);
|
|
91
95
|
// 买合计万三 + 卖合计万三。ERC-20 先把买入合计折成 BNB 再计提,避免 ANTFUN 微额报价为 0
|
|
92
96
|
let profitAmount = 0n;
|
|
93
97
|
if (useNativeToken) {
|
|
@@ -140,6 +144,7 @@ export async function pancakeBundleBuyFirstMultiWallet(params) {
|
|
|
140
144
|
useNativeToken,
|
|
141
145
|
deadline,
|
|
142
146
|
v2RouterAddress: config.v2RouterAddress,
|
|
147
|
+
amountOutMin: buyAmountOutMins[i],
|
|
143
148
|
});
|
|
144
149
|
return buyer.signTransaction({
|
|
145
150
|
...unsigned,
|
|
@@ -160,6 +165,7 @@ export async function pancakeBundleBuyFirstMultiWallet(params) {
|
|
|
160
165
|
useNativeToken,
|
|
161
166
|
deadline,
|
|
162
167
|
v2RouterAddress: config.v2RouterAddress,
|
|
168
|
+
amountOutMin: sellAmountOutMins[i],
|
|
163
169
|
});
|
|
164
170
|
return seller.signTransaction({
|
|
165
171
|
...unsigned,
|
|
@@ -16,6 +16,8 @@ export interface PancakeBuyFirstSignConfig {
|
|
|
16
16
|
skipQuoteOnError?: boolean;
|
|
17
17
|
userType?: UserType;
|
|
18
18
|
bribeAmount?: number;
|
|
19
|
+
/** 滑点基点,默认 100 = 1%。 */
|
|
20
|
+
slippageBps?: number;
|
|
19
21
|
v2RouterAddress?: string;
|
|
20
22
|
}
|
|
21
23
|
export type UserType = 'v0' | 'v1';
|
|
@@ -131,6 +133,8 @@ export type MultiSwapTxParams = {
|
|
|
131
133
|
seller: Wallet;
|
|
132
134
|
useNativeToken?: boolean;
|
|
133
135
|
v2RouterAddress?: string;
|
|
136
|
+
buyAmountOutMins?: bigint[];
|
|
137
|
+
sellAmountOutMins?: bigint[];
|
|
134
138
|
};
|
|
135
139
|
export type MultiSwapUnsignedResult = {
|
|
136
140
|
buyUnsignedArray: ethers.TransactionLike[];
|
|
@@ -6,6 +6,7 @@ import { getGasLimit } from '../../../core/gas/bundle-gas.js';
|
|
|
6
6
|
import { NonceManager } from '../../../utils/bundle-helpers.js';
|
|
7
7
|
import { BLOCKRAZOR_BUILDER_EOA, ZERO_ADDRESS, GAS_LIMITS, calculateBundleSwapRoundTripProfit } from '../../../core/constants/index.js';
|
|
8
8
|
import { quoteRoundTripProfitToNative } from '../../../core/quote/quote-helpers.js';
|
|
9
|
+
import { splitQuotedMins } from './quote.js';
|
|
9
10
|
import { buildMultiRouteTransactions, buildProfitTransaction, calculateBuyerFunds, createPancakeContext, getGasPrice, pancakeBundleBuyFirstMultiWallet, planMultiNonces, quoteTokenOutput, splitAmount, validateFinalBalances, validateSwapCounts, } from './bundle-buy-first-helpers.js';
|
|
10
11
|
export async function pancakeBundleBuyFirstMerkle(params) {
|
|
11
12
|
const { buyerPrivateKey, buyerPrivateKeys, sellerPrivateKey, sellerPrivateKeys, routeParams, buyerFunds, buyerFundsPercentage, config, quoteToken, quoteTokenDecimals = 18, buyCount: _buyCount, sellCount: _sellCount, taxRateBps = 0, // ✅ 税率(基点),用于计算实际可卖出代币数量
|
|
@@ -66,6 +67,9 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
66
67
|
actualTokenOut = (quoteResult.quotedTokenOut * BigInt(10000 - taxRateBps)) / 10000n;
|
|
67
68
|
}
|
|
68
69
|
const sellAmountsWei = splitAmount(actualTokenOut, sellCount);
|
|
70
|
+
const slippageBps = config.slippageBps ?? 100;
|
|
71
|
+
const buyAmountOutMins = splitQuotedMins(quoteResult.quotedTokenOut, buyAmountsWei, slippageBps);
|
|
72
|
+
const sellAmountOutMins = splitQuotedMins(buyerFundsInfo.buyerFundsWei, sellAmountsWei, slippageBps);
|
|
69
73
|
// ✅ 构建多笔买入和卖出交易
|
|
70
74
|
const swapUnsignedArray = await buildMultiRouteTransactions({
|
|
71
75
|
routeParams,
|
|
@@ -75,6 +79,8 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
75
79
|
seller,
|
|
76
80
|
useNativeToken,
|
|
77
81
|
v2RouterAddress: config.v2RouterAddress,
|
|
82
|
+
buyAmountOutMins,
|
|
83
|
+
sellAmountOutMins,
|
|
78
84
|
});
|
|
79
85
|
const finalGasLimit = getGasLimit(config, 800000);
|
|
80
86
|
const gasPrice = await getGasPrice(context.provider, config);
|
|
@@ -53,6 +53,8 @@ export type BuildSwapParams = {
|
|
|
53
53
|
tokenAddress: string;
|
|
54
54
|
useNativeToken?: boolean;
|
|
55
55
|
v2RouterAddress?: string;
|
|
56
|
+
sellAmountOutMin?: bigint;
|
|
57
|
+
buyAmountOutMin?: bigint;
|
|
56
58
|
};
|
|
57
59
|
export type BuyerBudgetParams = {
|
|
58
60
|
buyer: Wallet;
|
|
@@ -95,6 +97,8 @@ export interface PancakeSwapSignConfig {
|
|
|
95
97
|
reserveGasBNB?: number;
|
|
96
98
|
userType?: UserType;
|
|
97
99
|
bribeAmount?: number;
|
|
100
|
+
/** 滑点基点,默认 100 = 1%。买卖 amountOutMin 都按报价下浮。 */
|
|
101
|
+
slippageBps?: number;
|
|
98
102
|
v2RouterAddress?: string;
|
|
99
103
|
}
|
|
100
104
|
export interface PancakeSwapConfig extends CommonBundleConfig {
|
|
@@ -206,6 +206,8 @@ export async function buildBNBHopChainForERC20(payer, path, finalGasAmount, gasP
|
|
|
206
206
|
const fromWallet = path.hopWallets[i];
|
|
207
207
|
const toAddress = i === hopCount - 1 ? path.targetAddress : path.hopWallets[i + 1].address;
|
|
208
208
|
const amount = i === hopCount - 1 ? finalGasAmount : amountsPerHop[i + 1];
|
|
209
|
+
if (i === hopCount - 1 && amount === 0n)
|
|
210
|
+
continue;
|
|
209
211
|
const tx = {
|
|
210
212
|
to: toAddress,
|
|
211
213
|
value: amount,
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { ethers } from 'ethers';
|
|
2
|
-
import { type QuoteBNBResult, type QuoteSellParams, type UserType } from './bundle-swap-types.js';
|
|
2
|
+
import { type QuoteBNBResult, type QuoteSellParams, type RouteParams, type UserType } from './bundle-swap-types.js';
|
|
3
|
+
/** 100 bps = 1%。上限 50%,避免写成 0 输出。 */
|
|
4
|
+
export declare function applyPancakeSlippage(quotedOut: bigint, slippageBps?: number): bigint;
|
|
5
|
+
export declare function splitQuotedMins(quotedTotal: bigint, amounts: bigint[], slippageBps?: number): bigint[];
|
|
6
|
+
export declare function quoteBuyOutput(params: {
|
|
7
|
+
routeParams: RouteParams;
|
|
8
|
+
buyAmountWei: bigint;
|
|
9
|
+
provider: ethers.JsonRpcProvider;
|
|
10
|
+
v2RouterAddress?: string;
|
|
11
|
+
}): Promise<bigint>;
|
|
3
12
|
export declare function quoteSellOutput({ routeParams, sellAmountWei, provider, v2RouterAddress, }: QuoteSellParams): Promise<QuoteBNBResult>;
|
|
4
13
|
/**
|
|
5
14
|
* 根据 userType 计算利润率
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { ADDRESSES, getProfitRateBps as getSharedProfitRateBps, calculateBundleSwapRoundTripProfit } from '../../../core/constants/index.js';
|
|
2
2
|
import { quoteV2, quoteV3 } from '../../../core/quote/quote-helpers.js';
|
|
3
3
|
import { WBNB_ADDRESS, } from './bundle-swap-types.js';
|
|
4
|
+
/** 100 bps = 1%。上限 50%,避免写成 0 输出。 */
|
|
5
|
+
export function applyPancakeSlippage(quotedOut, slippageBps = 100) {
|
|
6
|
+
const bps = Math.max(0, Math.min(5000, Math.floor(Number.isFinite(slippageBps) ? slippageBps : 100)));
|
|
7
|
+
return (quotedOut * BigInt(10000 - bps)) / 10000n;
|
|
8
|
+
}
|
|
9
|
+
export function splitQuotedMins(quotedTotal, amounts, slippageBps = 100) {
|
|
10
|
+
const total = amounts.reduce((sum, amt) => sum + amt, 0n);
|
|
11
|
+
if (total <= 0n)
|
|
12
|
+
return amounts.map(() => 0n);
|
|
13
|
+
return amounts.map((amt) => applyPancakeSlippage((quotedTotal * amt) / total, slippageBps));
|
|
14
|
+
}
|
|
15
|
+
export async function quoteBuyOutput(params) {
|
|
16
|
+
const { routeParams, buyAmountWei, provider, v2RouterAddress } = params;
|
|
17
|
+
if (buyAmountWei <= 0n)
|
|
18
|
+
return 0n;
|
|
19
|
+
if (routeParams.routeType === 'v2') {
|
|
20
|
+
const reversePath = [...routeParams.v2Path].reverse();
|
|
21
|
+
const tokenIn = reversePath[0];
|
|
22
|
+
const tokenOut = reversePath[reversePath.length - 1];
|
|
23
|
+
if (!tokenIn || !tokenOut)
|
|
24
|
+
return 0n;
|
|
25
|
+
const result = await quoteV2(provider, tokenIn, tokenOut, buyAmountWei, 'BSC', v2RouterAddress);
|
|
26
|
+
return result.amountOut;
|
|
27
|
+
}
|
|
28
|
+
if (routeParams.routeType === 'v3-single') {
|
|
29
|
+
const result = await quoteV3(provider, routeParams.v3TokenOut, routeParams.v3TokenIn, buyAmountWei, 'BSC', routeParams.v3Fee);
|
|
30
|
+
return result.amountOut;
|
|
31
|
+
}
|
|
32
|
+
return 0n;
|
|
33
|
+
}
|
|
4
34
|
export async function quoteSellOutput({ routeParams, sellAmountWei, provider, v2RouterAddress, }) {
|
|
5
35
|
if (routeParams.routeType === 'v2') {
|
|
6
36
|
const { v2Path } = routeParams;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { applyPancakeSlippage, splitQuotedMins } from './quote.js';
|
|
3
|
+
describe('applyPancakeSlippage', () => {
|
|
4
|
+
it('drops 1% by default', () => {
|
|
5
|
+
expect(applyPancakeSlippage(1000000n)).toBe(990000n);
|
|
6
|
+
});
|
|
7
|
+
it('clamps out-of-range bps', () => {
|
|
8
|
+
expect(applyPancakeSlippage(1000000n, -10)).toBe(1000000n);
|
|
9
|
+
expect(applyPancakeSlippage(1000000n, 9000)).toBe(500000n);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
describe('splitQuotedMins', () => {
|
|
13
|
+
it('splits quoted output then applies slippage', () => {
|
|
14
|
+
expect(splitQuotedMins(1000000n, [250000n, 750000n], 100)).toEqual([247500n, 742500n]);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -6,7 +6,7 @@ import { getGasLimit } from '../../../core/gas/bundle-gas.js';
|
|
|
6
6
|
import { ethers, Contract, Wallet } from 'ethers';
|
|
7
7
|
import { calculateSellAmount } from '../../../utils/swap-helpers.js';
|
|
8
8
|
import { NonceManager, getDeadline, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
|
|
9
|
-
import { BRIBE_GAS_LIMIT, ERC20_BALANCE_OF_ABI, FLAT_FEE, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, ZERO_ADDRESS, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, } from './bundle-swap-helpers.js';
|
|
9
|
+
import { BRIBE_GAS_LIMIT, ERC20_BALANCE_OF_ABI, FLAT_FEE, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, ZERO_ADDRESS, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, quoteBuyOutput, applyPancakeSlippage, } from './bundle-swap-helpers.js';
|
|
10
10
|
/**
|
|
11
11
|
* PancakeSwap 批量换手(一卖多买)
|
|
12
12
|
*
|
|
@@ -88,6 +88,15 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}));
|
|
91
|
+
const slippageBps = config.slippageBps ?? 100;
|
|
92
|
+
const sellMinOut = applyPancakeSlippage(estimatedBNBOut, slippageBps);
|
|
93
|
+
const buyQuotedOuts = await Promise.all(buyAmountsWei.map((amt) => quoteBuyOutput({
|
|
94
|
+
routeParams,
|
|
95
|
+
buyAmountWei: amt,
|
|
96
|
+
provider: context.provider,
|
|
97
|
+
v2RouterAddress: config.v2RouterAddress,
|
|
98
|
+
})));
|
|
99
|
+
const buyMinOuts = buyQuotedOuts.map((quoted) => applyPancakeSlippage(quoted, slippageBps));
|
|
91
100
|
const deadline = getDeadline();
|
|
92
101
|
const v3RouterIface = new ethers.Interface(PANCAKE_V3_ROUTER_ABI);
|
|
93
102
|
const batchV2Addr = config.v2RouterAddress || PANCAKE_V2_ROUTER_ADDRESS;
|
|
@@ -97,11 +106,11 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
97
106
|
const v2RouterSeller = new Contract(batchV2Addr, PANCAKE_V2_ROUTER_ABI, seller);
|
|
98
107
|
if (useNativeToken) {
|
|
99
108
|
// ✅ 原生代币模式(BNB):Token → WBNB → BNB
|
|
100
|
-
sellUnsigned = await v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei,
|
|
109
|
+
sellUnsigned = await v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, sellMinOut, v2Path, seller.address, deadline);
|
|
101
110
|
}
|
|
102
111
|
else {
|
|
103
112
|
// ✅ ERC20 模式(USDT):Token → USDT
|
|
104
|
-
sellUnsigned = await v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei,
|
|
113
|
+
sellUnsigned = await v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, sellMinOut, v2Path, seller.address, deadline);
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
else if (routeParams.routeType === 'v3-single') {
|
|
@@ -114,7 +123,7 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
114
123
|
fee: v3Fee,
|
|
115
124
|
recipient: useNativeToken ? PANCAKE_V3_ROUTER_ADDRESS : seller.address,
|
|
116
125
|
amountIn: sellAmountWei,
|
|
117
|
-
amountOutMinimum:
|
|
126
|
+
amountOutMinimum: sellMinOut,
|
|
118
127
|
sqrtPriceLimitX96: 0n,
|
|
119
128
|
},
|
|
120
129
|
]);
|
|
@@ -140,11 +149,11 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
140
149
|
const v2RouterBuyer = new Contract(batchV2Addr, PANCAKE_V2_ROUTER_ABI, buyer);
|
|
141
150
|
if (useNativeToken) {
|
|
142
151
|
// ✅ 原生代币模式(BNB):BNB → WBNB → Token
|
|
143
|
-
return await v2RouterBuyer.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(0n, reversePath, buyer.address, deadline, { value: buyValue });
|
|
152
|
+
return await v2RouterBuyer.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(buyMinOuts[i] ?? 0n, reversePath, buyer.address, deadline, { value: buyValue });
|
|
144
153
|
}
|
|
145
154
|
else {
|
|
146
155
|
// ✅ ERC20 模式(USDT):USDT → Token
|
|
147
|
-
return await v2RouterBuyer.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(buyAmount, 0n, reversePath, buyer.address, deadline);
|
|
156
|
+
return await v2RouterBuyer.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(buyAmount, buyMinOuts[i] ?? 0n, reversePath, buyer.address, deadline);
|
|
148
157
|
}
|
|
149
158
|
}
|
|
150
159
|
else if (routeParams.routeType === 'v3-single') {
|
|
@@ -157,7 +166,7 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
157
166
|
fee: v3Fee,
|
|
158
167
|
recipient: buyer.address,
|
|
159
168
|
amountIn: buyAmount,
|
|
160
|
-
amountOutMinimum: 0n,
|
|
169
|
+
amountOutMinimum: buyMinOuts[i] ?? 0n,
|
|
161
170
|
sqrtPriceLimitX96: 0n,
|
|
162
171
|
},
|
|
163
172
|
]);
|
|
@@ -2,7 +2,7 @@ import { ethers } from 'ethers';
|
|
|
2
2
|
import { type BalanceValidationParams, type BuildSwapParams, type BuyerBudget, type BuyerBudgetParams, type NoncePlan, type PancakeContext, type PancakeSwapConfig, type PancakeSwapSignConfig, type PlanNonceParams, type SwapUnsignedResult } from './bundle-swap-types.js';
|
|
3
3
|
export declare function createPancakeContext(config: PancakeSwapSignConfig): PancakeContext;
|
|
4
4
|
export declare function getGasPrice(provider: ethers.JsonRpcProvider, config: PancakeSwapSignConfig | PancakeSwapConfig): Promise<bigint>;
|
|
5
|
-
export declare function buildSwapTransactions({ routeParams, sellAmountWei, buyAmountBNB, buyer, seller, tokenAddress, useNativeToken, v2RouterAddress, }: BuildSwapParams): Promise<SwapUnsignedResult>;
|
|
5
|
+
export declare function buildSwapTransactions({ routeParams, sellAmountWei, buyAmountBNB, buyer, seller, tokenAddress, useNativeToken, v2RouterAddress, sellAmountOutMin, buyAmountOutMin, }: BuildSwapParams): Promise<SwapUnsignedResult>;
|
|
6
6
|
export declare function calculateBuyerBudget({ buyer, quotedBNBOut, reserveGasBNB, useNativeToken, quoteToken, quoteTokenDecimals, provider, }: BuyerBudgetParams): Promise<BuyerBudget>;
|
|
7
7
|
/**
|
|
8
8
|
* ✅ 从前端传入的 startNonces 构建 NoncePlan(用于性能优化)
|
|
@@ -23,7 +23,7 @@ export async function getGasPrice(provider, config) {
|
|
|
23
23
|
}
|
|
24
24
|
return gasPrice;
|
|
25
25
|
}
|
|
26
|
-
export async function buildSwapTransactions({ routeParams, sellAmountWei, buyAmountBNB, buyer, seller, tokenAddress, useNativeToken = true, v2RouterAddress, }) {
|
|
26
|
+
export async function buildSwapTransactions({ routeParams, sellAmountWei, buyAmountBNB, buyer, seller, tokenAddress, useNativeToken = true, v2RouterAddress, sellAmountOutMin = 0n, buyAmountOutMin = 0n, }) {
|
|
27
27
|
void tokenAddress;
|
|
28
28
|
const deadline = getDeadline();
|
|
29
29
|
const buyValue = useNativeToken ? buyAmountBNB + FLAT_FEE : 0n;
|
|
@@ -36,19 +36,12 @@ export async function buildSwapTransactions({ routeParams, sellAmountWei, buyAmo
|
|
|
36
36
|
let sellUnsigned;
|
|
37
37
|
let buyUnsigned;
|
|
38
38
|
if (useNativeToken) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
sellUnsigned = await v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, 0n, v2Path, seller.address, deadline);
|
|
42
|
-
// 买入:BNB → WBNB → Token
|
|
43
|
-
buyUnsigned = await v2RouterBuyer.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(0n, reversePath, buyer.address, deadline, { value: buyValue });
|
|
39
|
+
sellUnsigned = await v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, sellAmountOutMin, v2Path, seller.address, deadline);
|
|
40
|
+
buyUnsigned = await v2RouterBuyer.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(buyAmountOutMin, reversePath, buyer.address, deadline, { value: buyValue });
|
|
44
41
|
}
|
|
45
42
|
else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
sellUnsigned = await v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, 0n, v2Path, seller.address, deadline);
|
|
49
|
-
// 买入:USDT → Token
|
|
50
|
-
buyUnsigned = await v2RouterBuyer.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(buyAmountBNB, // USDT 数量
|
|
51
|
-
0n, reversePath, buyer.address, deadline);
|
|
43
|
+
sellUnsigned = await v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, sellAmountOutMin, v2Path, seller.address, deadline);
|
|
44
|
+
buyUnsigned = await v2RouterBuyer.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(buyAmountBNB, buyAmountOutMin, reversePath, buyer.address, deadline);
|
|
52
45
|
}
|
|
53
46
|
return { sellUnsigned, buyUnsigned };
|
|
54
47
|
}
|
|
@@ -57,21 +50,19 @@ export async function buildSwapTransactions({ routeParams, sellAmountWei, buyAmo
|
|
|
57
50
|
const v3RouterIface = new ethers.Interface(PANCAKE_V3_ROUTER_ABI);
|
|
58
51
|
const v3RouterSeller = new Contract(PANCAKE_V3_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, seller);
|
|
59
52
|
const v3RouterBuyer = new Contract(PANCAKE_V3_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, buyer);
|
|
60
|
-
// 卖出:Token → WBNB,需要 unwrapWETH9
|
|
61
53
|
const sellSwapData = v3RouterIface.encodeFunctionData('exactInputSingle', [
|
|
62
54
|
{
|
|
63
55
|
tokenIn: v3TokenIn,
|
|
64
56
|
tokenOut: v3TokenOut,
|
|
65
57
|
fee: v3Fee,
|
|
66
|
-
recipient: PANCAKE_V3_ROUTER_ADDRESS,
|
|
58
|
+
recipient: PANCAKE_V3_ROUTER_ADDRESS,
|
|
67
59
|
amountIn: sellAmountWei,
|
|
68
|
-
amountOutMinimum:
|
|
60
|
+
amountOutMinimum: sellAmountOutMin,
|
|
69
61
|
sqrtPriceLimitX96: 0n,
|
|
70
62
|
},
|
|
71
63
|
]);
|
|
72
|
-
const sellUnwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [
|
|
64
|
+
const sellUnwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [sellAmountOutMin, seller.address]);
|
|
73
65
|
const sellUnsigned = await v3RouterSeller.multicall.populateTransaction(deadline, [sellSwapData, sellUnwrapData]);
|
|
74
|
-
// 买入:WBNB → Token
|
|
75
66
|
const buySwapData = v3RouterIface.encodeFunctionData('exactInputSingle', [
|
|
76
67
|
{
|
|
77
68
|
tokenIn: v3TokenOut,
|
|
@@ -79,7 +70,7 @@ export async function buildSwapTransactions({ routeParams, sellAmountWei, buyAmo
|
|
|
79
70
|
fee: v3Fee,
|
|
80
71
|
recipient: buyer.address,
|
|
81
72
|
amountIn: buyAmountBNB,
|
|
82
|
-
amountOutMinimum:
|
|
73
|
+
amountOutMinimum: buyAmountOutMin,
|
|
83
74
|
sqrtPriceLimitX96: 0n,
|
|
84
75
|
},
|
|
85
76
|
]);
|
|
@@ -7,7 +7,7 @@ import { ethers, Contract, Wallet } from 'ethers';
|
|
|
7
7
|
import { calculateSellAmount } from '../../../utils/swap-helpers.js';
|
|
8
8
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
|
|
9
9
|
import { ERC20_ABI } from '../../../abis/common.js';
|
|
10
|
-
import { BRIBE_GAS_LIMIT, FLAT_FEE, NATIVE_TRANSFER_GAS_LIMIT, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, ZERO_ADDRESS, buildBNBHopChainForERC20, buildERC20HopChain, buildNativeHopChain, calculateProfitAmount, createPancakeContext, generateDisperseHopPaths, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, } from './bundle-swap-helpers.js';
|
|
10
|
+
import { BRIBE_GAS_LIMIT, FLAT_FEE, NATIVE_TRANSFER_GAS_LIMIT, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, ZERO_ADDRESS, buildBNBHopChainForERC20, buildERC20HopChain, buildNativeHopChain, calculateProfitAmount, createPancakeContext, generateDisperseHopPaths, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, quoteBuyOutput, applyPancakeSlippage, } from './bundle-swap-helpers.js';
|
|
11
11
|
/**
|
|
12
12
|
* PancakeSwap 快捷批量换手(资金自动流转)
|
|
13
13
|
*
|
|
@@ -176,18 +176,22 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
176
176
|
sellerRequired = bribeAmount + sellerGasCost;
|
|
177
177
|
}
|
|
178
178
|
else {
|
|
179
|
-
// ERC20:卖方还要给每个买方转一笔买入 Gas(hop=0 直转 / hop>0 多跳)
|
|
180
179
|
sellerGasCost =
|
|
181
|
-
gasPrice *
|
|
182
|
-
|
|
183
|
-
finalGasLimit +
|
|
184
|
-
(NATIVE_TRANSFER_GAS_LIMIT + ERC20_TRANSFER_GAS) * BigInt(buyers.length) +
|
|
185
|
-
BRIBE_GAS_LIMIT);
|
|
186
|
-
sellerRequired = bribeAmount + sellerGasCost + gasPrice * finalGasLimit * BigInt(buyers.length);
|
|
180
|
+
gasPrice * (BRIBE_GAS_LIMIT + finalGasLimit + ERC20_TRANSFER_GAS * BigInt(buyers.length) + BRIBE_GAS_LIMIT);
|
|
181
|
+
sellerRequired = bribeAmount + sellerGasCost;
|
|
187
182
|
}
|
|
188
183
|
if (sellerBalance < sellerRequired) {
|
|
189
184
|
throw new Error(`主钱包 BNB 余额不足: 需要约 ${ethers.formatEther(sellerRequired)} BNB (贿赂: ${ethers.formatEther(bribeAmount)}, Gas: ${ethers.formatEther(sellerGasCost)}), 实际 ${ethers.formatEther(sellerBalance)} BNB`);
|
|
190
185
|
}
|
|
186
|
+
const slippageBps = config.slippageBps ?? 100;
|
|
187
|
+
const sellMinOut = applyPancakeSlippage(estimatedOutput, slippageBps);
|
|
188
|
+
const buyQuotedOuts = await Promise.all(transferAmountsWei.map((amt) => quoteBuyOutput({
|
|
189
|
+
routeParams,
|
|
190
|
+
buyAmountWei: amt,
|
|
191
|
+
provider: context.provider,
|
|
192
|
+
v2RouterAddress: config.v2RouterAddress,
|
|
193
|
+
})));
|
|
194
|
+
const buyMinOuts = buyQuotedOuts.map((quoted) => applyPancakeSlippage(quoted, slippageBps));
|
|
191
195
|
// ==================== 规划 Nonce ====================
|
|
192
196
|
// ✅ 如果前端传入了 startNonces,直接使用(性能优化)
|
|
193
197
|
let sellerNonce = startNonces && startNonces.length > 0 ? startNonces[0] : await nonceManager.getNextNonce(seller);
|
|
@@ -214,11 +218,11 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
214
218
|
const v2RouterSeller = new Contract(quickV2Addr, PANCAKE_V2_ROUTER_ABI, seller);
|
|
215
219
|
if (useNativeToken) {
|
|
216
220
|
// ✅ 原生代币模式(BNB):Token → WBNB → BNB
|
|
217
|
-
sellUnsigned = await v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei,
|
|
221
|
+
sellUnsigned = await v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, sellMinOut, v2Path, seller.address, deadline);
|
|
218
222
|
}
|
|
219
223
|
else {
|
|
220
224
|
// ✅ ERC20 模式(USDT):Token → USDT
|
|
221
|
-
sellUnsigned = await v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei,
|
|
225
|
+
sellUnsigned = await v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, sellMinOut, v2Path, seller.address, deadline);
|
|
222
226
|
}
|
|
223
227
|
}
|
|
224
228
|
else if (routeParams.routeType === 'v3-single') {
|
|
@@ -231,7 +235,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
231
235
|
fee: v3Fee,
|
|
232
236
|
recipient: useNativeToken ? PANCAKE_V3_ROUTER_ADDRESS : seller.address,
|
|
233
237
|
amountIn: sellAmountWei,
|
|
234
|
-
amountOutMinimum:
|
|
238
|
+
amountOutMinimum: sellMinOut,
|
|
235
239
|
sqrtPriceLimitX96: 0n,
|
|
236
240
|
},
|
|
237
241
|
]);
|
|
@@ -258,7 +262,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
258
262
|
type: txType,
|
|
259
263
|
});
|
|
260
264
|
// ==================== 3. 转账交易(支持多跳)====================
|
|
261
|
-
|
|
265
|
+
// 买地址 gas 由用户自备,卖方只转买入金额 / 计价币
|
|
262
266
|
// ✅ 生成多跳路径
|
|
263
267
|
const hopPaths = generateDisperseHopPaths(buyers.map((b) => b.address), disperseHopCount, context.provider);
|
|
264
268
|
// 收集所有中间钱包信息
|
|
@@ -272,7 +276,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
272
276
|
const transferNonces = buyers.map((_, i) => sellerNonce + i);
|
|
273
277
|
sellerNonce += buyers.length;
|
|
274
278
|
transferTxs = await Promise.all(buyers.map((buyer, i) => {
|
|
275
|
-
const transferValue = transferAmountsWei[i] + FLAT_FEE
|
|
279
|
+
const transferValue = transferAmountsWei[i] + FLAT_FEE;
|
|
276
280
|
return seller.signTransaction({
|
|
277
281
|
to: buyer.address,
|
|
278
282
|
value: transferValue,
|
|
@@ -286,30 +290,21 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
286
290
|
}
|
|
287
291
|
else {
|
|
288
292
|
const erc20Interface = new ethers.Interface(ERC20_ABI);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
transferTxs.push(await seller.signTransaction({
|
|
293
|
-
to: buyer.address,
|
|
294
|
-
value: buyerGasCost,
|
|
295
|
-
nonce: sellerNonce++,
|
|
296
|
-
gasPrice,
|
|
297
|
-
gasLimit: NATIVE_TRANSFER_GAS_LIMIT,
|
|
298
|
-
chainId: context.chainId,
|
|
299
|
-
type: txType,
|
|
300
|
-
}));
|
|
293
|
+
const transferNonces = buyers.map((_, i) => sellerNonce + i);
|
|
294
|
+
sellerNonce += buyers.length;
|
|
295
|
+
transferTxs = await Promise.all(buyers.map((buyer, i) => {
|
|
301
296
|
const transferData = erc20Interface.encodeFunctionData('transfer', [buyer.address, transferAmountsWei[i]]);
|
|
302
|
-
|
|
297
|
+
return seller.signTransaction({
|
|
303
298
|
to: quoteToken,
|
|
304
299
|
data: transferData,
|
|
305
300
|
value: 0n,
|
|
306
|
-
nonce:
|
|
301
|
+
nonce: transferNonces[i],
|
|
307
302
|
gasPrice,
|
|
308
303
|
gasLimit: ERC20_TRANSFER_GAS,
|
|
309
304
|
chainId: context.chainId,
|
|
310
305
|
type: txType,
|
|
311
|
-
})
|
|
312
|
-
}
|
|
306
|
+
});
|
|
307
|
+
}));
|
|
313
308
|
}
|
|
314
309
|
}
|
|
315
310
|
else {
|
|
@@ -318,7 +313,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
318
313
|
// BNB 多跳转账
|
|
319
314
|
// ✅ 修复:payer 每次只发送 1 笔交易(到第一个hop钱包),hop钱包用自己的nonce=0
|
|
320
315
|
const hopChains = await Promise.all(hopPaths.map((path, i) => {
|
|
321
|
-
const finalAmount = transferAmountsWei[i] + FLAT_FEE
|
|
316
|
+
const finalAmount = transferAmountsWei[i] + FLAT_FEE;
|
|
322
317
|
const payerNonce = sellerNonce + i; // ✅ payer 每个 buyer 只用 1 个 nonce
|
|
323
318
|
return buildNativeHopChain(seller, path, finalAmount, gasPrice, context.chainId, txType, payerNonce);
|
|
324
319
|
}));
|
|
@@ -328,12 +323,10 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
328
323
|
else {
|
|
329
324
|
// ERC20 多跳转账:先转 BNB(给中间钱包 gas),再转 ERC20
|
|
330
325
|
// ✅ 修复:payer 每次只发送 1 笔交易(到第一个hop钱包),hop钱包用自己的nonce=0/1
|
|
331
|
-
// 1. 构建 BNB
|
|
326
|
+
// 1. 构建 BNB 多跳链(只给中间钱包 gas,买地址自备)
|
|
332
327
|
const bnbHopChains = await Promise.all(hopPaths.map((path, i) => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const payerNonce = sellerNonce + i; // ✅ payer 每个 buyer 只用 1 个 nonce
|
|
336
|
-
return buildBNBHopChainForERC20(seller, path, finalGasAmount, gasPrice, context.chainId, txType, payerNonce);
|
|
328
|
+
const payerNonce = sellerNonce + i;
|
|
329
|
+
return buildBNBHopChainForERC20(seller, path, 0n, gasPrice, context.chainId, txType, payerNonce);
|
|
337
330
|
}));
|
|
338
331
|
const bnbTxs = bnbHopChains.flat();
|
|
339
332
|
sellerNonce += buyers.length; // ✅ payer 只增加 buyers.length 个 nonce
|
|
@@ -362,10 +355,10 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
362
355
|
const reversePath = [...v2Path].reverse();
|
|
363
356
|
const v2RouterBuyer = new Contract(quickV2Addr, PANCAKE_V2_ROUTER_ABI, buyer);
|
|
364
357
|
if (useNativeToken) {
|
|
365
|
-
buyUnsigned = await v2RouterBuyer.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(0n, reversePath, buyer.address, deadline, { value: buyValue });
|
|
358
|
+
buyUnsigned = await v2RouterBuyer.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(buyMinOuts[i] ?? 0n, reversePath, buyer.address, deadline, { value: buyValue });
|
|
366
359
|
}
|
|
367
360
|
else {
|
|
368
|
-
buyUnsigned = await v2RouterBuyer.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(buyAmount, 0n, reversePath, buyer.address, deadline);
|
|
361
|
+
buyUnsigned = await v2RouterBuyer.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(buyAmount, buyMinOuts[i] ?? 0n, reversePath, buyer.address, deadline);
|
|
369
362
|
}
|
|
370
363
|
}
|
|
371
364
|
else if (routeParams.routeType === 'v3-single') {
|
|
@@ -378,7 +371,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
378
371
|
fee: v3Fee,
|
|
379
372
|
recipient: buyer.address,
|
|
380
373
|
amountIn: buyAmount,
|
|
381
|
-
amountOutMinimum: 0n,
|
|
374
|
+
amountOutMinimum: buyMinOuts[i] ?? 0n,
|
|
382
375
|
sqrtPriceLimitX96: 0n,
|
|
383
376
|
},
|
|
384
377
|
]);
|
|
@@ -6,7 +6,7 @@ import { getGasLimit } from '../../../core/gas/bundle-gas.js';
|
|
|
6
6
|
import { ethers, Wallet } from 'ethers';
|
|
7
7
|
import { calculateSellAmount } from '../../../utils/swap-helpers.js';
|
|
8
8
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
|
|
9
|
-
import { BRIBE_GAS_LIMIT, ZERO_ADDRESS, buildNoncePlanFromNonces, buildSwapTransactions, WBNB_ADDRESS, calculateBuyerBudget, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, planNonces, quoteSellOutput, validateFinalBalances, } from './bundle-swap-helpers.js';
|
|
9
|
+
import { BRIBE_GAS_LIMIT, ZERO_ADDRESS, buildNoncePlanFromNonces, buildSwapTransactions, WBNB_ADDRESS, calculateBuyerBudget, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, planNonces, quoteSellOutput, applyPancakeSlippage, quoteBuyOutput, validateFinalBalances, } from './bundle-swap-helpers.js';
|
|
10
10
|
export async function pancakeBundleSwapMerkle(params) {
|
|
11
11
|
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKey, tokenAddress, routeParams, config, quoteToken, quoteTokenDecimals = 18, startNonces, // ✅ 可选:前端预获取的 nonces
|
|
12
12
|
} = params;
|
|
@@ -41,6 +41,13 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
41
41
|
quoteTokenDecimals,
|
|
42
42
|
provider: context.provider,
|
|
43
43
|
});
|
|
44
|
+
const quotedBuyOut = await quoteBuyOutput({
|
|
45
|
+
routeParams,
|
|
46
|
+
buyAmountWei: buyerBudget.buyAmountBNB,
|
|
47
|
+
provider: context.provider,
|
|
48
|
+
v2RouterAddress: config.v2RouterAddress,
|
|
49
|
+
});
|
|
50
|
+
const slippageBps = config.slippageBps ?? 100;
|
|
44
51
|
const swapUnsigned = await buildSwapTransactions({
|
|
45
52
|
routeParams,
|
|
46
53
|
sellAmountWei,
|
|
@@ -50,6 +57,8 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
50
57
|
tokenAddress,
|
|
51
58
|
useNativeToken,
|
|
52
59
|
v2RouterAddress: config.v2RouterAddress,
|
|
60
|
+
sellAmountOutMin: applyPancakeSlippage(quoteResult.estimatedBNBOut, slippageBps),
|
|
61
|
+
buyAmountOutMin: applyPancakeSlippage(quotedBuyOut, slippageBps),
|
|
53
62
|
});
|
|
54
63
|
const version = routeParams.routeType === 'v2' ? 'v2' : 'v3';
|
|
55
64
|
const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
|
|
@@ -146,12 +146,10 @@ export async function directBatchBuyForSubmit(params) {
|
|
|
146
146
|
}
|
|
147
147
|
const transactions = [];
|
|
148
148
|
const totalFlow = amounts.reduce((sum, amount) => sum + amount, 0n);
|
|
149
|
-
const isBatch = privateKeys.length > 1;
|
|
150
149
|
for (let i = 0; i < privateKeys.length; i++) {
|
|
151
150
|
const wallet = new Wallet(privateKeys[i], provider);
|
|
152
151
|
const preview = await query.previewBuy(token, amounts[i]);
|
|
153
|
-
const
|
|
154
|
-
const minOut = applySlippage(preview, effectiveSlippage);
|
|
152
|
+
const minOut = applySlippage(preview, slippageBps);
|
|
155
153
|
const calldata = encodeBuyCall(token, minOut);
|
|
156
154
|
const nonce = await wallet.getNonce('pending');
|
|
157
155
|
const signedTx = await wallet.signTransaction({
|
|
@@ -201,13 +199,11 @@ export async function directBatchSellForSubmit(params) {
|
|
|
201
199
|
const wallets = privateKeys.map((pk) => new Wallet(pk, provider));
|
|
202
200
|
const transactions = [];
|
|
203
201
|
const estimatedOuts = [];
|
|
204
|
-
const isBatch = privateKeys.length > 1;
|
|
205
202
|
for (let i = 0; i < privateKeys.length; i++) {
|
|
206
203
|
const wallet = wallets[i];
|
|
207
204
|
const preview = await query.previewSell(token, tokenAmounts[i]);
|
|
208
205
|
estimatedOuts.push(preview);
|
|
209
|
-
const
|
|
210
|
-
const minOut = applySlippage(preview, effectiveSlippage);
|
|
206
|
+
const minOut = applySlippage(preview, slippageBps);
|
|
211
207
|
const calldata = encodeSellCall(token, tokenAmounts[i], minOut);
|
|
212
208
|
const nonce = await wallet.getNonce('pending');
|
|
213
209
|
const signedTx = await wallet.signTransaction({
|
|
@@ -96,5 +96,5 @@ export declare function buildProfitMetadata(extractProfit: boolean, totalBuyAmou
|
|
|
96
96
|
* 比 Promise.all 更高效,N 个报价只需 1 次网络请求
|
|
97
97
|
*/
|
|
98
98
|
export declare function quoteSellOutputsWithQuote(portal: ethers.Contract, tokenAddress: string, amountsWei: bigint[], outputToken: string): Promise<bigint[]>;
|
|
99
|
-
export declare function resolveMinOutputs(
|
|
99
|
+
export declare function resolveMinOutputs(provided: (string | bigint)[] | undefined, walletCount: number, quotedOutputs: bigint[]): bigint[];
|
|
100
100
|
export {};
|
|
@@ -284,7 +284,9 @@ export async function quoteSellOutputsWithQuote(portal, tokenAddress, amountsWei
|
|
|
284
284
|
}));
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
-
export function resolveMinOutputs(
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
export function resolveMinOutputs(provided, walletCount, quotedOutputs) {
|
|
288
|
+
if (provided && provided.length === walletCount) {
|
|
289
|
+
return provided.map((m) => (typeof m === 'string' ? ethers.parseEther(m) : m));
|
|
290
|
+
}
|
|
291
|
+
return quotedOutputs.map((q) => (q * 9900n) / 10000n);
|
|
290
292
|
}
|
|
@@ -16,19 +16,18 @@ export async function flapPrivateSellMerkle(params) {
|
|
|
16
16
|
const amountWei = ethers.parseUnits(amount, 18);
|
|
17
17
|
const portal = new ethers.Contract(portalAddr, PORTAL_ABI, wallet);
|
|
18
18
|
// ✅ 并行获取 gasPrice、nonce、报价和构建未签名交易
|
|
19
|
-
const [gasPrice, currentNonce, { quotedOutput }
|
|
19
|
+
const [gasPrice, currentNonce, { quotedOutput, minOut }] = await Promise.all([
|
|
20
20
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
21
|
-
// ✅ 使用 pending nonce,避免 xLayer 等链上“已有 pending 交易”导致 nonce too low
|
|
22
21
|
provider.getTransactionCount(wallet.address, 'pending'),
|
|
23
22
|
resolveSingleSellOutputs(portal, tokenAddress, amountWei, minOutputAmount),
|
|
24
|
-
portal.swapExactInput.populateTransaction({
|
|
25
|
-
inputToken: tokenAddress,
|
|
26
|
-
outputToken: ZERO_ADDRESS,
|
|
27
|
-
inputAmount: amountWei,
|
|
28
|
-
minOutputAmount: 0n, // ✅ 已移除滑点保护
|
|
29
|
-
permitData: '0x',
|
|
30
|
-
}),
|
|
31
23
|
]);
|
|
24
|
+
const unsigned = await portal.swapExactInput.populateTransaction({
|
|
25
|
+
inputToken: tokenAddress,
|
|
26
|
+
outputToken: ZERO_ADDRESS,
|
|
27
|
+
inputAmount: amountWei,
|
|
28
|
+
minOutputAmount: minOut,
|
|
29
|
+
permitData: '0x',
|
|
30
|
+
});
|
|
32
31
|
const gasLimit = resolveGasLimit(config.gasLimitMultiplier);
|
|
33
32
|
const txType = getTxType(config);
|
|
34
33
|
// ✅ 获取贿赂金额
|
|
@@ -103,9 +102,8 @@ export async function flapBatchPrivateSellMerkle(params) {
|
|
|
103
102
|
const portal = new ethers.Contract(portalAddr, PORTAL_ABI, provider);
|
|
104
103
|
const portals = wallets.map((w) => new ethers.Contract(portalAddr, PORTAL_ABI, w));
|
|
105
104
|
// ✅ 并行获取 gasPrice、所有 nonces、所有报价和构建未签名交易
|
|
106
|
-
const [gasPrice, initialNonces, quotedOutputs
|
|
105
|
+
const [gasPrice, initialNonces, quotedOutputs] = await Promise.all([
|
|
107
106
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
108
|
-
// ✅ 使用 pending nonce,避免 xLayer 等链上“已有 pending 交易”导致 nonce too low
|
|
109
107
|
Promise.all(wallets.map((w) => provider.getTransactionCount(w.address, 'pending'))),
|
|
110
108
|
Promise.all(amountsWei.map((amount) => portal.quoteExactInput
|
|
111
109
|
.staticCall({
|
|
@@ -114,15 +112,14 @@ export async function flapBatchPrivateSellMerkle(params) {
|
|
|
114
112
|
inputAmount: amount,
|
|
115
113
|
})
|
|
116
114
|
.catch(() => 0n))),
|
|
117
|
-
// ✅ 并行构建未签名交易(已移除滑点保护:minOuts 固定为 0)
|
|
118
|
-
Promise.all(portals.map((p, i) => p.swapExactInput.populateTransaction({
|
|
119
|
-
inputToken: tokenAddress,
|
|
120
|
-
outputToken: ZERO_ADDRESS,
|
|
121
|
-
inputAmount: amountsWei[i],
|
|
122
|
-
minOutputAmount: 0n,
|
|
123
|
-
permitData: '0x',
|
|
124
|
-
}))),
|
|
125
115
|
]);
|
|
116
|
+
const unsignedList = await Promise.all(portals.map((p, i) => p.swapExactInput.populateTransaction({
|
|
117
|
+
inputToken: tokenAddress,
|
|
118
|
+
outputToken: ZERO_ADDRESS,
|
|
119
|
+
inputAmount: amountsWei[i],
|
|
120
|
+
minOutputAmount: (quotedOutputs[i] * 9900n) / 10000n,
|
|
121
|
+
permitData: '0x',
|
|
122
|
+
})));
|
|
126
123
|
const extractProfit = shouldExtractProfit();
|
|
127
124
|
const { totalProfit, maxRevenueIndex } = summarizeSellProfits(quotedOutputs, extractProfit);
|
|
128
125
|
// ✅ 获取贿赂金额
|
|
@@ -165,6 +165,8 @@ export async function buildBNBHopChainForERC20(payer, path, finalGasAmount, gasP
|
|
|
165
165
|
const fromWallet = path.hopWallets[i];
|
|
166
166
|
const toAddress = i === hopCount - 1 ? path.targetAddress : path.hopWallets[i + 1].address;
|
|
167
167
|
const amount = i === hopCount - 1 ? finalGasAmount : amountsPerHop[i + 1];
|
|
168
|
+
if (i === hopCount - 1 && amount === 0n)
|
|
169
|
+
continue;
|
|
168
170
|
signedTxs.push(await fromWallet.signTransaction({
|
|
169
171
|
to: toAddress,
|
|
170
172
|
value: amount,
|
|
@@ -139,15 +139,9 @@ export async function flapQuickBatchSwapMerkle(params) {
|
|
|
139
139
|
sellerRequired = bribeAmount + sellerGasCost;
|
|
140
140
|
}
|
|
141
141
|
else {
|
|
142
|
-
const reserveForBuyers = ethers.parseEther((config.reserveGasETH || 0.0005).toString());
|
|
143
142
|
sellerGasCost =
|
|
144
|
-
gasPrice *
|
|
145
|
-
|
|
146
|
-
finalGasLimit +
|
|
147
|
-
(NATIVE_TRANSFER_GAS_LIMIT + ERC20_TRANSFER_GAS) * BigInt(buyers.length) +
|
|
148
|
-
BRIBE_GAS_LIMIT);
|
|
149
|
-
sellerRequired =
|
|
150
|
-
bribeAmount + sellerGasCost + (reserveForBuyers + gasPrice * finalGasLimit) * BigInt(buyers.length);
|
|
143
|
+
gasPrice * (BRIBE_GAS_LIMIT + finalGasLimit + ERC20_TRANSFER_GAS * BigInt(buyers.length) + BRIBE_GAS_LIMIT);
|
|
144
|
+
sellerRequired = bribeAmount + sellerGasCost;
|
|
151
145
|
}
|
|
152
146
|
if (sellerBalance < sellerRequired) {
|
|
153
147
|
throw new Error(`主钱包 ${chainContext.nativeToken} 余额不足: 需要约 ${ethers.formatEther(sellerRequired)} (贿赂: ${ethers.formatEther(bribeAmount)}, Gas: ${ethers.formatEther(sellerGasCost)}), 实际 ${ethers.formatEther(sellerBalance)}`);
|
|
@@ -189,8 +183,6 @@ export async function flapQuickBatchSwapMerkle(params) {
|
|
|
189
183
|
});
|
|
190
184
|
const signedSell = await seller.signTransaction(sellTx);
|
|
191
185
|
// ==================== 3. 转账交易(支持多跳)====================
|
|
192
|
-
const reserveGas = ethers.parseEther((config.reserveGasETH || 0.0005).toString());
|
|
193
|
-
const buyerGasCost = gasPrice * finalGasLimit;
|
|
194
186
|
// ✅ 生成多跳路径
|
|
195
187
|
const hopPaths = generateDisperseHopPaths(buyers.map((b) => b.address), disperseHopCount, chainContext.provider);
|
|
196
188
|
// 收集所有中间钱包信息
|
|
@@ -204,7 +196,7 @@ export async function flapQuickBatchSwapMerkle(params) {
|
|
|
204
196
|
const transferNonces = buyers.map((_, i) => sellerNonce + i);
|
|
205
197
|
sellerNonce += buyers.length;
|
|
206
198
|
transferTxs = await Promise.all(buyers.map((buyer, i) => {
|
|
207
|
-
const transferValue = transferAmountsWei[i]
|
|
199
|
+
const transferValue = transferAmountsWei[i];
|
|
208
200
|
return seller.signTransaction({
|
|
209
201
|
to: buyer.address,
|
|
210
202
|
value: transferValue,
|
|
@@ -218,30 +210,21 @@ export async function flapQuickBatchSwapMerkle(params) {
|
|
|
218
210
|
}
|
|
219
211
|
else {
|
|
220
212
|
const erc20Interface = new ethers.Interface(ERC20_ABI);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
transferTxs.push(await seller.signTransaction({
|
|
225
|
-
to: buyer.address,
|
|
226
|
-
value: reserveGas + buyerGasCost,
|
|
227
|
-
nonce: sellerNonce++,
|
|
228
|
-
gasPrice,
|
|
229
|
-
gasLimit: NATIVE_TRANSFER_GAS_LIMIT,
|
|
230
|
-
chainId: chainContext.chainId,
|
|
231
|
-
type: txType,
|
|
232
|
-
}));
|
|
213
|
+
const transferNonces = buyers.map((_, i) => sellerNonce + i);
|
|
214
|
+
sellerNonce += buyers.length;
|
|
215
|
+
transferTxs = await Promise.all(buyers.map((buyer, i) => {
|
|
233
216
|
const transferData = erc20Interface.encodeFunctionData('transfer', [buyer.address, transferAmountsWei[i]]);
|
|
234
|
-
|
|
217
|
+
return seller.signTransaction({
|
|
235
218
|
to: quoteToken,
|
|
236
219
|
data: transferData,
|
|
237
220
|
value: 0n,
|
|
238
|
-
nonce:
|
|
221
|
+
nonce: transferNonces[i],
|
|
239
222
|
gasPrice,
|
|
240
223
|
gasLimit: ERC20_TRANSFER_GAS,
|
|
241
224
|
chainId: chainContext.chainId,
|
|
242
225
|
type: txType,
|
|
243
|
-
})
|
|
244
|
-
}
|
|
226
|
+
});
|
|
227
|
+
}));
|
|
245
228
|
}
|
|
246
229
|
}
|
|
247
230
|
else {
|
|
@@ -250,7 +233,7 @@ export async function flapQuickBatchSwapMerkle(params) {
|
|
|
250
233
|
// 原生代币多跳转账
|
|
251
234
|
// ✅ 修复:payer 每次只发送 1 笔交易(到第一个hop钱包),hop钱包用自己的nonce=0
|
|
252
235
|
const hopChains = await Promise.all(hopPaths.map((path, i) => {
|
|
253
|
-
const finalAmount = transferAmountsWei[i]
|
|
236
|
+
const finalAmount = transferAmountsWei[i];
|
|
254
237
|
const payerNonce = sellerNonce + i; // ✅ payer 每个 buyer 只用 1 个 nonce
|
|
255
238
|
return buildNativeHopChain(seller, path, finalAmount, gasPrice, chainContext.chainId, txType, payerNonce);
|
|
256
239
|
}));
|
|
@@ -262,7 +245,7 @@ export async function flapQuickBatchSwapMerkle(params) {
|
|
|
262
245
|
// ✅ 修复:payer 每次只发送 1 笔交易(到第一个hop钱包),hop钱包用自己的nonce=0/1
|
|
263
246
|
// 1. 构建 BNB 多跳链
|
|
264
247
|
const bnbHopChains = await Promise.all(hopPaths.map((path, i) => {
|
|
265
|
-
const finalGasAmount =
|
|
248
|
+
const finalGasAmount = 0n;
|
|
266
249
|
const payerNonce = sellerNonce + i; // ✅ payer 每个 buyer 只用 1 个 nonce
|
|
267
250
|
return buildBNBHopChainForERC20(seller, path, finalGasAmount, gasPrice, chainContext.chainId, txType, payerNonce);
|
|
268
251
|
}));
|