four-flap-meme-sdk 4.5.0 → 4.5.2

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.
@@ -7,6 +7,7 @@ import { CHAINS, getMulticall3Address } from '../../../core/constants/index.js';
7
7
  import { buildCreateTokenReq } from '../../../core/clients/four.js';
8
8
  import { computeFourCreateValueWei, isNativeRaisedToken, } from '../../../domains/four/raised-token.js';
9
9
  import { PLATFORM_CREATE_FEE } from './constants.js';
10
+ import { withGasHeadroom } from '../../../core/gas/bundle-gas.js';
10
11
  export async function resolveCreateRaisedToken(fourClient, tokenInfo) {
11
12
  if (tokenInfo.raisedToken)
12
13
  return tokenInfo.raisedToken;
@@ -67,7 +68,7 @@ export async function signQuoteApproveIfNeeded(params) {
67
68
  try {
68
69
  const estimated = await provider.estimateGas({ ...unsigned, from: params.wallet.address });
69
70
  if (estimated > 0n)
70
- gasLimit = estimated;
71
+ gasLimit = withGasHeadroom(estimated);
71
72
  }
72
73
  catch {
73
74
  /* 估算失败用 15 万,避免代理报价币 8 万不够 */
@@ -2,7 +2,7 @@
2
2
  * Four.meme Bundle 核心方法 — 批量卖出流程
3
3
  */
4
4
  import { ethers, Contract, Interface } from 'ethers';
5
- import { getGasLimit } from '../../../core/gas/bundle-gas.js';
5
+ import { getGasLimit, withGasHeadroom } from '../../../core/gas/bundle-gas.js';
6
6
  import { NonceManager, getOptimizedGasPrice, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
7
7
  import { ADDRESSES, CHAINS, applySlippageBps, reverseSlippageBps } from '../../../core/constants/index.js';
8
8
  import { MULTICALL3_ABI } from '../../../abis/common.js';
@@ -98,7 +98,7 @@ async function executeSellFlow(options) {
98
98
  from: wallets[0].address,
99
99
  });
100
100
  if (estimated > 0n)
101
- sellGasLimit = estimated;
101
+ sellGasLimit = withGasHeadroom(estimated);
102
102
  }
103
103
  catch {
104
104
  /* 估算失败继续用配置上限 */
@@ -5,6 +5,7 @@ import { Contract, parseEther, parseUnits } from 'ethers';
5
5
  import { ADDRESSES, ZERO_ADDRESS } from '../../../core/constants/index.js';
6
6
  import { ERC20_ABI } from '../../../abis/common.js';
7
7
  import { batchCheckAllowances } from '../../../core/erc20/index.js';
8
+ import { withGasHeadroom } from '../../../core/gas/bundle-gas.js';
8
9
  const HELPER_TRY_BUY_ABI = [
9
10
  'function tryBuy(address token, uint256 amount, uint256 funds) view returns (address tokenManager, address quote, uint256 estimatedAmount, uint256 estimatedCost, uint256 estimatedFee, uint256 amountMsgValue, uint256 amountApproval, uint256 amountFunds)',
10
11
  ];
@@ -60,7 +61,7 @@ export async function signQuoteApproveTxs(params) {
60
61
  try {
61
62
  const estimated = await provider.estimateGas({ ...unsigned, from: wallets[i].address });
62
63
  if (estimated > 0n)
63
- gasLimit = estimated;
64
+ gasLimit = withGasHeadroom(estimated);
64
65
  }
65
66
  catch {
66
67
  /* 估算失败用 15 万 */
@@ -9,6 +9,7 @@ import { NonceManager, getOptimizedGasPrice, getGasPriceConfig, buildProfitHopTr
9
9
  import { getBribeAmount, getProfitRecipient } from '../../config-helpers.js';
10
10
  import { calculateBundleSwapRoundTripProfit } from '../../../core/constants/index.js';
11
11
  import { TM_ABI, TM_ADDRESS } from '../swap-internal.js';
12
+ import { withGasHeadroom } from '../../../core/gas/bundle-gas.js';
12
13
  /**
13
14
  * Four内盘捆绑换手
14
15
  */
@@ -82,9 +83,9 @@ export async function fourBundleSwapMerkle(params) {
82
83
  provider.estimateGas({ ...buyUnsigned, from: buyer.address, value: buyerFunds }),
83
84
  ]);
84
85
  if (sellGas > 0n)
85
- sellGasLimit = sellGas;
86
+ sellGasLimit = withGasHeadroom(sellGas);
86
87
  if (buyGas > 0n)
87
- buyGasLimit = buyGas;
88
+ buyGasLimit = withGasHeadroom(buyGas);
88
89
  }
89
90
  catch {
90
91
  /* 估算失败继续用配置上限 */
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { JsonRpcProvider, Transaction, Wallet } from 'ethers';
5
5
  import { ROBINHOOD_CHAIN_ID, ROBINHOOD_RPC_URL } from './constants.js';
6
+ import { withGasHeadroom } from '../../core/gas/bundle-gas.js';
6
7
  export async function signRobinhoodTx(params) {
7
8
  const rpcUrl = params.rpcUrl ?? ROBINHOOD_RPC_URL;
8
9
  const provider = new JsonRpcProvider(rpcUrl, ROBINHOOD_CHAIN_ID, { staticNetwork: true });
@@ -25,8 +26,10 @@ export async function signRobinhoodTx(params) {
25
26
  data: params.data,
26
27
  value: params.value ?? 0n,
27
28
  });
28
- if (estimated > 0n)
29
- gasLimit = estimated;
29
+ if (estimated > 0n) {
30
+ const isCall = params.data != null && params.data !== '0x';
31
+ gasLimit = isCall ? withGasHeadroom(estimated) : estimated;
32
+ }
30
33
  }
31
34
  catch {
32
35
  /* 节点没给出上限时用服务端配置 */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { estimateApproveGasLimit, estimateGasWithSafety, withGasHeadroom } from './bundle-gas.js';
3
+ describe('gas headroom', () => {
4
+ it('估算结果默认留 20% 余量(捆绑后面的交易状态会变,刚好够会 OutOfGas)', async () => {
5
+ expect(withGasHeadroom(273241n)).toBe(327889n);
6
+ expect(withGasHeadroom(0n)).toBe(0n);
7
+ const provider = { estimateGas: async () => 100000n };
8
+ expect(await estimateApproveGasLimit(provider, '0x1', '0x2', '0x')).toBe(120000n);
9
+ expect(await estimateGasWithSafety(provider, {})).toBe(120000n);
10
+ expect(await estimateGasWithSafety(provider, {}, { multiplier: 1 })).toBe(100000n);
11
+ });
12
+ });
@@ -11,6 +11,13 @@ export interface GasEstimateConfig {
11
11
  fallbackGasLimit?: bigint;
12
12
  allowFailure?: boolean;
13
13
  }
14
+ /**
15
+ * eth_estimateGas 按 latest 状态给「刚好够」的值。捆绑里排在后面的交易执行时,前面的买卖已经改了
16
+ * 池子 / 税币状态,实际用量会更高;刚好够的上限会 OutOfGas,BlockRazor 整包判成
17
+ * non-reverting tx in bundle failed。所有估算都加 20% 余量(4.4.3 去掉后大量捆绑失败)。
18
+ */
19
+ export declare const GAS_HEADROOM_PERCENT = 120n;
20
+ export declare function withGasHeadroom(gas: bigint): bigint;
14
21
  export declare function estimateGasWithSafety(provider: JsonRpcProvider, txRequest: TransactionRequest, config?: GasEstimateConfig): Promise<bigint>;
15
22
  export declare function estimateGasBatch(provider: JsonRpcProvider, txRequests: TransactionRequest[], config?: GasEstimateConfig): Promise<bigint[]>;
16
23
  export interface BuildTransactionOptions {
@@ -22,8 +22,19 @@ export async function getOptimizedGasPrice(provider, config) {
22
22
  }
23
23
  return gasPrice;
24
24
  }
25
+ /**
26
+ * eth_estimateGas 按 latest 状态给「刚好够」的值。捆绑里排在后面的交易执行时,前面的买卖已经改了
27
+ * 池子 / 税币状态,实际用量会更高;刚好够的上限会 OutOfGas,BlockRazor 整包判成
28
+ * non-reverting tx in bundle failed。所有估算都加 20% 余量(4.4.3 去掉后大量捆绑失败)。
29
+ */
30
+ export const GAS_HEADROOM_PERCENT = 120n;
31
+ export function withGasHeadroom(gas) {
32
+ if (gas <= 0n)
33
+ return gas;
34
+ return (gas * GAS_HEADROOM_PERCENT) / 100n;
35
+ }
25
36
  export async function estimateGasWithSafety(provider, txRequest, config) {
26
- const multiplier = config?.multiplier ?? 1;
37
+ const multiplier = config?.multiplier ?? 1.2;
27
38
  const fallback = config?.fallbackGasLimit ?? 600000n;
28
39
  const allowFailure = config?.allowFailure ?? true;
29
40
  try {
@@ -96,7 +107,7 @@ export async function estimateApproveGasLimit(provider, from, to, data, fallback
96
107
  try {
97
108
  const estimated = await provider.estimateGas({ from, to, data });
98
109
  if (estimated > 0n)
99
- return estimated;
110
+ return withGasHeadroom(estimated);
100
111
  }
101
112
  catch {
102
113
  /* 节点没给出上限 */
@@ -3,6 +3,7 @@
3
3
  * 模块加载时 patch Wallet.prototype.signTransaction(全局一次)
4
4
  */
5
5
  import { Wallet } from 'ethers';
6
+ import { withGasHeadroom } from '../gas/bundle-gas.js';
6
7
  function chainGasPrice(fee) {
7
8
  const candidates = [fee.maxFeePerGas, fee.gasPrice].filter((v) => v != null && v > 0n);
8
9
  if (candidates.length === 0)
@@ -63,8 +64,10 @@ Wallet.prototype.signTransaction = async function (tx) {
63
64
  data: signable.data,
64
65
  value: signable.value,
65
66
  });
66
- if (estimated > 0n)
67
- signable = { ...signable, gasLimit: estimated };
67
+ if (estimated > 0n) {
68
+ const isCall = signable.data != null && signable.data !== '0x';
69
+ signable = { ...signable, gasLimit: isCall ? withGasHeadroom(estimated) : estimated };
70
+ }
68
71
  }
69
72
  catch {
70
73
  /* 节点没给出 gas 上限时保留原来的上限,避免发不出去 */
@@ -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
+ }
@@ -15,3 +15,4 @@ export * from './quote.js';
15
15
  export * from './profit.js';
16
16
  export * from './router.js';
17
17
  export * from './dyo-encode.js';
18
+ export * from './direct-router-approvals.js';
@@ -15,3 +15,4 @@ export * from './quote.js';
15
15
  export * from './profit.js';
16
16
  export * from './router.js';
17
17
  export * from './dyo-encode.js';
18
+ export * from './direct-router-approvals.js';
@@ -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 [nonces, gasPrice, nativeProfitWei] = await Promise.all([
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,
@@ -38,7 +38,7 @@ export function getErrorMessage(key, ...args) {
38
38
  */
39
39
  export async function estimateGasWithMultiplier(provider, txRequest, multiplier) {
40
40
  const estimated = await provider.estimateGas(txRequest);
41
- const mult = multiplier ?? 1;
41
+ const mult = multiplier ?? 1.2; // 默认 20% 安全余量(捆绑后面的交易状态会变)
42
42
  return BigInt(Math.ceil(Number(estimated) * mult));
43
43
  }
44
44
  /**
@@ -41,7 +41,7 @@ export async function waitForBundleWithProvider(provider, firstTxSigned, _bundle
41
41
  */
42
42
  export async function estimateGasWithMultiplier(provider, txRequest, multiplier) {
43
43
  const estimated = await provider.estimateGas(txRequest);
44
- const mult = multiplier ?? 1;
44
+ const mult = multiplier ?? 1.2; // 默认 20% 安全余量(捆绑后面的交易状态会变)
45
45
  return BigInt(Math.ceil(Number(estimated) * mult));
46
46
  }
47
47
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",