four-flap-meme-sdk 4.4.3 → 4.5.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/README.md +16 -22
- package/dist/bundle/four-meme/types/buy-first.d.ts +2 -1
- package/dist/bundle/four-meme/types/config.d.ts +2 -1
- package/dist/bundle/four-meme/types/swap-types.d.ts +2 -1
- package/dist/bundle/four-meme/utils-sweep/erc20-no-hop.js +16 -8
- package/dist/bundle/four-meme/utils-sweep/main.js +15 -1
- package/dist/bundle/four-meme/utils-sweep/native-no-hop.js +17 -9
- package/dist/bundle/index.d.ts +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/internal.d.ts +2 -0
- package/dist/bundle/internal.js +7 -3
- package/dist/bundle/submit/index.d.ts +1 -3
- package/dist/bundle/submit/index.js +1 -3
- package/dist/chains/bsc/index.d.ts +1 -1
- package/dist/chains/bsc/index.js +1 -1
- package/dist/chains/bsc/pancake/bundle-buy-first-types.d.ts +2 -1
- package/dist/chains/bsc/pancake/bundle-swap-types.d.ts +2 -1
- package/dist/chains/bsc/platforms/four/index.d.ts +1 -1
- package/dist/chains/bsc/platforms/four/index.js +1 -1
- package/dist/core/clients/blockrazor.js +8 -6
- package/dist/core/clients/blockrazor.test.d.ts +1 -0
- package/dist/core/clients/blockrazor.test.js +23 -0
- package/dist/core/clients/index.d.ts +0 -1
- package/dist/core/clients/index.js +0 -1
- package/dist/core/clients/merkle.js +12 -7
- package/dist/core/clients/merkle.test.d.ts +1 -0
- package/dist/core/clients/merkle.test.js +8 -0
- package/dist/core/nonce/nonce-manager.d.ts +2 -0
- package/dist/core/nonce/nonce-manager.js +21 -1
- package/dist/core/tx/wallet-sign-patch.js +22 -11
- package/dist/core/tx/wallet-sign-patch.test.d.ts +1 -0
- package/dist/core/tx/wallet-sign-patch.test.js +21 -0
- package/dist/domains/dex/direct-router-v2-buy.js +13 -0
- package/dist/domains/flap/portal-bundle-merkle/private/buy.js +3 -3
- package/dist/domains/flap/portal-bundle-merkle/private/helpers.d.ts +1 -1
- package/dist/domains/flap/portal-bundle-merkle/private/helpers.js +4 -8
- package/dist/domains/flap/portal-bundle-merkle/private/sell.js +3 -3
- package/dist/domains/flap/portal-bundle-merkle/swap-buy-first-types.d.ts +2 -1
- package/dist/domains/flap/portal-bundle-merkle/swap-helpers.d.ts +2 -1
- package/dist/domains/flap/portal-bundle-merkle/swap-planning.js +5 -2
- package/dist/domains/flap/portal-bundle-merkle/swap-planning.test.d.ts +1 -0
- package/dist/domains/flap/portal-bundle-merkle/swap-planning.test.js +16 -0
- package/dist/domains/flap/portal-bundle-merkle/types-config.d.ts +2 -1
- package/dist/domains/merkle/index.d.ts +1 -1
- package/dist/domains/merkle/index.js +1 -1
- package/dist/exports/root-bundle-and-tooling.d.ts +1 -1
- package/dist/exports/root-bundle-and-tooling.js +1 -1
- package/dist/exports/root-foundations.d.ts +0 -1
- package/dist/exports/root-foundations.js +0 -1
- package/dist/utils/swap-helpers.d.ts +1 -15
- package/dist/utils/swap-helpers.js +0 -80
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**four.meme 和 Flap Protocol 统一 SDK - 生产环境 MEV 保护方案**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
提供完整的代币发行和交易功能,签名后通过 BlockRazor / 48.club / Emit 提交,确保 MEV 保护和原子性。
|
|
6
6
|
|
|
7
7
|
## 核心优势
|
|
8
8
|
|
|
@@ -20,9 +20,9 @@ npm i four-flap-meme-sdk ethers
|
|
|
20
20
|
|
|
21
21
|
**要求**:Node.js >= 18, ESM, ethers v6
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## 提交
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
SDK 只负责构建与签名;Merkle 网络客户端和 `submitBundleToMerkle` 已移除,请使用 BlockRazor、48.club 或 Emit 提交。
|
|
26
26
|
|
|
27
27
|
## 浏览器环境 CORS 支持
|
|
28
28
|
|
|
@@ -66,7 +66,6 @@ const result = await createTokenWithBundleBuyMerkle({
|
|
|
66
66
|
label: 'Meme'
|
|
67
67
|
},
|
|
68
68
|
config: {
|
|
69
|
-
apiKey: 'YOUR_MERKLE_API_KEY',
|
|
70
69
|
rpcUrl: 'https://bsc-dataseed.binance.org'
|
|
71
70
|
}
|
|
72
71
|
});
|
|
@@ -84,7 +83,7 @@ await batchBuyWithBundleMerkle({
|
|
|
84
83
|
privateKeys: ['0x...买家1', '0x...买家2', '0x...买家3'],
|
|
85
84
|
buyAmounts: ['0.3', '0.5', '0.2'],
|
|
86
85
|
tokenAddress: '0x...代币地址',
|
|
87
|
-
config: {
|
|
86
|
+
config: {}
|
|
88
87
|
});
|
|
89
88
|
```
|
|
90
89
|
|
|
@@ -98,7 +97,7 @@ await batchSellWithBundleMerkle({
|
|
|
98
97
|
privateKeys: ['0x...卖家1', '0x...卖家2'],
|
|
99
98
|
sellAmounts: ['1000', '2000'],
|
|
100
99
|
tokenAddress: '0x...代币地址',
|
|
101
|
-
config: {
|
|
100
|
+
config: {}
|
|
102
101
|
});
|
|
103
102
|
```
|
|
104
103
|
|
|
@@ -110,7 +109,7 @@ import { approveFourBatchMerkle } from 'four-flap-meme-sdk';
|
|
|
110
109
|
await approveFourBatchMerkle({
|
|
111
110
|
privateKeys: ['0x...地址1', '0x...地址2'],
|
|
112
111
|
tokenAddress: '0x...代币地址',
|
|
113
|
-
config: {
|
|
112
|
+
config: {}
|
|
114
113
|
});
|
|
115
114
|
```
|
|
116
115
|
|
|
@@ -131,7 +130,7 @@ await fourPancakeProxyBatchBuyMerkle({
|
|
|
131
130
|
['0x...WBNB', '0x...TOKEN']
|
|
132
131
|
],
|
|
133
132
|
routeType: 'v2',
|
|
134
|
-
config: {
|
|
133
|
+
config: {}
|
|
135
134
|
});
|
|
136
135
|
|
|
137
136
|
// V2 批量卖出
|
|
@@ -143,7 +142,7 @@ await fourPancakeProxyBatchSellMerkle({
|
|
|
143
142
|
['0x...TOKEN', '0x...WBNB']
|
|
144
143
|
],
|
|
145
144
|
routeType: 'v2',
|
|
146
|
-
config: {
|
|
145
|
+
config: {}
|
|
147
146
|
});
|
|
148
147
|
```
|
|
149
148
|
|
|
@@ -158,7 +157,7 @@ await fourPancakeProxyBatchBuyMerkle({
|
|
|
158
157
|
tokenOut: '0x...TOKEN',
|
|
159
158
|
fee: 2500, // 0.25%
|
|
160
159
|
routeType: 'v3-single',
|
|
161
|
-
config: {
|
|
160
|
+
config: {}
|
|
162
161
|
});
|
|
163
162
|
|
|
164
163
|
// V3 多跳买入
|
|
@@ -168,7 +167,7 @@ await fourPancakeProxyBatchBuyMerkle({
|
|
|
168
167
|
lpAddresses: ['0x...WBNB_USDT_LP', '0x...USDT_TOKEN_LP'],
|
|
169
168
|
exactTokenIn: '0x...WBNB',
|
|
170
169
|
routeType: 'v3-multi',
|
|
171
|
-
config: {
|
|
170
|
+
config: {}
|
|
172
171
|
});
|
|
173
172
|
```
|
|
174
173
|
|
|
@@ -196,7 +195,6 @@ const result = await flapCreateTokenWithBundleBuyMerkle({
|
|
|
196
195
|
meta: 'QmXxx...' // IPFS CID
|
|
197
196
|
},
|
|
198
197
|
config: {
|
|
199
|
-
apiKey: 'YOUR_MERKLE_API_KEY',
|
|
200
198
|
rpcUrl: 'https://bsc-dataseed.binance.org'
|
|
201
199
|
},
|
|
202
200
|
dexThresh: 1,
|
|
@@ -222,7 +220,7 @@ await flapBatchBuyWithBundleMerkle({
|
|
|
222
220
|
privateKeys: ['0x...买家1', '0x...买家2'],
|
|
223
221
|
buyAmounts: ['0.5', '1.0'],
|
|
224
222
|
tokenAddress: '0x...代币地址',
|
|
225
|
-
config: {
|
|
223
|
+
config: {}
|
|
226
224
|
});
|
|
227
225
|
|
|
228
226
|
// 批量卖出
|
|
@@ -231,7 +229,7 @@ await flapBatchSellWithBundleMerkle({
|
|
|
231
229
|
privateKeys: ['0x...卖家1', '0x...卖家2'],
|
|
232
230
|
sellAmounts: ['1000', '2000'],
|
|
233
231
|
tokenAddress: '0x...代币地址',
|
|
234
|
-
config: {
|
|
232
|
+
config: {}
|
|
235
233
|
});
|
|
236
234
|
```
|
|
237
235
|
|
|
@@ -253,7 +251,7 @@ await flapPancakeProxyBatchBuyMerkle({
|
|
|
253
251
|
['0x...WBNB', '0x...TOKEN']
|
|
254
252
|
],
|
|
255
253
|
routeType: 'v2',
|
|
256
|
-
config: {
|
|
254
|
+
config: {}
|
|
257
255
|
});
|
|
258
256
|
|
|
259
257
|
// V3 单跳卖出
|
|
@@ -265,7 +263,7 @@ await flapPancakeProxyBatchSellMerkle({
|
|
|
265
263
|
tokenOut: '0x...WBNB',
|
|
266
264
|
fee: 2500,
|
|
267
265
|
routeType: 'v3-single',
|
|
268
|
-
config: {
|
|
266
|
+
config: {}
|
|
269
267
|
});
|
|
270
268
|
```
|
|
271
269
|
|
|
@@ -275,7 +273,6 @@ await flapPancakeProxyBatchSellMerkle({
|
|
|
275
273
|
|
|
276
274
|
```typescript
|
|
277
275
|
type BundleMerkleConfig = {
|
|
278
|
-
apiKey: string; // Merkle API Key(必填)
|
|
279
276
|
rpcUrl?: string; // RPC 节点(可选)
|
|
280
277
|
bundleBlockOffset?: number; // Bundle 有效期,默认 200 区块
|
|
281
278
|
gasLimitMultiplier?: number; // Gas limit 倍数,默认 1.2
|
|
@@ -331,11 +328,8 @@ type BundleMerkleConfig = {
|
|
|
331
328
|
|
|
332
329
|
## 常见问题
|
|
333
330
|
|
|
334
|
-
**Q:
|
|
335
|
-
A:
|
|
336
|
-
|
|
337
|
-
**Q: Merkle API Key 如何获取?**
|
|
338
|
-
A: 访问 [Merkle.io](https://merkle.io) 注册并获取免费 API Key。
|
|
331
|
+
**Q: 现在如何提交 Bundle?**
|
|
332
|
+
A: 使用 BlockRazor、48.club 或 Emit service;Merkle 网络客户端已移除。
|
|
339
333
|
|
|
340
334
|
**Q: Bundle 交易失败怎么办?**
|
|
341
335
|
A: 检查配置参数(gasLimit、blockOffset、slippage),确保所有地址有足够余额,适当增大超时时间。
|
|
@@ -6,7 +6,8 @@ export type FourBuyFirstSignConfig = FourSignConfig & {
|
|
|
6
6
|
userType?: UserType;
|
|
7
7
|
};
|
|
8
8
|
export type FourBuyFirstConfig = CommonBundleConfig & {
|
|
9
|
-
|
|
9
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容,不再读取 */
|
|
10
|
+
apiKey?: string;
|
|
10
11
|
customRpcUrl?: string;
|
|
11
12
|
bundleBlockOffset?: number;
|
|
12
13
|
reserveGasBNB?: number;
|
|
@@ -22,7 +22,8 @@ export type FourSignConfig = {
|
|
|
22
22
|
bribeAmount?: number;
|
|
23
23
|
};
|
|
24
24
|
export type FourBundleMerkleConfig = {
|
|
25
|
-
|
|
25
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容,不再读取 */
|
|
26
|
+
apiKey?: string;
|
|
26
27
|
customRpcUrl?: string;
|
|
27
28
|
fourApiUrl?: string;
|
|
28
29
|
bundleBlockOffset?: number;
|
|
@@ -16,7 +16,8 @@ export type FourSwapSignConfig = {
|
|
|
16
16
|
};
|
|
17
17
|
/** Swap Merkle 配置(保留兼容) */
|
|
18
18
|
export type FourSwapConfig = CommonBundleConfig & {
|
|
19
|
-
|
|
19
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容,不再读取 */
|
|
20
|
+
apiKey?: string;
|
|
20
21
|
customRpcUrl?: string;
|
|
21
22
|
bundleBlockOffset?: number;
|
|
22
23
|
reserveGasBNB?: number;
|
|
@@ -113,14 +113,23 @@ export async function sweepErc20NoHop(ctx) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
// ✅ 第二步:生成归集交易(ERC20 全额归集,费用以原生币单独支付)
|
|
116
|
+
// 支付者可能既要归集又要发利润,必须先按“主交易 + 利润”预留连续 nonce;
|
|
117
|
+
// 且支付者不能再从批量 nonce 里取一次,否则 target 同时在 sources 时会拿到 m+1 做主交易、m 做利润。
|
|
116
118
|
let payerProfitNonce;
|
|
119
|
+
let payerMainNonce;
|
|
120
|
+
const payerAddressErc20 = extractProfit && nativeProfitAmount > 0n && profitPayerWalletErc20
|
|
121
|
+
? profitPayerWalletErc20.address.toLowerCase()
|
|
122
|
+
: undefined;
|
|
117
123
|
if (extractProfit && nativeProfitAmount > 0n && profitPayerWalletErc20) {
|
|
118
|
-
const
|
|
119
|
-
|
|
124
|
+
const payerIsSourceWithAmount = targetIndexInSources >= 0 && useTargetAsPayerErc20 && sweepAmounts[targetIndexInSources] > 0n;
|
|
125
|
+
const payerNeedsMain = !useTargetAsPayerErc20 || payerIsSourceWithAmount;
|
|
126
|
+
const nonces = await nonceManager.getNextNonceBatch(profitPayerWalletErc20, payerNeedsMain ? 2 : 1);
|
|
127
|
+
if (payerNeedsMain)
|
|
128
|
+
payerMainNonce = nonces[0];
|
|
129
|
+
payerProfitNonce = payerNeedsMain ? nonces[1] : nonces[0];
|
|
120
130
|
}
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
const walletsToSweepErc20 = wallets.filter((_, i) => sweepAmounts[i] > 0n && (useTargetAsPayerErc20 || i !== maxSweepIndex));
|
|
131
|
+
// 支付者已单独预留 nonce,不能再进入批量查询。
|
|
132
|
+
const walletsToSweepErc20 = wallets.filter((w, i) => sweepAmounts[i] > 0n && (!payerAddressErc20 || w.address.toLowerCase() !== payerAddressErc20));
|
|
124
133
|
const noncesErc20 = walletsToSweepErc20.length > 0 ? await nonceManager.getNextNoncesForWallets(walletsToSweepErc20) : [];
|
|
125
134
|
let nonceIdxErc20 = 0;
|
|
126
135
|
const txPromises = wallets.map(async (w, i) => {
|
|
@@ -129,10 +138,9 @@ export async function sweepErc20NoHop(ctx) {
|
|
|
129
138
|
return null;
|
|
130
139
|
// ✅ ERC20 归集:全部归集,不扣除代币(利润从 BNB 支付)
|
|
131
140
|
const actualToSend = toSend;
|
|
132
|
-
// ✅ 支付者使用预留的第一个 nonce,其他钱包使用批量获取的 nonce
|
|
133
141
|
let nonce;
|
|
134
|
-
if (
|
|
135
|
-
nonce =
|
|
142
|
+
if (payerMainNonce !== undefined && w.address.toLowerCase() === payerAddressErc20) {
|
|
143
|
+
nonce = payerMainNonce;
|
|
136
144
|
}
|
|
137
145
|
else {
|
|
138
146
|
nonce = noncesErc20[nonceIdxErc20++];
|
|
@@ -16,7 +16,7 @@ import { sweepNativeNoHop } from './native-no-hop.js';
|
|
|
16
16
|
import { sweepErc20NoHop } from './erc20-no-hop.js';
|
|
17
17
|
import { sweepWithHops } from './with-hops.js';
|
|
18
18
|
export async function sweepWithBundleMerkle(params) {
|
|
19
|
-
const { sourcePrivateKeys, target, targetPrivateKey, ratioPct, ratios, amount, amounts, tokenAddress, tokenDecimals, skipIfInsufficient = true, hopCount = 0, hopPrivateKeys, sources, config, } = params;
|
|
19
|
+
const { sourcePrivateKeys, target, targetPrivateKey, ratioPct, ratios, amount, amounts, tokenAddress, tokenDecimals, skipIfInsufficient = true, hopCount = 0, hopPrivateKeys, sources, config, startNonce, } = params;
|
|
20
20
|
// 快速返回空结果
|
|
21
21
|
if (!sourcePrivateKeys || sourcePrivateKeys.length === 0) {
|
|
22
22
|
return {
|
|
@@ -45,6 +45,17 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
45
45
|
const hopCountInput = resolveHopCountInput(actualKeys.length, hopCount, sources);
|
|
46
46
|
const preparedHops = providedHops ?? _generateHopWallets(actualKeys.length, hopCountInput);
|
|
47
47
|
const hasHops = preparedHops !== null;
|
|
48
|
+
if (providedHops) {
|
|
49
|
+
// 中转交易当前按 nonce 0/1 组装;复用已发过交易的 hop key 会直接冲突。
|
|
50
|
+
for (const group of providedHops) {
|
|
51
|
+
for (const key of group) {
|
|
52
|
+
const address = new Wallet(key).address;
|
|
53
|
+
if (await provider.getTransactionCount(address, 'latest') > 0) {
|
|
54
|
+
throw new Error('hopPrivateKeys 中包含已使用的地址;请换用未发过交易的中转私钥');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
48
59
|
const maxHopCount = hasHops ? Math.max(...preparedHops.map((h) => h.length)) : 0;
|
|
49
60
|
// 先按配置算上限,再向节点估算。估算成功就用估算值,归集才不会把没用完的 gas 退回钱包。
|
|
50
61
|
let finalGasLimit = _calculateGasLimit(config, isNative, hasHops, maxHopCount);
|
|
@@ -74,6 +85,9 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
74
85
|
const bribeSignedTx = [];
|
|
75
86
|
// ✅ 共享 NonceManager:统一协调 贿赂/利润提取 等 targetWallet 交易的 nonce,避免同一钱包 nonce 冲突
|
|
76
87
|
const nonceManager = new NonceManager(provider);
|
|
88
|
+
if (startNonce !== undefined && actualKeys[0]) {
|
|
89
|
+
nonceManager.seedNextNonce(new Wallet(actualKeys[0]).address, startNonce);
|
|
90
|
+
}
|
|
77
91
|
// ✅ 贿赂交易放在最前面(由 targetWallet 签名)
|
|
78
92
|
if (needBribeTx && targetWallet) {
|
|
79
93
|
const [bribeGasPrice, bribeNonce] = await Promise.all([
|
|
@@ -15,6 +15,7 @@ export async function sweepNativeNoHop(ctx) {
|
|
|
15
15
|
let profitHopWallets;
|
|
16
16
|
const wallets = actualKeys.map((pk) => new Wallet(pk, provider));
|
|
17
17
|
const addresses = wallets.map((w) => w.address);
|
|
18
|
+
const targetIndexInSources = addresses.findIndex((addr) => addr.toLowerCase() === target.toLowerCase());
|
|
18
19
|
// ✅ 复用主流程共享的 NonceManager(贿赂交易已用它预留 targetWallet 的 nonce,避免利润提取与之冲突)
|
|
19
20
|
const nonceManager = ctx.nonceManager ?? new NonceManager(provider);
|
|
20
21
|
// ✅ 优化:并行获取 gasPrice 和余额
|
|
@@ -91,13 +92,20 @@ export async function sweepNativeNoHop(ctx) {
|
|
|
91
92
|
const profitPayerWallet = useTargetAsPayer ? targetWallet : maxSweepIndex >= 0 ? wallets[maxSweepIndex] : null;
|
|
92
93
|
// ✅ 先为支付者预留 nonce,确保 nonce 连续
|
|
93
94
|
let payerProfitNonce;
|
|
95
|
+
let payerMainNonce;
|
|
96
|
+
const payerAddress = extractProfit && totalProfit > 0n && profitPayerWallet
|
|
97
|
+
? profitPayerWallet.address.toLowerCase()
|
|
98
|
+
: undefined;
|
|
94
99
|
if (extractProfit && totalProfit > 0n && profitPayerWallet) {
|
|
95
|
-
const
|
|
96
|
-
|
|
100
|
+
const payerIsSourceWithAmount = targetIndexInSources >= 0 && useTargetAsPayer && sweepAmounts[targetIndexInSources] > 0n;
|
|
101
|
+
const payerNeedsMain = !useTargetAsPayer || payerIsSourceWithAmount;
|
|
102
|
+
const nonces = await nonceManager.getNextNonceBatch(profitPayerWallet, payerNeedsMain ? 2 : 1);
|
|
103
|
+
if (payerNeedsMain)
|
|
104
|
+
payerMainNonce = nonces[0];
|
|
105
|
+
payerProfitNonce = payerNeedsMain ? nonces[1] : nonces[0];
|
|
97
106
|
}
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
const walletsToSweep = wallets.filter((_, i) => sweepAmounts[i] > 0n && (useTargetAsPayer || i !== maxSweepIndex));
|
|
107
|
+
// 支付者已单独预留 nonce,不能再进入批量查询。
|
|
108
|
+
const walletsToSweep = wallets.filter((w, i) => sweepAmounts[i] > 0n && (!payerAddress || w.address.toLowerCase() !== payerAddress));
|
|
101
109
|
const nonces = walletsToSweep.length > 0 ? await nonceManager.getNextNoncesForWallets(walletsToSweep) : [];
|
|
102
110
|
let nonceIdx = 0;
|
|
103
111
|
const txPromises = wallets.map(async (w, i) => {
|
|
@@ -108,17 +116,17 @@ export async function sweepNativeNoHop(ctx) {
|
|
|
108
116
|
if (extractProfit && !useTargetAsPayer) {
|
|
109
117
|
// ✅ 如果由源钱包支付利润,支付者扣除所有利润总和;其他钱包不扣利润,归集干净
|
|
110
118
|
if (i === maxSweepIndex && totalProfit > 0n) {
|
|
111
|
-
actualToSend = toSend - totalProfit; //
|
|
119
|
+
actualToSend = toSend > totalProfit ? toSend - totalProfit : 0n; // 支付者扣除利润,不足则不发
|
|
112
120
|
}
|
|
113
121
|
else {
|
|
114
122
|
actualToSend = toSend; // 其他钱包不扣利润,归集全部
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
125
|
// ✅ 如果由 targetWallet 支付利润,所有源钱包都归集全部金额
|
|
118
|
-
//
|
|
126
|
+
// 支付者 actualToSend=0 时仍发 0 值交易,消费预留 nonce,避免利润 nonce 出现空洞。
|
|
119
127
|
let nonce;
|
|
120
|
-
if (
|
|
121
|
-
nonce =
|
|
128
|
+
if (payerMainNonce !== undefined && w.address.toLowerCase() === payerAddress) {
|
|
129
|
+
nonce = payerMainNonce;
|
|
122
130
|
}
|
|
123
131
|
else {
|
|
124
132
|
nonce = nonces[nonceIdx++];
|
package/dist/bundle/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { getErc20Decimals, getErc20DecimalsMerkle, generateHopWallets, normalize
|
|
|
5
5
|
export { resolveChainName, createSignProvider, createSignWallet, resolveSignContext, normalizeItemsOrRecipientsAmounts, resolveProvidedHops, resolveHopCountInput, signBribeTransaction, type SignConfigInput, type HopItem, } from './sign-context-helpers.js';
|
|
6
6
|
export { BUNDLE_ERRORS, ERRORS, getBundleErrorMessage, getErrorMessage } from './errors.js';
|
|
7
7
|
export { getTxType, getGasPriceMultiplier, getBundleOptions, getGasPriceConfig, shouldExtractProfit, getProfitRateBps, getProfitRecipient, calculateProfit, calculateBatchProfit, getBribeAmount, type BundleGasConfigInput, type BundleMerkleConfigInput, } from './config-helpers.js';
|
|
8
|
-
export {
|
|
8
|
+
export { submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, submitDirectToRpc, submitDirectToRpcSequential, submitDirectToRpcSequentialNoWait, submitDirectToRpcParallel, submitDirectToRpcSmart, type BlockRazorSubmitConfig, type BlockRazorSubmitResult, type DirectSubmitConfig, type DirectSubmitResult, type DirectTxResult, } from './submit.js';
|
package/dist/bundle/index.js
CHANGED
|
@@ -5,4 +5,4 @@ export { getErc20Decimals, getErc20DecimalsMerkle, generateHopWallets, normalize
|
|
|
5
5
|
export { resolveChainName, createSignProvider, createSignWallet, resolveSignContext, normalizeItemsOrRecipientsAmounts, resolveProvidedHops, resolveHopCountInput, signBribeTransaction, } from './sign-context-helpers.js';
|
|
6
6
|
export { BUNDLE_ERRORS, ERRORS, getBundleErrorMessage, getErrorMessage } from './errors.js';
|
|
7
7
|
export { getTxType, getGasPriceMultiplier, getBundleOptions, getGasPriceConfig, shouldExtractProfit, getProfitRateBps, getProfitRecipient, calculateProfit, calculateBatchProfit, getBribeAmount, } from './config-helpers.js';
|
|
8
|
-
export {
|
|
8
|
+
export { submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, submitDirectToRpc, submitDirectToRpcSequential, submitDirectToRpcSequentialNoWait, submitDirectToRpcParallel, submitDirectToRpcSmart, } from './submit.js';
|
|
@@ -22,6 +22,8 @@ export declare function estimateSweepLimits(params: {
|
|
|
22
22
|
tokenAddress?: string;
|
|
23
23
|
nativeFallback: bigint;
|
|
24
24
|
tokenFallback: bigint;
|
|
25
|
+
/** 估算值安全垫;节点估值通常是刚好够,批量转账/合约收款方需要余量 */
|
|
26
|
+
bufferPercent?: number;
|
|
25
27
|
}): Promise<{
|
|
26
28
|
nativeGasLimit: bigint;
|
|
27
29
|
tokenGasLimit: bigint;
|
package/dist/bundle/internal.js
CHANGED
|
@@ -131,6 +131,10 @@ export async function batchGetTokenAndNativeBalances(provider, addresses, tokenA
|
|
|
131
131
|
}
|
|
132
132
|
/** 归集/分发:按节点估算这笔转账的 gas。失败返回 null,调用方再用固定上限。 */
|
|
133
133
|
export async function estimateSweepLimits(params) {
|
|
134
|
+
const withBuffer = (gas) => {
|
|
135
|
+
const pct = params.bufferPercent ?? 20;
|
|
136
|
+
return gas + (gas * BigInt(Math.max(0, Math.floor(pct)))) / 100n;
|
|
137
|
+
};
|
|
134
138
|
try {
|
|
135
139
|
if (!params.tokenAddress) {
|
|
136
140
|
const gas = await params.provider.estimateGas({
|
|
@@ -139,7 +143,7 @@ export async function estimateSweepLimits(params) {
|
|
|
139
143
|
value: 0n,
|
|
140
144
|
});
|
|
141
145
|
if (gas > 0n)
|
|
142
|
-
return { nativeGasLimit: gas, tokenGasLimit: gas, estimated: true };
|
|
146
|
+
return { nativeGasLimit: withBuffer(gas), tokenGasLimit: withBuffer(gas), estimated: true };
|
|
143
147
|
}
|
|
144
148
|
else {
|
|
145
149
|
const iface = new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
@@ -155,8 +159,8 @@ export async function estimateSweepLimits(params) {
|
|
|
155
159
|
]);
|
|
156
160
|
if (tokenGas > 0n) {
|
|
157
161
|
return {
|
|
158
|
-
nativeGasLimit: nativeGas > 0n ? nativeGas : params.nativeFallback,
|
|
159
|
-
tokenGasLimit: tokenGas,
|
|
162
|
+
nativeGasLimit: nativeGas > 0n ? withBuffer(nativeGas) : params.nativeFallback,
|
|
163
|
+
tokenGasLimit: withBuffer(tokenGas),
|
|
160
164
|
estimated: true,
|
|
161
165
|
};
|
|
162
166
|
}
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
* Bundle 提交 barrel
|
|
3
3
|
*
|
|
4
4
|
* 原超长文件按提交后端拆分为:
|
|
5
|
-
* - helpers.ts:
|
|
6
|
-
* - merkle.ts:Merkle 提交后端
|
|
5
|
+
* - helpers.ts:BlockRazor 共用批量提交辅助
|
|
7
6
|
* - blockrazor.ts:BlockRazor 提交后端
|
|
8
7
|
* - direct.ts:逐笔 RPC 广播后端
|
|
9
8
|
*
|
|
10
9
|
* 对外 export 符号与路径保持不变。
|
|
11
10
|
*/
|
|
12
|
-
export { submitBundleToMerkle, submitMultipleBundles, submitMultipleBundlesParallel, type MerkleSubmitConfig, type SubmitBundleResult, } from './merkle.js';
|
|
13
11
|
export { submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, type BlockRazorSubmitConfig, type BlockRazorSubmitResult, } from './blockrazor.js';
|
|
14
12
|
export { submitDirectToRpc, submitDirectToRpcSequential, submitDirectToRpcSequentialNoWait, submitDirectToRpcParallel, submitDirectToRpcSmart, type DirectSubmitConfig, type DirectTxResult, type DirectSubmitResult, } from './direct.js';
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
* Bundle 提交 barrel
|
|
3
3
|
*
|
|
4
4
|
* 原超长文件按提交后端拆分为:
|
|
5
|
-
* - helpers.ts:
|
|
6
|
-
* - merkle.ts:Merkle 提交后端
|
|
5
|
+
* - helpers.ts:BlockRazor 共用批量提交辅助
|
|
7
6
|
* - blockrazor.ts:BlockRazor 提交后端
|
|
8
7
|
* - direct.ts:逐笔 RPC 广播后端
|
|
9
8
|
*
|
|
10
9
|
* 对外 export 符号与路径保持不变。
|
|
11
10
|
*/
|
|
12
|
-
export { submitBundleToMerkle, submitMultipleBundles, submitMultipleBundlesParallel, } from './merkle.js';
|
|
13
11
|
export { submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, } from './blockrazor.js';
|
|
14
12
|
export { submitDirectToRpc, submitDirectToRpcSequential, submitDirectToRpcSequentialNoWait, submitDirectToRpcParallel, submitDirectToRpcSmart, } from './direct.js';
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export * from './constants.js';
|
|
7
7
|
export * as platforms from './platforms/index.js';
|
|
8
|
-
export { disperseWithBundleMerkle, sweepWithBundleMerkle, pairwiseTransferWithBundleMerkle, fourBundleSwapMerkle, fourBatchSwapMerkle, fourQuickBatchSwapMerkle, fourBundleBuyFirstMerkle, createTokenWithBundleBuyMerkle as fourCreateTokenWithBundleBuyMerkle, batchSellWithBundleMerkle as fourBatchSellWithBundleMerkle, fourBatchPrivateBuyMerkle,
|
|
8
|
+
export { disperseWithBundleMerkle, sweepWithBundleMerkle, pairwiseTransferWithBundleMerkle, fourBundleSwapMerkle, fourBatchSwapMerkle, fourQuickBatchSwapMerkle, fourBundleBuyFirstMerkle, createTokenWithBundleBuyMerkle as fourCreateTokenWithBundleBuyMerkle, batchSellWithBundleMerkle as fourBatchSellWithBundleMerkle, fourBatchPrivateBuyMerkle, submitBundleToBlockRazor, submitDirectToRpc, submitDirectToRpcSmart, approveFourTokenManagerBatch, } from './platforms/four/index.js';
|
|
9
9
|
export { pancakeBundleSwapMerkle, pancakeBatchSwapMerkle, pancakeQuickBatchSwapMerkle, pancakeCrossSwapMerkle, pancakeBundleBuyFirstMerkle, } from './platforms/pancake/index.js';
|
|
10
10
|
export { IroFactoryQuery, createProject as iroCreateProject, encodeCreateProjectCall as iroEncodeCreateProjectCall, parseCreateProjectEvent as iroParseCreateProjectEvent, IroTokenQuery, subscribe as iroSubscribe, pledge as iroPledge, batchSubscribe as iroBatchSubscribe, batchSubscribeForSubmit as iroBatchSubscribeForSubmit, IroPoolQuery, poolRemovePledge as iroPoolRemovePledge, poolExtract as iroPoolExtract, poolTransferAwards as iroPoolTransferAwards, batchExtract as iroBatchExtract, IRO_FACTORY_ADDRESS, IRO_FACTORY_ABI, IRO_TOKEN_ABI, IRO_POOL_ABI, CREATE_PROJECT_GAS_LIMIT as BSC_CREATE_PROJECT_GAS_LIMIT, IRO_PLATFORM_URL, IRO_TESTNET_PLATFORM_URL, setIroApiBase, setIroTestApiBase, iroApiCreateProject, iroApiUploadImage, } from './platforms/iro/index.js';
|
package/dist/chains/bsc/index.js
CHANGED
|
@@ -20,7 +20,7 @@ fourBundleBuyFirstMerkle,
|
|
|
20
20
|
// 发币 / 批量卖 / 私有买入(消费者白名单别名)
|
|
21
21
|
createTokenWithBundleBuyMerkle as fourCreateTokenWithBundleBuyMerkle, batchSellWithBundleMerkle as fourBatchSellWithBundleMerkle, fourBatchPrivateBuyMerkle,
|
|
22
22
|
// 提交
|
|
23
|
-
|
|
23
|
+
submitBundleToBlockRazor, submitDirectToRpc, submitDirectToRpcSmart,
|
|
24
24
|
// 授权
|
|
25
25
|
approveFourTokenManagerBatch, } from './platforms/four/index.js';
|
|
26
26
|
// ============================================================================
|
|
@@ -48,7 +48,8 @@ export interface V3MultiRouteParams {
|
|
|
48
48
|
}
|
|
49
49
|
export type RouteParams = V2RouteParams | V3SingleRouteParams | V3MultiRouteParams;
|
|
50
50
|
export interface PancakeBuyFirstConfig extends CommonBundleConfig {
|
|
51
|
-
|
|
51
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容,不再读取 */
|
|
52
|
+
apiKey?: string;
|
|
52
53
|
customRpcUrl?: string;
|
|
53
54
|
bundleBlockOffset?: number;
|
|
54
55
|
reserveGasBNB?: number;
|
|
@@ -103,7 +103,8 @@ export interface PancakeSwapSignConfig {
|
|
|
103
103
|
v2RouterAddress?: string;
|
|
104
104
|
}
|
|
105
105
|
export interface PancakeSwapConfig extends CommonBundleConfig {
|
|
106
|
-
|
|
106
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容,不再读取 */
|
|
107
|
+
apiKey?: string;
|
|
107
108
|
customRpcUrl?: string;
|
|
108
109
|
bundleBlockOffset?: number;
|
|
109
110
|
reserveGasBNB?: number;
|
|
@@ -19,6 +19,6 @@ export { sweepWithBundleMerkle } from '../../../../bundle/four-meme/utils-sweep.
|
|
|
19
19
|
export { pairwiseTransferWithBundleMerkle } from '../../../../bundle/four-meme/utils-pairwise.js';
|
|
20
20
|
export { fourPancakeProxyBatchBuyMerkle, fourPancakeProxyBatchSellMerkle, approveFourPancakeProxy, approveFourPancakeProxyBatch, } from '../../../../bundle/four-meme/pancake-proxy.js';
|
|
21
21
|
export { approveFourTokenManagerBatch } from '../../../../bundle/four-meme/approve-tokenmanager.js';
|
|
22
|
-
export {
|
|
22
|
+
export { submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, submitDirectToRpc, submitDirectToRpcSequential, submitDirectToRpcSequentialNoWait, submitDirectToRpcParallel, submitDirectToRpcSmart, type BlockRazorSubmitConfig, type BlockRazorSubmitResult, type DirectSubmitConfig, type DirectSubmitResult, type DirectTxResult, } from '../../../../bundle/submit.js';
|
|
23
23
|
export { fourBundleBuyFirstMerkle } from '../../../../bundle/four-meme/swap-buy-first.js';
|
|
24
24
|
export { fourBundleSwapMerkle, fourBatchSwapMerkle, fourQuickBatchSwapMerkle, } from '../../../../bundle/four-meme/swap.js';
|
|
@@ -24,7 +24,7 @@ export { fourPancakeProxyBatchBuyMerkle, fourPancakeProxyBatchSellMerkle, approv
|
|
|
24
24
|
// TokenManager 授权方法
|
|
25
25
|
export { approveFourTokenManagerBatch } from '../../../../bundle/four-meme/approve-tokenmanager.js';
|
|
26
26
|
// Bundle 提交方法(服务器端使用)
|
|
27
|
-
export {
|
|
27
|
+
export { submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, submitDirectToRpc, submitDirectToRpcSequential, submitDirectToRpcSequentialNoWait, submitDirectToRpcParallel, submitDirectToRpcSmart, } from '../../../../bundle/submit.js';
|
|
28
28
|
// 先买后卖换手方法
|
|
29
29
|
export { fourBundleBuyFirstMerkle } from '../../../../bundle/four-meme/swap-buy-first.js';
|
|
30
30
|
// 换手方法
|
|
@@ -146,16 +146,18 @@ export class BlockRazorClient {
|
|
|
146
146
|
}
|
|
147
147
|
// 验证 Gas Price(最低 0.05 Gwei)
|
|
148
148
|
for (const rawTx of params.txs) {
|
|
149
|
+
let tx;
|
|
149
150
|
try {
|
|
150
|
-
|
|
151
|
-
const gasPrice = tx.gasPrice || 0n;
|
|
152
|
-
const minGasPrice = ethers.parseUnits(String(MIN_GAS_PRICE_GWEI), 'gwei');
|
|
153
|
-
if (gasPrice < minGasPrice) {
|
|
154
|
-
throw new Error(`Transaction gas price (${ethers.formatUnits(gasPrice, 'gwei')} gwei) is below minimum (${MIN_GAS_PRICE_GWEI} gwei)`);
|
|
155
|
-
}
|
|
151
|
+
tx = Transaction.from(rawTx);
|
|
156
152
|
}
|
|
157
153
|
catch {
|
|
158
154
|
// 解析失败,跳过检查
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const gasPrice = tx.gasPrice && tx.gasPrice > 0n ? tx.gasPrice : tx.maxFeePerGas ?? 0n;
|
|
158
|
+
const minGasPrice = ethers.parseUnits(String(MIN_GAS_PRICE_GWEI), 'gwei');
|
|
159
|
+
if (gasPrice < minGasPrice) {
|
|
160
|
+
throw new Error(`Transaction gas price (${ethers.formatUnits(gasPrice, 'gwei')} gwei) is below minimum (${MIN_GAS_PRICE_GWEI} gwei)`);
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
// 构建请求参数
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Wallet } from 'ethers';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { BlockRazorClient } from './blockrazor.js';
|
|
4
|
+
describe('BlockRazor gas price validation', () => {
|
|
5
|
+
it('低于 0.05 Gwei 的签名交易被拒绝', async () => {
|
|
6
|
+
const client = new BlockRazorClient({
|
|
7
|
+
apiKey: 'test',
|
|
8
|
+
chainId: 56,
|
|
9
|
+
customRpcUrl: 'http://127.0.0.1:1',
|
|
10
|
+
});
|
|
11
|
+
const wallet = Wallet.createRandom();
|
|
12
|
+
const raw = await wallet.signTransaction({
|
|
13
|
+
to: Wallet.createRandom().address,
|
|
14
|
+
value: 0n,
|
|
15
|
+
nonce: 0,
|
|
16
|
+
gasLimit: 21000n,
|
|
17
|
+
gasPrice: 1n,
|
|
18
|
+
chainId: 56,
|
|
19
|
+
type: 0,
|
|
20
|
+
});
|
|
21
|
+
await expect(client.sendBundleRaw({ txs: [raw] })).rejects.toThrow(/below minimum/);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -3,6 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { FourClient, buildLoginMessage, buildCreateTokenReq, type FourConfig, type GenerateNonceReq, type LoginReq, type CreateTokenReq, type CreateTokenResp, type FourTokenTaxInfo, type FourRaisedToken, type FourTokenLabel, } from './four.js';
|
|
5
5
|
export { Club48Client, sendBatchPrivateTransactions, sendBackrunBundle, type BundleParams, type BundleStatus, type Club48Config, } from './club48.js';
|
|
6
|
-
export { MerkleClient, createMerkleClient, type MerkleConfig, type BundleParams as MerkleBundleParams, type SendBundleOptions, type BundleResult, type TransactionResult, } from './merkle.js';
|
|
7
6
|
export { BlockRazorClient, createBlockRazorClient, BLOCKRAZOR_BUILDER_EOA, type BlockRazorConfig, type BlockRazorBundleParams, type SendBundleOptions as BlockRazorSendBundleOptions, type BundleResult as BlockRazorBundleResult, type TransactionResult as BlockRazorTransactionResult, type IncentiveTransactionParams, } from './blockrazor.js';
|
|
8
7
|
export { EmitServiceClient, submitToEmitServiceBlockRazor, type EmitServiceClientConfig, type EmitServiceSubmitRequest, type EmitServiceSubmitResponse, type EmitServiceSubmitResponseData, type EmitServiceRouteConfigResponse, } from './emitservice.js';
|
|
@@ -3,6 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { FourClient, buildLoginMessage, buildCreateTokenReq, } from './four.js';
|
|
5
5
|
export { Club48Client, sendBatchPrivateTransactions, sendBackrunBundle, } from './club48.js';
|
|
6
|
-
export { MerkleClient, createMerkleClient, } from './merkle.js';
|
|
7
6
|
export { BlockRazorClient, createBlockRazorClient, BLOCKRAZOR_BUILDER_EOA, } from './blockrazor.js';
|
|
8
7
|
export { EmitServiceClient, submitToEmitServiceBlockRazor, } from './emitservice.js';
|
|
@@ -38,12 +38,15 @@ export class MerkleClient {
|
|
|
38
38
|
constructor(config) {
|
|
39
39
|
this.chainId = config.chainId;
|
|
40
40
|
this.chainName = config.chainId === CHAINS.BSC.chainId ? 'bsc' : 'ethereum';
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
? CHAINS.BSC.
|
|
45
|
-
: CHAINS.ETHEREUM.
|
|
46
|
-
const rpcUrl = config.customRpcUrl ||
|
|
41
|
+
// Blink/Merkle 的 bundle RPC 是带 API key 的节点 endpoint;公共 BSC 节点
|
|
42
|
+
// 不支持 eth_sendBundle。customRpcUrl 可显式覆盖(需自带 key)。
|
|
43
|
+
const bundleBase = config.chainId === CHAINS.BSC.chainId
|
|
44
|
+
? CHAINS.BSC.bundleRpcUrl
|
|
45
|
+
: CHAINS.ETHEREUM.bundleRpcUrl;
|
|
46
|
+
const rpcUrl = config.customRpcUrl || (bundleBase ? `${bundleBase}/v1/${config.apiKey}` : '');
|
|
47
|
+
if (!rpcUrl) {
|
|
48
|
+
throw new Error('Merkle bundle RPC endpoint is not configured');
|
|
49
|
+
}
|
|
47
50
|
this.provider = createProvider(rpcUrl, this.chainId, this.chainName);
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
@@ -366,10 +369,12 @@ export class MerkleClient {
|
|
|
366
369
|
*/
|
|
367
370
|
getProviderInfo() {
|
|
368
371
|
const rpcConnection = this.provider._getConnection?.();
|
|
372
|
+
const rawUrl = rpcConnection?.url ?? 'unknown';
|
|
369
373
|
return {
|
|
370
374
|
chainId: this.chainId,
|
|
371
375
|
chainName: this.chainName,
|
|
372
|
-
|
|
376
|
+
// API key 在路径中,调试输出只保留脱敏后的 endpoint。
|
|
377
|
+
rpcUrl: rawUrl.replace(/\/v1\/[^/?#]+/, '/v1/***'),
|
|
373
378
|
};
|
|
374
379
|
}
|
|
375
380
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { MerkleClient } from './merkle.js';
|
|
3
|
+
describe('MerkleClient endpoint', () => {
|
|
4
|
+
it('默认使用带 apiKey 的 Blink BSC bundle RPC', () => {
|
|
5
|
+
const client = new MerkleClient({ apiKey: 'test-key', chainId: 56 });
|
|
6
|
+
expect(client.getProviderInfo().rpcUrl).toContain('https://bsc.blinklabs.xyz/v1/***');
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -14,6 +14,8 @@ export declare class NonceManager {
|
|
|
14
14
|
private readOnchainNonce;
|
|
15
15
|
getNextNonce(wallet: Wallet | string): Promise<number>;
|
|
16
16
|
getNextNonceBatch(wallet: Wallet | string, count: number): Promise<number[]>;
|
|
17
|
+
/** 预填某个地址的下一个 nonce(用于调用方显式 startNonce 的并行场景)。 */
|
|
18
|
+
seedNextNonce(wallet: Wallet | string, nextNonce: number): void;
|
|
17
19
|
clearTemp(): void;
|
|
18
20
|
getTempCachedNonce(address: string): number | undefined;
|
|
19
21
|
getNextNoncesForWallets(wallets: (Wallet | string)[]): Promise<number[]>;
|
|
@@ -157,6 +157,14 @@ export class NonceManager {
|
|
|
157
157
|
return Array.from({ length: count }, (_, i) => startNonce + i);
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
|
+
/** 预填某个地址的下一个 nonce(用于调用方显式 startNonce 的并行场景)。 */
|
|
161
|
+
seedNextNonce(wallet, nextNonce) {
|
|
162
|
+
if (!Number.isInteger(nextNonce) || nextNonce < 0)
|
|
163
|
+
return;
|
|
164
|
+
const address = typeof wallet === 'string' ? wallet : wallet.address;
|
|
165
|
+
const key = address.toLowerCase();
|
|
166
|
+
this.tempNonceCache.set(key, nextNonce);
|
|
167
|
+
}
|
|
160
168
|
clearTemp() {
|
|
161
169
|
this.tempNonceCache.clear();
|
|
162
170
|
}
|
|
@@ -210,10 +218,22 @@ export class NonceManager {
|
|
|
210
218
|
const rawResults = await asBatchSendProvider(this.provider)._send(batchRequests);
|
|
211
219
|
const byId = new Map();
|
|
212
220
|
for (const row of rawResults) {
|
|
221
|
+
if (row.error || row.result == null) {
|
|
222
|
+
throw new Error(`batch eth_getTransactionCount failed: ${row.error?.message || 'missing result'}`);
|
|
223
|
+
}
|
|
213
224
|
const id = Number(row.id);
|
|
225
|
+
if (!Number.isInteger(id))
|
|
226
|
+
throw new Error('batch eth_getTransactionCount returned invalid id');
|
|
214
227
|
byId.set(id, parseRpcQuantity(row.result));
|
|
215
228
|
}
|
|
216
|
-
queryResults = uniqueQueries.map((_, idx) =>
|
|
229
|
+
queryResults = uniqueQueries.map((_, idx) => {
|
|
230
|
+
const latest = byId.get(idx * 2 + 1);
|
|
231
|
+
const pending = byId.get(idx * 2 + 2);
|
|
232
|
+
if (latest === undefined || pending === undefined) {
|
|
233
|
+
throw new Error('batch eth_getTransactionCount returned incomplete result');
|
|
234
|
+
}
|
|
235
|
+
return Math.max(latest, pending);
|
|
236
|
+
});
|
|
217
237
|
}
|
|
218
238
|
catch {
|
|
219
239
|
queryResults = await Promise.all(uniqueQueries.map(({ address }) => this.readOnchainNonce(address)));
|
|
@@ -46,18 +46,29 @@ Wallet.prototype.signTransaction = async function (tx) {
|
|
|
46
46
|
/* 节点没给出 gas 价格时保留交易上已有的出价 */
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
signable = { ...signable, gasLimit: estimated };
|
|
49
|
+
let hasGasLimit = false;
|
|
50
|
+
if (signable.gasLimit != null) {
|
|
51
|
+
try {
|
|
52
|
+
hasGasLimit = BigInt(signable.gasLimit) > 0n;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
hasGasLimit = false;
|
|
56
|
+
}
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if (!hasGasLimit) {
|
|
59
|
+
try {
|
|
60
|
+
const estimated = await provider.estimateGas({
|
|
61
|
+
from: signable.from ?? this.address,
|
|
62
|
+
to: signable.to,
|
|
63
|
+
data: signable.data,
|
|
64
|
+
value: signable.value,
|
|
65
|
+
});
|
|
66
|
+
if (estimated > 0n)
|
|
67
|
+
signable = { ...signable, gasLimit: estimated };
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* 节点没给出 gas 上限时保留原来的上限,避免发不出去 */
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
74
|
return _origWalletSign.call(this, signable);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './wallet-sign-patch.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Transaction, Wallet } from 'ethers';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import './wallet-sign-patch.js';
|
|
4
|
+
describe('wallet sign patch gasLimit', () => {
|
|
5
|
+
it('保留调用方显式 gasLimit,不被 estimateGas 覆盖', async () => {
|
|
6
|
+
const estimateGas = vi.fn(async () => 21000n);
|
|
7
|
+
const provider = { estimateGas };
|
|
8
|
+
const wallet = new Wallet(Wallet.createRandom().privateKey, provider);
|
|
9
|
+
const raw = await wallet.signTransaction({
|
|
10
|
+
to: Wallet.createRandom().address,
|
|
11
|
+
value: 0n,
|
|
12
|
+
nonce: 0,
|
|
13
|
+
gasLimit: 500000n,
|
|
14
|
+
gasPrice: 1000000000n,
|
|
15
|
+
chainId: 56,
|
|
16
|
+
type: 0,
|
|
17
|
+
});
|
|
18
|
+
expect(Transaction.from(raw).gasLimit).toBe(500000n);
|
|
19
|
+
expect(estimateGas).not.toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -71,6 +71,19 @@ export async function directV2BatchBuy(params) {
|
|
|
71
71
|
activeNonces = activeIndices.map((i) => nonces[i]);
|
|
72
72
|
activeBuyAllowances = activeIndices.map((i) => buyAllowances[i]);
|
|
73
73
|
}
|
|
74
|
+
// 0 金额钱包不参与 swap;否则 amountIn=0 会让整包 revert,拖死正常钱包。
|
|
75
|
+
const positiveIndices = activeFlowAmounts
|
|
76
|
+
.map((amount, index) => (amount > 0n ? index : -1))
|
|
77
|
+
.filter((index) => index >= 0);
|
|
78
|
+
if (positiveIndices.length === 0) {
|
|
79
|
+
throw new Error('所有买方金额为 0,无法执行');
|
|
80
|
+
}
|
|
81
|
+
if (positiveIndices.length !== activeWallets.length) {
|
|
82
|
+
activeWallets = positiveIndices.map((i) => activeWallets[i]);
|
|
83
|
+
activeFlowAmounts = positiveIndices.map((i) => activeFlowAmounts[i]);
|
|
84
|
+
activeNonces = positiveIndices.map((i) => activeNonces[i]);
|
|
85
|
+
activeBuyAllowances = positiveIndices.map((i) => activeBuyAllowances[i]);
|
|
86
|
+
}
|
|
74
87
|
const activeTotalFlowWei = activeFlowAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
75
88
|
const activeBaseProfitWei = calculateProfitAmount(activeTotalFlowWei);
|
|
76
89
|
let activeProfitWei = activeBaseProfitWei > 0n ? activeBaseProfitWei : 0n;
|
|
@@ -4,13 +4,13 @@ import { FLAP_PORTAL_ADDRESSES } from '../../constants.js';
|
|
|
4
4
|
import { ZERO_ADDRESS } from '../../../../core/constants/index.js';
|
|
5
5
|
import { GAS_LIMITS } from '../../../../core/constants/index.js';
|
|
6
6
|
import { PORTAL_ABI, getTxType, getGasPriceConfig, shouldExtractProfit, calculateBatchProfit, getProfitRecipient, getBribeAmount, BLOCKRAZOR_BUILDER_EOA, } from '../config.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createPrivateContext, splitBuyAmount, parseMinOutput, resolveGasLimit, resolveBatchMinOutputs, findMaxIndex, signBatchTransactions, } from './helpers.js';
|
|
8
8
|
/**
|
|
9
9
|
* 私有购买(单笔)(Merkle 版本)
|
|
10
10
|
*/
|
|
11
11
|
export async function flapPrivateBuyMerkle(params) {
|
|
12
12
|
const { chain, privateKey, tokenAddress, amountIn, minOutputAmount, config } = params;
|
|
13
|
-
const { chainId, provider } =
|
|
13
|
+
const { chainId, provider } = createPrivateContext(chain, config);
|
|
14
14
|
const wallet = new Wallet(privateKey, provider);
|
|
15
15
|
const portalAddr = FLAP_PORTAL_ADDRESSES[chain];
|
|
16
16
|
const originalAmountWei = ethers.parseEther(amountIn);
|
|
@@ -97,7 +97,7 @@ export async function flapBatchPrivateBuyMerkle(params) {
|
|
|
97
97
|
if (privateKeys.length !== amountsIn.length) {
|
|
98
98
|
throw new Error('privateKeys and amountsIn length mismatch');
|
|
99
99
|
}
|
|
100
|
-
const { chainId, provider } =
|
|
100
|
+
const { chainId, provider } = createPrivateContext(chain, config);
|
|
101
101
|
const portalAddr = FLAP_PORTAL_ADDRESSES[chain];
|
|
102
102
|
const wallets = privateKeys.map((k) => new Wallet(k, provider));
|
|
103
103
|
const originalAmountsWei = amountsIn.map((a) => ethers.parseEther(a));
|
|
@@ -2,7 +2,7 @@ import { ethers, Wallet } from 'ethers';
|
|
|
2
2
|
import type { FlapPrivateBuyMerkleParams } from '../types.js';
|
|
3
3
|
type FlapChain = FlapPrivateBuyMerkleParams['chain'];
|
|
4
4
|
type FlapConfig = FlapPrivateBuyMerkleParams['config'];
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function createPrivateContext(chain: FlapChain, config: FlapConfig): {
|
|
6
6
|
chainId: number;
|
|
7
7
|
provider: ethers.JsonRpcProvider;
|
|
8
8
|
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { ethers } from 'ethers';
|
|
2
|
-
import { MerkleClient } from '../../../../core/clients/merkle.js';
|
|
3
2
|
import { createProvider } from '../../../../utils/provider-factory.js';
|
|
4
3
|
import { ZERO_ADDRESS, CHAINS, applySlippageBps } from '../../../../core/constants/index.js';
|
|
5
4
|
import { CHAIN_ID_MAP, getTxType, calculateProfit, } from '../config.js';
|
|
6
5
|
const DEFAULT_GAS_LIMIT = 800000;
|
|
7
|
-
export function
|
|
6
|
+
export function createPrivateContext(chain, config) {
|
|
8
7
|
const chainId = CHAIN_ID_MAP[chain];
|
|
9
8
|
// ✅ 检查是否为 Merkle 支持的链(BSC, ETH)
|
|
10
9
|
// Monad 不支持 Merkle 捆绑服务,使用普通 RPC
|
|
@@ -19,14 +18,11 @@ export function createMerkleContext(chain, config) {
|
|
|
19
18
|
provider: createProvider(config.customRpcUrl, chainId, chain.toLowerCase()),
|
|
20
19
|
};
|
|
21
20
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
chainId: chainId, // 类型断言为 Merkle 支持的 chainId
|
|
25
|
-
customRpcUrl: config.customRpcUrl,
|
|
26
|
-
});
|
|
21
|
+
const rpcUrl = config.customRpcUrl
|
|
22
|
+
|| (chainId === CHAINS.BSC.chainId ? CHAINS.BSC.rpcUrls.default : CHAINS.ETHEREUM.rpcUrls.default);
|
|
27
23
|
return {
|
|
28
24
|
chainId,
|
|
29
|
-
provider:
|
|
25
|
+
provider: createProvider(rpcUrl, chainId, chain.toLowerCase()),
|
|
30
26
|
};
|
|
31
27
|
}
|
|
32
28
|
export function splitBuyAmount(amount, extractProfit) {
|
|
@@ -4,13 +4,13 @@ import { FLAP_PORTAL_ADDRESSES } from '../../constants.js';
|
|
|
4
4
|
import { ZERO_ADDRESS, applySlippageBps } from '../../../../core/constants/index.js';
|
|
5
5
|
import { GAS_LIMITS } from '../../../../core/constants/index.js';
|
|
6
6
|
import { PORTAL_ABI, getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, getProfitRecipient, getBribeAmount, BLOCKRAZOR_BUILDER_EOA, } from '../config.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createPrivateContext, resolveGasLimit, resolveSingleSellOutputs, signBatchTransactions, summarizeSellProfits, } from './helpers.js';
|
|
8
8
|
/**
|
|
9
9
|
* 私有卖出(单笔)(Merkle 版本)
|
|
10
10
|
*/
|
|
11
11
|
export async function flapPrivateSellMerkle(params) {
|
|
12
12
|
const { chain, privateKey, tokenAddress, amount, minOutputAmount, config } = params;
|
|
13
|
-
const { chainId, provider } =
|
|
13
|
+
const { chainId, provider } = createPrivateContext(chain, config);
|
|
14
14
|
const wallet = new Wallet(privateKey, provider);
|
|
15
15
|
const portalAddr = FLAP_PORTAL_ADDRESSES[chain];
|
|
16
16
|
const amountWei = ethers.parseUnits(amount, 18);
|
|
@@ -95,7 +95,7 @@ export async function flapBatchPrivateSellMerkle(params) {
|
|
|
95
95
|
if (privateKeys.length !== amounts.length) {
|
|
96
96
|
throw new Error('privateKeys and amounts length mismatch');
|
|
97
97
|
}
|
|
98
|
-
const { chainId, provider } =
|
|
98
|
+
const { chainId, provider } = createPrivateContext(chain, config);
|
|
99
99
|
const portalAddr = FLAP_PORTAL_ADDRESSES[chain];
|
|
100
100
|
const wallets = privateKeys.map((k) => new Wallet(k, provider));
|
|
101
101
|
const amountsWei = amounts.map((a) => ethers.parseUnits(a, 18));
|
|
@@ -13,7 +13,8 @@ export interface FlapBuyFirstSignConfig extends FlapSignConfig {
|
|
|
13
13
|
userType?: UserType;
|
|
14
14
|
}
|
|
15
15
|
export interface FlapBuyFirstConfig extends CommonBundleConfig {
|
|
16
|
-
|
|
16
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容 */
|
|
17
|
+
apiKey?: string;
|
|
17
18
|
customRpcUrl?: string;
|
|
18
19
|
bundleBlockOffset?: number;
|
|
19
20
|
reserveGasETH?: number;
|
|
@@ -49,7 +49,8 @@ export declare const MONAD_WMON: "0x3bd359c1119da7da1d913d1c4d2b7c461115433a";
|
|
|
49
49
|
export declare function getTokenToNativeQuote(provider: ethers.JsonRpcProvider, tokenAddress: string, tokenAmount: bigint, chainId: number): Promise<bigint>;
|
|
50
50
|
export type FlapChain = 'bsc' | 'xlayer' | 'base';
|
|
51
51
|
export interface FlapSwapConfig extends CommonBundleConfig {
|
|
52
|
-
|
|
52
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容 */
|
|
53
|
+
apiKey?: string;
|
|
53
54
|
customRpcUrl?: string;
|
|
54
55
|
bundleBlockOffset?: number;
|
|
55
56
|
reserveGasETH?: number;
|
|
@@ -100,6 +100,9 @@ export async function calculateBuyerNeed({ buyer, quotedNative, reserveGasEth, n
|
|
|
100
100
|
buyerBalance = await erc20.balanceOf(buyer.address);
|
|
101
101
|
}
|
|
102
102
|
const estimatedBuyerNeed = quotedNative;
|
|
103
|
+
if (estimatedBuyerNeed <= 0n) {
|
|
104
|
+
throw new Error('卖出报价为 0,拒绝用买方全部余额买入;请稍后重试或检查池子流动性');
|
|
105
|
+
}
|
|
103
106
|
const buyerNeedTotal = useNativeToken ? estimatedBuyerNeed + reserveGas : estimatedBuyerNeed;
|
|
104
107
|
if (buyerBalance < buyerNeedTotal) {
|
|
105
108
|
if (useNativeToken) {
|
|
@@ -113,7 +116,7 @@ export async function calculateBuyerNeed({ buyer, quotedNative, reserveGasEth, n
|
|
|
113
116
|
` - 实际: ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
|
-
const maxBuyerValue = estimatedBuyerNeed
|
|
119
|
+
const maxBuyerValue = estimatedBuyerNeed;
|
|
117
120
|
return { reserveGas, buyerBalance, buyerNeedTotal, maxBuyerValue };
|
|
118
121
|
}
|
|
119
122
|
export async function validateBalances({ buyerAddress, portalGasCost, provider, chainContext, seller, useNativeToken, }) {
|
|
@@ -168,7 +171,7 @@ export async function planNonces({ seller, buyer, approvalExists, extractProfit,
|
|
|
168
171
|
return { sellerNonce: nonces[0], buyerNonce: nonces[1] };
|
|
169
172
|
}
|
|
170
173
|
export async function buildProfitTransaction({ provider, seller, profitAmount, profitNonce, gasPrice, chainId, txType, }) {
|
|
171
|
-
if (
|
|
174
|
+
if (profitNonce === undefined || profitAmount === 0n) {
|
|
172
175
|
return null;
|
|
173
176
|
}
|
|
174
177
|
const profitHopResult = await buildProfitHopTransactions({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ethers, Wallet } from 'ethers';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { calculateBuyerNeed } from './swap-planning.js';
|
|
4
|
+
describe('calculateBuyerNeed quote guard', () => {
|
|
5
|
+
it('报价为 0 时拒绝用买方全部余额买入', async () => {
|
|
6
|
+
const provider = {
|
|
7
|
+
getBalance: vi.fn(async () => ethers.parseEther('1')),
|
|
8
|
+
};
|
|
9
|
+
const buyer = new Wallet(Wallet.createRandom().privateKey, provider);
|
|
10
|
+
await expect(calculateBuyerNeed({
|
|
11
|
+
buyer,
|
|
12
|
+
quotedNative: 0n,
|
|
13
|
+
nativeToken: 'BNB',
|
|
14
|
+
})).rejects.toThrow(/卖出报价为 0/);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -62,7 +62,8 @@ export type FlapSignConfig = {
|
|
|
62
62
|
extractProfitOnLaunch?: boolean;
|
|
63
63
|
};
|
|
64
64
|
export type FlapBundleMerkleConfig = {
|
|
65
|
-
|
|
65
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容,不再读取 */
|
|
66
|
+
apiKey?: string;
|
|
66
67
|
customRpcUrl?: string;
|
|
67
68
|
bundleBlockOffset?: number;
|
|
68
69
|
minBlockOffset?: number;
|
|
@@ -8,5 +8,5 @@ export { sweepWithBundleMerkle } from '../../bundle/four-meme/utils-sweep.js';
|
|
|
8
8
|
export { pairwiseTransferWithBundleMerkle } from '../../bundle/four-meme/utils-pairwise.js';
|
|
9
9
|
export { fourBundleSwapMerkle, fourBatchSwapMerkle, fourQuickBatchSwapMerkle } from '../../bundle/four-meme/swap.js';
|
|
10
10
|
export { fourBundleBuyFirstMerkle } from '../../bundle/four-meme/swap-buy-first.js';
|
|
11
|
-
export {
|
|
11
|
+
export { submitBundleToBlockRazor, submitDirectToRpc, submitDirectToRpcSmart, type BlockRazorSubmitConfig, type DirectSubmitConfig, } from '../../bundle/submit.js';
|
|
12
12
|
export type { DisperseSignParams, DisperseMerkleResult, SweepSignParams, SweepMerkleResult, FourBundleSwapSignParams, FourSwapResult, FourBatchSwapSignParams, FourBatchSwapResult, FourQuickBatchSwapSignParams, FourQuickBatchSwapResult, PairwiseTransferSignParams, PairwiseTransferMerkleResult, } from '../../bundle/four-meme/types/index.js';
|
|
@@ -8,4 +8,4 @@ export { sweepWithBundleMerkle } from '../../bundle/four-meme/utils-sweep.js';
|
|
|
8
8
|
export { pairwiseTransferWithBundleMerkle } from '../../bundle/four-meme/utils-pairwise.js';
|
|
9
9
|
export { fourBundleSwapMerkle, fourBatchSwapMerkle, fourQuickBatchSwapMerkle } from '../../bundle/four-meme/swap.js';
|
|
10
10
|
export { fourBundleBuyFirstMerkle } from '../../bundle/four-meme/swap-buy-first.js';
|
|
11
|
-
export {
|
|
11
|
+
export { submitBundleToBlockRazor, submitDirectToRpc, submitDirectToRpcSmart, } from '../../bundle/submit.js';
|
|
@@ -20,7 +20,7 @@ export { sweepWithBundleMerkle } from '../bundle/four-meme/utils-sweep.js';
|
|
|
20
20
|
export { pairwiseTransferWithBundleMerkle } from '../bundle/four-meme/utils-pairwise.js';
|
|
21
21
|
export { fourPancakeProxyBatchBuyMerkle, fourPancakeProxyBatchSellMerkle, approveFourPancakeProxy, approveFourPancakeProxyBatch, } from '../bundle/four-meme/pancake-proxy.js';
|
|
22
22
|
export { approveFourTokenManagerBatch } from '../bundle/four-meme/approve-tokenmanager.js';
|
|
23
|
-
export {
|
|
23
|
+
export { submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, type BlockRazorSubmitConfig, type BlockRazorSubmitResult, } from '../bundle/submit.js';
|
|
24
24
|
export type { FourBundleMerkleConfig, FourSignConfig, FourCreateWithBundleBuySignParams, FourBatchBuySignParams, FourBatchSellSignParams, FourPrivateBuySignParams, FourPrivateSellSignParams, FourBatchPrivateBuySignParams, FourBatchPrivateSellSignParams, FourPancakeProxyBatchBuySignParams, FourPancakeProxyBatchSellSignParams, DisperseSignParams as FourDisperseSignParams, SweepSignParams as FourSweepSignParams, FourCreateWithBundleBuyMerkleParams, FourCreateWithBundleBuyMerkleResult, FourBatchBuyMerkleParams, FourBatchBuyMerkleResult, FourBatchSellMerkleParams, FourBatchSellMerkleResult, FourPrivateBuyMerkleParams, FourPrivateSellMerkleParams, FourBatchPrivateBuyMerkleParams, FourBatchPrivateSellMerkleParams, FourBatchPrivateMerkleResult, FourPrivateTransactionResult, DisperseMerkleParams, DisperseMerkleResult, SweepMerkleParams, SweepMerkleResult, FourPancakeProxyBatchBuyParams, FourPancakeProxyBatchBuyResult, FourPancakeProxyBatchSellParams, FourPancakeProxyBatchSellResult, FourPancakeProxyApprovalParams, FourPancakeProxyApprovalBatchParams, FourPancakeProxyApprovalBatchResult, ApproveFourTokenManagerBatchParams, ApproveFourTokenManagerBatchResult, } from '../bundle/four-meme/types/index.js';
|
|
25
25
|
export type { OpenFourCreateSignParams } from '../bundle/four-meme/core.js';
|
|
26
26
|
export type { OpenFourBatchBuySignParams } from '../bundle/four-meme/private.js';
|
|
@@ -20,7 +20,7 @@ export { sweepWithBundleMerkle } from '../bundle/four-meme/utils-sweep.js';
|
|
|
20
20
|
export { pairwiseTransferWithBundleMerkle } from '../bundle/four-meme/utils-pairwise.js';
|
|
21
21
|
export { fourPancakeProxyBatchBuyMerkle, fourPancakeProxyBatchSellMerkle, approveFourPancakeProxy, approveFourPancakeProxyBatch, } from '../bundle/four-meme/pancake-proxy.js';
|
|
22
22
|
export { approveFourTokenManagerBatch } from '../bundle/four-meme/approve-tokenmanager.js';
|
|
23
|
-
export {
|
|
23
|
+
export { submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, } from '../bundle/submit.js';
|
|
24
24
|
// ✅ Four.meme TaxToken 工具(税币识别/查询/领取奖励)
|
|
25
25
|
export { isTaxToken as fourIsTaxToken, isAntiSniperFeeMode as fourIsAntiSniperFeeMode, getCreatorType as fourGetCreatorType, getTaxTokenInfo as fourGetTaxTokenInfo, getClaimableFee as fourGetClaimableFee, getClaimedFee as fourGetClaimedFee, getUserRewardInfo as fourGetUserRewardInfo, buildClaimFeeTx as fourBuildClaimFeeTx, buildBatchClaimFeeTxs as fourBuildBatchClaimFeeTxs, validateFourTokenTaxInfo, TAX_TOKEN_ABI, TransferMode as FourTransferMode, } from '../domains/four/index.js';
|
|
26
26
|
export { PinataClient } from '../domains/flap/pinata.js';
|
|
@@ -31,7 +31,6 @@ export { parseFlapError, getFlapErrorMessage, getFlapErrorMessageEn, type FlapEr
|
|
|
31
31
|
export { getFlapMetaByAddress, getFlapMetasByAddresses } from '../domains/flap/meta.js';
|
|
32
32
|
export { createTokenFlow, type CreateTokenFlowInput, type CreateTokenFlowOutput } from '../domains/flows/create.js';
|
|
33
33
|
export { Club48Client, sendBatchPrivateTransactions, sendBackrunBundle, type BundleParams, type BundleStatus, type Club48Config, } from '../core/clients/club48.js';
|
|
34
|
-
export { MerkleClient, createMerkleClient, type MerkleConfig, type BundleParams as MerkleBundleParams, type SendBundleOptions, type BundleResult, type TransactionResult, } from '../core/clients/merkle.js';
|
|
35
34
|
export { BlockRazorClient, createBlockRazorClient, BLOCKRAZOR_BUILDER_EOA, type BlockRazorConfig, type BlockRazorBundleParams, type SendBundleOptions as BlockRazorSendBundleOptions, type BundleResult as BlockRazorBundleResult, type TransactionResult as BlockRazorTransactionResult, type IncentiveTransactionParams, } from '../core/clients/blockrazor.js';
|
|
36
35
|
export { EmitServiceClient, submitToEmitServiceBlockRazor, type EmitServiceClientConfig, type EmitServiceSubmitRequest, type EmitServiceSubmitResponse, type EmitServiceSubmitResponseData, type EmitServiceRouteConfigResponse, } from '../core/clients/emitservice.js';
|
|
37
36
|
export { NonceManager, getOptimizedGasPrice, estimateGasWithSafety, estimateGasBatch, buildTransaction, signTransactionsBatch, signTransactionsBatchMultiWallet, validateSignedTransactions, getDeadline, encodeV3Path, decodeV3Path, getGasLimit, getGasPriceConfig, getTxType, getChainId, type GasPriceConfig, type GasEstimateConfig, type BuildTransactionOptions, type CommonBundleConfig, } from '../utils/bundle-helpers.js';
|
|
@@ -66,7 +66,6 @@ export { parseFlapError, getFlapErrorMessage, getFlapErrorMessageEn, } from '../
|
|
|
66
66
|
export { getFlapMetaByAddress, getFlapMetasByAddresses } from '../domains/flap/meta.js';
|
|
67
67
|
export { createTokenFlow } from '../domains/flows/create.js';
|
|
68
68
|
export { Club48Client, sendBatchPrivateTransactions, sendBackrunBundle, } from '../core/clients/club48.js';
|
|
69
|
-
export { MerkleClient, createMerkleClient, } from '../core/clients/merkle.js';
|
|
70
69
|
export { BlockRazorClient, createBlockRazorClient, BLOCKRAZOR_BUILDER_EOA, } from '../core/clients/blockrazor.js';
|
|
71
70
|
export { EmitServiceClient, submitToEmitServiceBlockRazor, } from '../core/clients/emitservice.js';
|
|
72
71
|
export { NonceManager, getOptimizedGasPrice, estimateGasWithSafety, estimateGasBatch, buildTransaction, signTransactionsBatch, signTransactionsBatchMultiWallet, validateSignedTransactions, getDeadline, encodeV3Path, decodeV3Path, getGasLimit, getGasPriceConfig, getTxType, getChainId, } from '../utils/bundle-helpers.js';
|
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 换手交易通用工具函数
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import type { MerkleClient } from '../core/clients/merkle.js';
|
|
6
|
-
/**
|
|
7
|
-
* 检查并执行代币授权(如果需要)
|
|
8
|
-
* ✅ 与 pancake-proxy.ts 的授权逻辑一致
|
|
9
|
-
*/
|
|
10
|
-
export declare function ensureTokenApproval(provider: JsonRpcProvider, merkle: MerkleClient, wallet: Wallet, tokenAddress: string, spenderAddress: string, config: {
|
|
11
|
-
gasLimit?: number;
|
|
12
|
-
gasLimitMultiplier?: number;
|
|
13
|
-
minGasPriceGwei?: number;
|
|
14
|
-
maxGasPriceGwei?: number;
|
|
15
|
-
txType?: 0 | 2;
|
|
16
|
-
bundleBlockOffset?: number;
|
|
17
|
-
waitTimeoutMs?: number;
|
|
18
|
-
}): Promise<void>;
|
|
4
|
+
import { JsonRpcProvider } from 'ethers';
|
|
19
5
|
/**
|
|
20
6
|
* 获取代币余额
|
|
21
7
|
*/
|
|
@@ -2,88 +2,8 @@
|
|
|
2
2
|
* 换手交易通用工具函数
|
|
3
3
|
*/
|
|
4
4
|
import { ethers, Contract, Interface } from 'ethers';
|
|
5
|
-
import { batchCheckAllowances } from '../core/erc20/index.js';
|
|
6
5
|
import { ERC20_ABI, MULTICALL3_ABI } from '../abis/common.js';
|
|
7
6
|
import { getMulticall3Address } from '../core/constants/index.js';
|
|
8
|
-
import { NonceManager, getOptimizedGasPrice } from './bundle-helpers.js';
|
|
9
|
-
const APPROVAL_THRESHOLD = ethers.MaxUint256 / 2n;
|
|
10
|
-
/**
|
|
11
|
-
* 获取 Gas Limit(授权专用)
|
|
12
|
-
* 优先使用 config.gasLimit,否则使用默认值 * multiplier
|
|
13
|
-
*
|
|
14
|
-
* ✅ ethers v6 最佳实践:直接使用 bigint 类型
|
|
15
|
-
*/
|
|
16
|
-
function getApprovalGasLimit(config, defaultGas = 80000) {
|
|
17
|
-
// 优先使用前端传入的 gasLimit
|
|
18
|
-
if (config.gasLimit !== undefined) {
|
|
19
|
-
// 如果已经是 bigint,直接返回;否则转换
|
|
20
|
-
return typeof config.gasLimit === 'bigint' ? config.gasLimit : BigInt(config.gasLimit);
|
|
21
|
-
}
|
|
22
|
-
// 使用默认值 * multiplier
|
|
23
|
-
const multiplier = config.gasLimitMultiplier ?? 1.0;
|
|
24
|
-
const calculatedGas = Math.ceil(defaultGas * multiplier);
|
|
25
|
-
// JavaScript 原生 BigInt 转换(ethers v6 兼容)
|
|
26
|
-
return BigInt(calculatedGas);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* 检查并执行代币授权(如果需要)
|
|
30
|
-
* ✅ 与 pancake-proxy.ts 的授权逻辑一致
|
|
31
|
-
*/
|
|
32
|
-
export async function ensureTokenApproval(provider, merkle, wallet, tokenAddress, spenderAddress, config) {
|
|
33
|
-
// 使用Multicall3批量检查(虽然只有一个地址,但保持接口一致)
|
|
34
|
-
const allowances = await batchCheckAllowances(provider, tokenAddress, [wallet.address], spenderAddress);
|
|
35
|
-
const currentAllowance = allowances[0];
|
|
36
|
-
if (currentAllowance >= APPROVAL_THRESHOLD) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
// ✅ 使用 NonceManager(与 pancake-proxy.ts 一致)
|
|
40
|
-
const nonceManager = new NonceManager(provider);
|
|
41
|
-
// 构建授权交易
|
|
42
|
-
const tokenContract = new Contract(tokenAddress, ERC20_ABI, wallet);
|
|
43
|
-
const approveUnsigned = await tokenContract.approve.populateTransaction(spenderAddress, ethers.MaxUint256);
|
|
44
|
-
// ✅ 使用 getApprovalGasLimit 函数处理 Gas Limit(与 pancake-proxy.ts 一致)
|
|
45
|
-
const approveGasLimit = getApprovalGasLimit(config, 80000);
|
|
46
|
-
// ✅ 使用 getOptimizedGasPrice 处理 Gas Price(与 pancake-proxy.ts 一致)
|
|
47
|
-
const gasPriceConfig = {};
|
|
48
|
-
if (config.minGasPriceGwei !== undefined) {
|
|
49
|
-
gasPriceConfig.baseGasPrice = ethers.parseUnits(String(config.minGasPriceGwei), 'gwei');
|
|
50
|
-
gasPriceConfig.multiplierPercent = 0;
|
|
51
|
-
}
|
|
52
|
-
if (config.maxGasPriceGwei !== undefined) {
|
|
53
|
-
gasPriceConfig.maxGasPrice = ethers.parseUnits(String(config.maxGasPriceGwei), 'gwei');
|
|
54
|
-
}
|
|
55
|
-
const gasPrice = await getOptimizedGasPrice(provider, gasPriceConfig);
|
|
56
|
-
// ✅ 使用 NonceManager 获取 nonce
|
|
57
|
-
const nonce = await nonceManager.getNextNonce(wallet);
|
|
58
|
-
// ✅ 获取 chainId(不硬编码)
|
|
59
|
-
const network = await provider.getNetwork();
|
|
60
|
-
const chainId = Number(network.chainId);
|
|
61
|
-
// 签名
|
|
62
|
-
const signedTx = await wallet.signTransaction({
|
|
63
|
-
...approveUnsigned,
|
|
64
|
-
from: wallet.address,
|
|
65
|
-
nonce,
|
|
66
|
-
gasLimit: approveGasLimit,
|
|
67
|
-
gasPrice,
|
|
68
|
-
chainId,
|
|
69
|
-
type: config.txType ?? 0,
|
|
70
|
-
});
|
|
71
|
-
// 清理临时 nonce 缓存
|
|
72
|
-
nonceManager.clearTemp();
|
|
73
|
-
// ✅ 提交 Bundle(使用 blockOffset)
|
|
74
|
-
const blockOffset = config.bundleBlockOffset ?? 5;
|
|
75
|
-
const bundleResult = await merkle.sendBundle({
|
|
76
|
-
transactions: [signedTx],
|
|
77
|
-
blockOffset,
|
|
78
|
-
minTimestamp: 0,
|
|
79
|
-
maxTimestamp: 0,
|
|
80
|
-
});
|
|
81
|
-
// 等待确认
|
|
82
|
-
const results = await merkle.waitForBundleConfirmation(bundleResult.txHashes, 1, config.waitTimeoutMs ?? 120000);
|
|
83
|
-
if (!results[0]?.success) {
|
|
84
|
-
throw new Error(`授权失败: ${results[0]?.error || '未知错误'}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
7
|
async function readDecimalsAndBalance(provider, tokenAddress, walletAddress) {
|
|
88
8
|
try {
|
|
89
9
|
const chainId = Number((await provider.getNetwork()).chainId);
|