four-flap-meme-sdk 4.4.3 → 4.5.1
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-approvals.d.ts +28 -0
- package/dist/domains/dex/direct-router-approvals.js +45 -0
- package/dist/domains/dex/direct-router-helpers.d.ts +1 -0
- package/dist/domains/dex/direct-router-helpers.js +1 -0
- package/dist/domains/dex/direct-router-v2-buy.js +13 -0
- package/dist/domains/dex/direct-router-v3.js +43 -4
- 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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2/V3 直连 Router 卖出/买入前的自动授权。
|
|
3
|
+
*
|
|
4
|
+
* 与 V2 路径保持一致:钱包对 Router 的 allowance 不足时,在该钱包的起始 nonce 上
|
|
5
|
+
* 插入一笔 `approve(spender, MaxUint256)`,并按顺序把后续交易 nonce 顺延。
|
|
6
|
+
*
|
|
7
|
+
* 背景:BlockRazor 的 bundle 要求包内交易都不能回滚。漏掉授权时,卖出的
|
|
8
|
+
* `TransferHelper.safeTransferFrom` 会直接 STF 回滚,整包被拒(表现为
|
|
9
|
+
* `non-reverting tx in bundle failed`),而且不会指出是哪一笔。
|
|
10
|
+
*/
|
|
11
|
+
import { type JsonRpcProvider, type Wallet } from 'ethers';
|
|
12
|
+
export interface SignMissingAllowanceApprovalsParams {
|
|
13
|
+
provider: JsonRpcProvider;
|
|
14
|
+
wallets: Wallet[];
|
|
15
|
+
/** 会被原地修改:插入 approve 的钱包 nonce +1,后续交易自动顺延 */
|
|
16
|
+
nonces: number[];
|
|
17
|
+
tokenAddress: string;
|
|
18
|
+
spender: string;
|
|
19
|
+
/** 与 wallets 等长;<= 0 表示该钱包本轮没有交易,不授权 */
|
|
20
|
+
amounts: bigint[];
|
|
21
|
+
/** 与 wallets 等长;RPC 失败时传 MaxUint256 可跳过授权 */
|
|
22
|
+
allowances: bigint[];
|
|
23
|
+
txType: number;
|
|
24
|
+
gasPrice: bigint;
|
|
25
|
+
chainId: number;
|
|
26
|
+
}
|
|
27
|
+
/** 返回按钱包顺序排好的 approve 原始交易(未授权的钱包不产生交易) */
|
|
28
|
+
export declare function signMissingAllowanceApprovals(params: SignMissingAllowanceApprovalsParams): Promise<string[]>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2/V3 直连 Router 卖出/买入前的自动授权。
|
|
3
|
+
*
|
|
4
|
+
* 与 V2 路径保持一致:钱包对 Router 的 allowance 不足时,在该钱包的起始 nonce 上
|
|
5
|
+
* 插入一笔 `approve(spender, MaxUint256)`,并按顺序把后续交易 nonce 顺延。
|
|
6
|
+
*
|
|
7
|
+
* 背景:BlockRazor 的 bundle 要求包内交易都不能回滚。漏掉授权时,卖出的
|
|
8
|
+
* `TransferHelper.safeTransferFrom` 会直接 STF 回滚,整包被拒(表现为
|
|
9
|
+
* `non-reverting tx in bundle failed`),而且不会指出是哪一笔。
|
|
10
|
+
*/
|
|
11
|
+
import { MaxUint256, ethers } from 'ethers';
|
|
12
|
+
import { estimateApproveGasLimit } from '../../core/gas/bundle-gas.js';
|
|
13
|
+
import { ERC20_ABI } from '../../abis/common.js';
|
|
14
|
+
import { buildGasFields } from '../../core/gas/bundle-gas.js';
|
|
15
|
+
/** 返回按钱包顺序排好的 approve 原始交易(未授权的钱包不产生交易) */
|
|
16
|
+
export async function signMissingAllowanceApprovals(params) {
|
|
17
|
+
const { provider, wallets, nonces, tokenAddress, spender, amounts, allowances, txType, gasPrice, chainId } = params;
|
|
18
|
+
if (!ethers.isAddress(tokenAddress) || !ethers.isAddress(spender))
|
|
19
|
+
return [];
|
|
20
|
+
const tokenIface = new ethers.Interface(ERC20_ABI);
|
|
21
|
+
const approveData = tokenIface.encodeFunctionData('approve', [spender, MaxUint256]);
|
|
22
|
+
const txs = [];
|
|
23
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
24
|
+
const amount = amounts[i] ?? 0n;
|
|
25
|
+
if (amount <= 0n)
|
|
26
|
+
continue;
|
|
27
|
+
const allowance = allowances[i];
|
|
28
|
+
if (allowance === undefined || allowance >= amount)
|
|
29
|
+
continue;
|
|
30
|
+
const wallet = wallets[i];
|
|
31
|
+
const gasLimit = await estimateApproveGasLimit(provider, wallet.address, tokenAddress, approveData);
|
|
32
|
+
const tx = await wallet.signTransaction({
|
|
33
|
+
to: tokenAddress,
|
|
34
|
+
data: approveData,
|
|
35
|
+
value: 0n,
|
|
36
|
+
nonce: nonces[i],
|
|
37
|
+
gasLimit,
|
|
38
|
+
...buildGasFields(txType, gasPrice),
|
|
39
|
+
chainId,
|
|
40
|
+
});
|
|
41
|
+
txs.push(tx);
|
|
42
|
+
nonces[i] += 1;
|
|
43
|
+
}
|
|
44
|
+
return txs;
|
|
45
|
+
}
|
|
@@ -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;
|
|
@@ -7,6 +7,8 @@ import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFie
|
|
|
7
7
|
import { getProfitRecipient, CHAINS, GAS_LIMITS } from '../../core/constants/index.js';
|
|
8
8
|
import { getErrorMessageFromUnknown } from '../../core/index.js';
|
|
9
9
|
import { batchGetBalances } from '../../bundle/internal.js';
|
|
10
|
+
import { batchCheckAllowances } from '../../core/erc20/allowances.js';
|
|
11
|
+
import { signMissingAllowanceApprovals } from './direct-router-approvals.js';
|
|
10
12
|
import { getTokenToNativeQuote } from '../../core/quote/quote-helpers.js';
|
|
11
13
|
import { CHAIN_IDS, V3_ROUTER02_ABI, V3_ROUTER_LEGACY_ABI, buildBribeTransaction, buildProfitTransactionWithHops, calculateProfitAmount, findMaxFlowIndex, getBribeAmount, getCachedProvider, getDeadline, getGasPrice, getWrappedNative, isLegacySwapRouter, isNativeToken, quoteXLayerV3TokenToNativeBySlot0, } from './direct-router-helpers.js';
|
|
12
14
|
// ============================================================================
|
|
@@ -37,7 +39,8 @@ export async function directV3BatchBuy(params) {
|
|
|
37
39
|
const totalFlowWei = flowAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
38
40
|
const baseProfitWei = calculateProfitAmount(totalFlowWei);
|
|
39
41
|
// ✅ 方案 B:并行获取 nonces、gasPrice 和 ERC20 报价
|
|
40
|
-
const
|
|
42
|
+
const quoteToApprove = useNative ? '' : String(quoteToken || '');
|
|
43
|
+
const [nonces, gasPrice, nativeProfitWei, allowances] = await Promise.all([
|
|
41
44
|
startNonces && startNonces.length === wallets.length
|
|
42
45
|
? Promise.resolve(startNonces)
|
|
43
46
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
@@ -48,6 +51,10 @@ export async function directV3BatchBuy(params) {
|
|
|
48
51
|
? quoteXLayerV3TokenToNativeBySlot0({ provider, tokenIn: quoteToken, amountIn: baseProfitWei, fee })
|
|
49
52
|
: getTokenToNativeQuote(provider, quoteToken, baseProfitWei, chain, 'v3', fee)
|
|
50
53
|
: Promise.resolve(baseProfitWei),
|
|
54
|
+
// ERC-20 计价币买入要先把计价币授权给 Router,ERC-20 结算不会自动带 approve
|
|
55
|
+
quoteToApprove && ethers.isAddress(quoteToApprove) && ethers.isAddress(String(routerAddress || ''))
|
|
56
|
+
? batchCheckAllowances(provider, quoteToApprove, wallets.map((w) => w.address), routerAddress).catch(() => wallets.map(() => ethers.MaxUint256))
|
|
57
|
+
: Promise.resolve(wallets.map(() => ethers.MaxUint256)),
|
|
51
58
|
]);
|
|
52
59
|
let profitWei = nativeProfitWei > 0n ? nativeProfitWei : 0n;
|
|
53
60
|
if (profitWei > 0n && chain.toUpperCase() === 'ENI') {
|
|
@@ -55,6 +62,21 @@ export async function directV3BatchBuy(params) {
|
|
|
55
62
|
}
|
|
56
63
|
const gasLimit = getGasLimit(config, 300000);
|
|
57
64
|
const txType = config.txType ?? 0;
|
|
65
|
+
// 计价币 allowance 不足时同包补 approve;nonces[i] 会被顺延
|
|
66
|
+
const approveTxs = quoteToApprove
|
|
67
|
+
? await signMissingAllowanceApprovals({
|
|
68
|
+
provider,
|
|
69
|
+
wallets,
|
|
70
|
+
nonces,
|
|
71
|
+
tokenAddress: quoteToApprove,
|
|
72
|
+
spender: String(routerAddress || ''),
|
|
73
|
+
amounts: flowAmounts,
|
|
74
|
+
allowances,
|
|
75
|
+
txType,
|
|
76
|
+
gasPrice,
|
|
77
|
+
chainId,
|
|
78
|
+
})
|
|
79
|
+
: [];
|
|
58
80
|
const routerIface = new ethers.Interface(routerAbi);
|
|
59
81
|
const inputToken = useNative ? wrappedNative : quoteToken;
|
|
60
82
|
const deadline = getDeadline();
|
|
@@ -184,7 +206,7 @@ export async function directV3BatchBuy(params) {
|
|
|
184
206
|
profitHopWallets = profitResult.hopWallets;
|
|
185
207
|
}
|
|
186
208
|
}
|
|
187
|
-
const signedTxs = [...bribeTxs, ...swapTxs, ...profitTxs];
|
|
209
|
+
const signedTxs = [...approveTxs, ...bribeTxs, ...swapTxs, ...profitTxs];
|
|
188
210
|
return {
|
|
189
211
|
signedTransactions: signedTxs,
|
|
190
212
|
profitHopWallets,
|
|
@@ -214,12 +236,16 @@ export async function directV3BatchSell(params) {
|
|
|
214
236
|
const routerAbi = useLegacyRouter ? V3_ROUTER_LEGACY_ABI : V3_ROUTER02_ABI;
|
|
215
237
|
const wallets = privateKeys.map((pk) => new Wallet(pk, provider));
|
|
216
238
|
// ✅ 并行获取所有 RPC 数据
|
|
217
|
-
const [balances, nonces, gasPrice] = await Promise.all([
|
|
239
|
+
const [balances, nonces, gasPrice, allowances] = await Promise.all([
|
|
218
240
|
batchGetBalances(provider, wallets.map((w) => w.address), tokenAddress, chainId),
|
|
219
241
|
startNonces && startNonces.length === wallets.length
|
|
220
242
|
? Promise.resolve(startNonces)
|
|
221
243
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
222
244
|
getGasPrice(provider, config),
|
|
245
|
+
// 卖出要先把代币授权给 Router,否则 TransferHelper 直接 STF 回滚、整包被拒
|
|
246
|
+
ethers.isAddress(String(tokenAddress || '')) && ethers.isAddress(String(routerAddress || ''))
|
|
247
|
+
? batchCheckAllowances(provider, tokenAddress, wallets.map((w) => w.address), routerAddress).catch(() => wallets.map(() => ethers.MaxUint256))
|
|
248
|
+
: Promise.resolve(wallets.map(() => ethers.MaxUint256)),
|
|
223
249
|
]);
|
|
224
250
|
const gasLimit = getGasLimit(config, 350000);
|
|
225
251
|
const txType = config.txType ?? 0;
|
|
@@ -238,6 +264,19 @@ export async function directV3BatchSell(params) {
|
|
|
238
264
|
}
|
|
239
265
|
}
|
|
240
266
|
const totalSellAmount = sellAmountsWei.reduce((sum, o) => sum + o, 0n);
|
|
267
|
+
// 代币 allowance 不足时同包补 approve;nonces[i] 会被顺延
|
|
268
|
+
const approveTxs = await signMissingAllowanceApprovals({
|
|
269
|
+
provider,
|
|
270
|
+
wallets,
|
|
271
|
+
nonces,
|
|
272
|
+
tokenAddress: String(tokenAddress || ''),
|
|
273
|
+
spender: String(routerAddress || ''),
|
|
274
|
+
amounts: sellAmountsWei,
|
|
275
|
+
allowances,
|
|
276
|
+
txType,
|
|
277
|
+
gasPrice,
|
|
278
|
+
chainId,
|
|
279
|
+
});
|
|
241
280
|
// ✅ 修复:对于卖出操作,利润应基于【得到的 BNB/原生代币】而不是【卖出的代币数量】
|
|
242
281
|
// 使用 V2 Router 获取卖出报价,然后基于预估得到的原生代币数量计算利润
|
|
243
282
|
const nativeProfitPromise = (async () => {
|
|
@@ -417,7 +456,7 @@ export async function directV3BatchSell(params) {
|
|
|
417
456
|
.filter((r) => r.type === 'swap')
|
|
418
457
|
.sort((a, b) => a.index - b.index)
|
|
419
458
|
.map((r) => r.tx);
|
|
420
|
-
const signedTxs = [...bribeTxs, ...swapTxs, ...profitTxs];
|
|
459
|
+
const signedTxs = [...approveTxs, ...bribeTxs, ...swapTxs, ...profitTxs];
|
|
421
460
|
return {
|
|
422
461
|
signedTransactions: signedTxs,
|
|
423
462
|
profitHopWallets,
|
|
@@ -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
|
*/
|