four-flap-meme-sdk 4.0.14 → 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/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 +16 -7
- 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/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 {
|
|
@@ -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
|
*
|
|
@@ -183,6 +183,15 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
183
183
|
if (sellerBalance < sellerRequired) {
|
|
184
184
|
throw new Error(`主钱包 BNB 余额不足: 需要约 ${ethers.formatEther(sellerRequired)} BNB (贿赂: ${ethers.formatEther(bribeAmount)}, Gas: ${ethers.formatEther(sellerGasCost)}), 实际 ${ethers.formatEther(sellerBalance)} BNB`);
|
|
185
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));
|
|
186
195
|
// ==================== 规划 Nonce ====================
|
|
187
196
|
// ✅ 如果前端传入了 startNonces,直接使用(性能优化)
|
|
188
197
|
let sellerNonce = startNonces && startNonces.length > 0 ? startNonces[0] : await nonceManager.getNextNonce(seller);
|
|
@@ -209,11 +218,11 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
209
218
|
const v2RouterSeller = new Contract(quickV2Addr, PANCAKE_V2_ROUTER_ABI, seller);
|
|
210
219
|
if (useNativeToken) {
|
|
211
220
|
// ✅ 原生代币模式(BNB):Token → WBNB → BNB
|
|
212
|
-
sellUnsigned = await v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei,
|
|
221
|
+
sellUnsigned = await v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, sellMinOut, v2Path, seller.address, deadline);
|
|
213
222
|
}
|
|
214
223
|
else {
|
|
215
224
|
// ✅ ERC20 模式(USDT):Token → USDT
|
|
216
|
-
sellUnsigned = await v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei,
|
|
225
|
+
sellUnsigned = await v2RouterSeller.swapExactTokensForTokensSupportingFeeOnTransferTokens.populateTransaction(sellAmountWei, sellMinOut, v2Path, seller.address, deadline);
|
|
217
226
|
}
|
|
218
227
|
}
|
|
219
228
|
else if (routeParams.routeType === 'v3-single') {
|
|
@@ -226,7 +235,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
226
235
|
fee: v3Fee,
|
|
227
236
|
recipient: useNativeToken ? PANCAKE_V3_ROUTER_ADDRESS : seller.address,
|
|
228
237
|
amountIn: sellAmountWei,
|
|
229
|
-
amountOutMinimum:
|
|
238
|
+
amountOutMinimum: sellMinOut,
|
|
230
239
|
sqrtPriceLimitX96: 0n,
|
|
231
240
|
},
|
|
232
241
|
]);
|
|
@@ -346,10 +355,10 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
346
355
|
const reversePath = [...v2Path].reverse();
|
|
347
356
|
const v2RouterBuyer = new Contract(quickV2Addr, PANCAKE_V2_ROUTER_ABI, buyer);
|
|
348
357
|
if (useNativeToken) {
|
|
349
|
-
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 });
|
|
350
359
|
}
|
|
351
360
|
else {
|
|
352
|
-
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);
|
|
353
362
|
}
|
|
354
363
|
}
|
|
355
364
|
else if (routeParams.routeType === 'v3-single') {
|
|
@@ -362,7 +371,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
362
371
|
fee: v3Fee,
|
|
363
372
|
recipient: buyer.address,
|
|
364
373
|
amountIn: buyAmount,
|
|
365
|
-
amountOutMinimum: 0n,
|
|
374
|
+
amountOutMinimum: buyMinOuts[i] ?? 0n,
|
|
366
375
|
sqrtPriceLimitX96: 0n,
|
|
367
376
|
},
|
|
368
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
|
// ✅ 获取贿赂金额
|