four-flap-meme-sdk 4.3.14 → 4.3.15

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.
@@ -11,7 +11,7 @@ import { getTxType, getGasPriceConfig, getBribeAmount, calculateProfit, getProfi
11
11
  import { signBribeTransaction } from '../../../core/sign-context-helpers.js';
12
12
  import { createChainContext, loginFourClient, resolveTokenImage } from '../core-helpers.js';
13
13
  import { OPENFOUR_CORE_ABI } from '../../../abis/openfour.js';
14
- import { computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, loadOpenFourAddresses, } from '../../../domains/openfour/index.js';
14
+ import { OPENFOUR_CREATE_EXPIRED_MESSAGE, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, loadOpenFourAddresses, shouldRefreshOpenFourCreateArg, } from '../../../domains/openfour/index.js';
15
15
  import { signQuoteApproveIfNeeded } from './create-token-quote.js';
16
16
  const BSC_SUBMIT_FEE_DUST_WEI = 10n ** 12n;
17
17
  export function openFourSubmitProfitWei(nativeFlowWei) {
@@ -32,6 +32,23 @@ function signedNativeFlowWei(signedTxs) {
32
32
  }
33
33
  }, 0n);
34
34
  }
35
+ async function readChainTimestamp(provider) {
36
+ const block = await provider.getBlock('latest');
37
+ return BigInt(block?.timestamp ?? Math.floor(Date.now() / 1000));
38
+ }
39
+ async function fetchFreshOpenFourCreateArg(fourClient, accessToken, body, provider) {
40
+ const pull = () => fourClient.createOpenFourToken(accessToken, body);
41
+ let createResp = await pull();
42
+ let decoded = decodeOpenFourCreateArg(createResp.createArg);
43
+ if (shouldRefreshOpenFourCreateArg(decoded.validTimestamp, await readChainTimestamp(provider))) {
44
+ createResp = await pull();
45
+ decoded = decodeOpenFourCreateArg(createResp.createArg);
46
+ }
47
+ if (shouldRefreshOpenFourCreateArg(decoded.validTimestamp, await readChainTimestamp(provider))) {
48
+ throw new Error(OPENFOUR_CREATE_EXPIRED_MESSAGE);
49
+ }
50
+ return createResp;
51
+ }
35
52
  function toRaisedToken(cfg) {
36
53
  return {
37
54
  symbol: cfg.symbol,
@@ -84,8 +101,8 @@ export async function createOpenFourTokenMerkle(params) {
84
101
  totalSupply: cfg.totalSupply,
85
102
  raisedAmount: cfg.raisedAmount,
86
103
  };
87
- const createResp = await fourClient.createOpenFourToken(accessToken, body);
88
104
  const addrs = await loadOpenFourAddresses(provider);
105
+ const createResp = await fetchFreshOpenFourCreateArg(fourClient, accessToken, body, provider);
89
106
  const decoded = decodeOpenFourCreateArg(createResp.createArg);
90
107
  const valueWei = computeOpenFourCreateValueWei(decoded, addrs.wrappedNative);
91
108
  const gasPrice = await getOptimizedGasPrice(provider, getGasPriceConfig(config));
@@ -138,6 +138,8 @@ export interface PancakeBundleSwapSignParams {
138
138
  quoteToken?: string;
139
139
  quoteTokenDecimals?: number;
140
140
  startNonces?: number[];
141
+ /** 税币基点。卖出按扣税后数量报价,买入 minOut 再扣税。 */
142
+ taxRateBps?: number;
141
143
  }
142
144
  export interface PancakeBundleSwapParams {
143
145
  sellerPrivateKey: string;
@@ -1,6 +1,20 @@
1
1
  import { ethers } from 'ethers';
2
2
  import { applySlippageBps } from '../../../core/constants/index.js';
3
3
  import { type QuoteBNBResult, type QuoteSellParams, type RouteParams, type UserType } from './bundle-swap-types.js';
4
+ /** 路由卖出端的计价代币(path 最后一项 / v3TokenOut)。 */
5
+ export declare function routeQuoteToken(routeParams: RouteParams): string | undefined;
6
+ /**
7
+ * 股票池 / USDT 池 path 不是 WBNB 时,即使调用方漏传 quoteToken 也不能走 ETH 买卖。
8
+ * Pancake swapExactETH* 要求 path[0]==WETH,否则 INVALID_PATH。
9
+ */
10
+ export declare function resolvePancakeQuoteMode(params: {
11
+ quoteToken?: string;
12
+ routeParams: RouteParams;
13
+ wrappedNative: string;
14
+ }): {
15
+ useNativeToken: boolean;
16
+ quoteToken?: string;
17
+ };
4
18
  /** Pancake 买卖 minOut;实现见 core applySlippageBps。 */
5
19
  export declare const applyPancakeSlippage: typeof applySlippageBps;
6
20
  export declare function splitQuotedMins(quotedTotal: bigint, amounts: bigint[], slippageBps?: number): bigint[];
@@ -1,6 +1,36 @@
1
1
  import { ADDRESSES, applySlippageBps, getProfitRateBps as getSharedProfitRateBps, calculateBundleSwapRoundTripProfit } from '../../../core/constants/index.js';
2
2
  import { quoteV2, quoteV3 } from '../../../core/quote/quote-helpers.js';
3
- import { WBNB_ADDRESS, } from './bundle-swap-types.js';
3
+ import { WBNB_ADDRESS, ZERO_ADDRESS, } from './bundle-swap-types.js';
4
+ function normalizeAddr(addr) {
5
+ return String(addr || '').trim().toLowerCase();
6
+ }
7
+ /** 路由卖出端的计价代币(path 最后一项 / v3TokenOut)。 */
8
+ export function routeQuoteToken(routeParams) {
9
+ if (routeParams.routeType === 'v2' && routeParams.v2Path?.length) {
10
+ return routeParams.v2Path[routeParams.v2Path.length - 1];
11
+ }
12
+ if (routeParams.routeType === 'v3-single') {
13
+ return routeParams.v3TokenOut;
14
+ }
15
+ const multiPath = routeParams.v2Path;
16
+ if (multiPath?.length)
17
+ return multiPath[multiPath.length - 1];
18
+ return undefined;
19
+ }
20
+ /**
21
+ * 股票池 / USDT 池 path 不是 WBNB 时,即使调用方漏传 quoteToken 也不能走 ETH 买卖。
22
+ * Pancake swapExactETH* 要求 path[0]==WETH,否则 INVALID_PATH。
23
+ */
24
+ export function resolvePancakeQuoteMode(params) {
25
+ const wrapped = normalizeAddr(params.wrappedNative);
26
+ const explicit = normalizeAddr(params.quoteToken);
27
+ const token = explicit && explicit !== normalizeAddr(ZERO_ADDRESS) ? params.quoteToken.trim() : routeQuoteToken(params.routeParams);
28
+ const tokenNorm = normalizeAddr(token);
29
+ if (!tokenNorm || tokenNorm === normalizeAddr(ZERO_ADDRESS) || tokenNorm === wrapped) {
30
+ return { useNativeToken: true };
31
+ }
32
+ return { useNativeToken: false, quoteToken: token };
33
+ }
4
34
  /** Pancake 买卖 minOut;实现见 core applySlippageBps。 */
5
35
  export const applyPancakeSlippage = applySlippageBps;
6
36
  export function splitQuotedMins(quotedTotal, amounts, slippageBps = 100) {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { reverseBuyRouteParams } from './bundle-buy-first-quote.js';
3
- import { applyPancakeSlippage, applyTaxBps, taxedSellQuoteInput, isSameWalletBuySell, resolveBuyFirstSellMins, sequentialBuyAmountOutMins, sequentialPrefixOutputs, pairSameWalletSellAmounts, splitQuotedMins, } from './quote.js';
3
+ import { applyPancakeSlippage, applyTaxBps, taxedSellQuoteInput, isSameWalletBuySell, resolveBuyFirstSellMins, sequentialBuyAmountOutMins, sequentialPrefixOutputs, pairSameWalletSellAmounts, splitQuotedMins, resolvePancakeQuoteMode, } from './quote.js';
4
4
  describe('applyPancakeSlippage', () => {
5
5
  it('drops 1% by default', () => {
6
6
  expect(applyPancakeSlippage(1000000n)).toBe(990000n);
@@ -36,6 +36,26 @@ describe('taxedSellQuoteInput', () => {
36
36
  expect(taxedSellQuoteInput(10000n, 100)).toBe(9900n);
37
37
  });
38
38
  });
39
+ describe('resolvePancakeQuoteMode', () => {
40
+ const wbnb = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
41
+ const spcxb = '0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1';
42
+ const token = '0x7db2f0c15f74ab0dc0d4f49650dd60c45ffb7777';
43
+ it('treats omitted quoteToken as ERC20 when v2 path ends at a stock quote', () => {
44
+ const mode = resolvePancakeQuoteMode({
45
+ routeParams: { routeType: 'v2', v2Path: [token, spcxb] },
46
+ wrappedNative: wbnb,
47
+ });
48
+ expect(mode.useNativeToken).toBe(false);
49
+ expect(mode.quoteToken?.toLowerCase()).toBe(spcxb.toLowerCase());
50
+ });
51
+ it('keeps native mode when v2 path ends at wrapped BNB', () => {
52
+ const mode = resolvePancakeQuoteMode({
53
+ routeParams: { routeType: 'v2', v2Path: [token, wbnb] },
54
+ wrappedNative: wbnb,
55
+ });
56
+ expect(mode.useNativeToken).toBe(true);
57
+ });
58
+ });
39
59
  describe('pairSameWalletSellAmounts', () => {
40
60
  it('pairs each wash wallet sell to that wallet guaranteed buy out', () => {
41
61
  expect(pairSameWalletSellAmounts(true, [900n, 700n], 2)).toEqual([900n, 700n]);
@@ -16,6 +16,7 @@ export interface PancakeBatchSwapSignParams {
16
16
  quoteToken?: string;
17
17
  quoteTokenDecimals?: number;
18
18
  startNonces?: number[];
19
+ taxRateBps?: number;
19
20
  }
20
21
  /**
21
22
  * 批量换手结果
@@ -6,7 +6,7 @@ import { getGasLimit } from '../../../core/gas/bundle-gas.js';
6
6
  import { ethers, Contract, Wallet } from 'ethers';
7
7
  import { calculateSellAmount } from '../../../utils/swap-helpers.js';
8
8
  import { NonceManager, getDeadline, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
9
- import { BRIBE_GAS_LIMIT, ERC20_BALANCE_OF_ABI, FLAT_FEE, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, ZERO_ADDRESS, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, quoteBuyOutput, applyPancakeSlippage, } from './bundle-swap-helpers.js';
9
+ import { BRIBE_GAS_LIMIT, ERC20_BALANCE_OF_ABI, FLAT_FEE, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, quoteBuyOutput, applyPancakeSlippage, resolvePancakeQuoteMode, taxedSellQuoteInput, sequentialBuyAmountOutMins, } from './bundle-swap-helpers.js';
10
10
  /**
11
11
  * PancakeSwap 批量换手(一卖多买)
12
12
  *
@@ -17,7 +17,7 @@ import { BRIBE_GAS_LIMIT, ERC20_BALANCE_OF_ABI, FLAT_FEE, PANCAKE_V2_ROUTER_ABI,
17
17
  */
18
18
  export async function pancakeBatchSwapMerkle(params) {
19
19
  const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerAmounts, tokenAddress, routeParams, config, quoteToken, quoteTokenDecimals = 18, startNonces, // ✅ 可选:前端预获取的 nonces
20
- } = params;
20
+ taxRateBps = 0, } = params;
21
21
  // ✅ 校验买方数量(最多 24 个)
22
22
  const MAX_BUYERS = 24;
23
23
  if (buyerPrivateKeys.length === 0) {
@@ -26,7 +26,13 @@ export async function pancakeBatchSwapMerkle(params) {
26
26
  if (buyerPrivateKeys.length > MAX_BUYERS) {
27
27
  throw new Error(`买方钱包数量超过限制: ${buyerPrivateKeys.length} > ${MAX_BUYERS}`);
28
28
  }
29
- const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
29
+ const quoteMode = resolvePancakeQuoteMode({
30
+ quoteToken,
31
+ routeParams,
32
+ wrappedNative: WBNB_ADDRESS,
33
+ });
34
+ const useNativeToken = quoteMode.useNativeToken;
35
+ const effectiveQuoteToken = quoteMode.quoteToken;
30
36
  const context = createPancakeContext(config);
31
37
  const seller = new Wallet(sellerPrivateKey, context.provider);
32
38
  const buyers = buyerPrivateKeys.map((pk) => new Wallet(pk, context.provider));
@@ -42,7 +48,7 @@ export async function pancakeBatchSwapMerkle(params) {
42
48
  const { amount: sellAmountWei, decimals } = sellAmountResult;
43
49
  const quoteResult = await quoteSellOutput({
44
50
  routeParams,
45
- sellAmountWei,
51
+ sellAmountWei: taxedSellQuoteInput(sellAmountWei, taxRateBps),
46
52
  provider: context.provider,
47
53
  v2RouterAddress: config.v2RouterAddress,
48
54
  });
@@ -70,7 +76,7 @@ export async function pancakeBatchSwapMerkle(params) {
70
76
  }
71
77
  // ✅ 并行验证所有买方余额
72
78
  const reserveGas = ethers.parseEther((config.reserveGasBNB ?? 0.0005).toString());
73
- const erc20Contract = useNativeToken ? null : new Contract(quoteToken, ERC20_BALANCE_OF_ABI, context.provider);
79
+ const erc20Contract = useNativeToken ? null : new Contract(effectiveQuoteToken, ERC20_BALANCE_OF_ABI, context.provider);
74
80
  await Promise.all(buyers.map(async (buyer, i) => {
75
81
  const buyAmount = buyAmountsWei[i];
76
82
  if (useNativeToken) {
@@ -96,7 +102,7 @@ export async function pancakeBatchSwapMerkle(params) {
96
102
  provider: context.provider,
97
103
  v2RouterAddress: config.v2RouterAddress,
98
104
  })));
99
- const buyMinOuts = buyQuotedOuts.map((quoted) => applyPancakeSlippage(quoted, slippageBps));
105
+ const buyMinOuts = sequentialBuyAmountOutMins(buyQuotedOuts, taxRateBps, slippageBps);
100
106
  const deadline = getDeadline();
101
107
  const v3RouterIface = new ethers.Interface(PANCAKE_V3_ROUTER_ABI);
102
108
  const batchV2Addr = config.v2RouterAddress || PANCAKE_V2_ROUTER_ADDRESS;
@@ -219,7 +225,7 @@ export async function pancakeBatchSwapMerkle(params) {
219
225
  tokenAddress,
220
226
  sellAmountWei,
221
227
  sellProceedsWei: estimatedBNBOut,
222
- quoteTokenIn: useNativeToken ? WBNB_ADDRESS : quoteToken,
228
+ quoteTokenIn: useNativeToken ? WBNB_ADDRESS : effectiveQuoteToken,
223
229
  version,
224
230
  fee,
225
231
  v2RouterAddress: config.v2RouterAddress,
@@ -229,7 +235,7 @@ export async function pancakeBatchSwapMerkle(params) {
229
235
  profitAmount = calculateProfitAmount(profitNotional, config.userType);
230
236
  }
231
237
  else {
232
- const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, quoteToken, profitNotional, version, fee);
238
+ const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, effectiveQuoteToken, profitNotional, version, fee);
233
239
  profitAmount = calculateProfitAmount(estimatedBNBValue, config.userType);
234
240
  }
235
241
  // 计算利润 nonce
@@ -7,7 +7,7 @@ import { ethers, Contract, Wallet } from 'ethers';
7
7
  import { calculateSellAmount } from '../../../utils/swap-helpers.js';
8
8
  import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
9
9
  import { ERC20_ABI } from '../../../abis/common.js';
10
- import { BRIBE_GAS_LIMIT, FLAT_FEE, NATIVE_TRANSFER_GAS_LIMIT, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, ZERO_ADDRESS, buildBNBHopChainForERC20, buildERC20HopChain, buildNativeHopChain, calculateProfitAmount, createPancakeContext, generateDisperseHopPaths, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, quoteBuyOutput, applyPancakeSlippage, sequentialBuyAmountOutMins, taxedSellQuoteInput, } from './bundle-swap-helpers.js';
10
+ import { BRIBE_GAS_LIMIT, FLAT_FEE, NATIVE_TRANSFER_GAS_LIMIT, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, buildBNBHopChainForERC20, buildERC20HopChain, buildNativeHopChain, calculateProfitAmount, createPancakeContext, generateDisperseHopPaths, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, quoteBuyOutput, applyPancakeSlippage, sequentialBuyAmountOutMins, taxedSellQuoteInput, resolvePancakeQuoteMode, } from './bundle-swap-helpers.js';
11
11
  /**
12
12
  * PancakeSwap 快捷批量换手(资金自动流转)
13
13
  *
@@ -26,13 +26,17 @@ export async function pancakeQuickBatchSwapMerkle(params) {
26
26
  const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerRatios, buyerAmounts, tokenAddress, routeParams, config, quoteToken, quoteTokenDecimals = 18, disperseHopCount = 0, // ✅ 转账多跳数(默认0=直接转账)
27
27
  startNonces, // ✅ 可选:前端预获取的 nonces
28
28
  taxRateBps = 0, } = params;
29
- // ✅ 判断是否使用原生代币
30
- const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
31
- // ✅ 根据 chainId 获取正确的 Wrapped Native Token 地址
32
29
  const chainId = config.chainId ?? CHAINS.BSC.chainId;
33
30
  const WRAPPED_NATIVE = chainId === CHAINS.XLAYER.chainId
34
- ? ADDRESSES.XLAYER.WOKB.toLowerCase() // XLayer: WOKB
35
- : ADDRESSES.BSC.WBNB.toLowerCase(); // BSC: WBNB
31
+ ? ADDRESSES.XLAYER.WOKB.toLowerCase()
32
+ : ADDRESSES.BSC.WBNB.toLowerCase();
33
+ const quoteMode = resolvePancakeQuoteMode({
34
+ quoteToken,
35
+ routeParams,
36
+ wrappedNative: WRAPPED_NATIVE,
37
+ });
38
+ const useNativeToken = quoteMode.useNativeToken;
39
+ const effectiveQuoteToken = quoteMode.quoteToken;
36
40
  // ✅ 动态计算最大买方数量(根据多跳数)
37
41
  // 固定开销: 贿赂(1) + 卖出(1) + 利润多跳(PROFIT_HOP_COUNT + 1)
38
42
  // BNB 模式(无多跳): 转账(N) + 买入(N) = 2N
@@ -98,8 +102,8 @@ export async function pancakeQuickBatchSwapMerkle(params) {
98
102
  }
99
103
  else {
100
104
  // ERC20 模式:输出必须是指定的 quoteToken
101
- if (!sellOutputToken || sellOutputToken.toLowerCase() !== quoteToken.toLowerCase()) {
102
- throw new Error(`ERC20 模式要求卖出路径以 ${quoteToken} 结尾(当前输出: ${sellOutputToken || '未知'})。`);
105
+ if (!sellOutputToken || sellOutputToken.toLowerCase() !== effectiveQuoteToken.toLowerCase()) {
106
+ throw new Error(`ERC20 模式要求卖出路径以 ${effectiveQuoteToken} 结尾(当前输出: ${sellOutputToken || '未知'})。`);
103
107
  }
104
108
  }
105
109
  // ✅ 创建共享资源
@@ -127,7 +131,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
127
131
  tokenAddress,
128
132
  sellAmountWei,
129
133
  sellProceedsWei: estimatedOutput,
130
- quoteTokenIn: useNativeToken ? WBNB_ADDRESS : quoteToken,
134
+ quoteTokenIn: useNativeToken ? WBNB_ADDRESS : effectiveQuoteToken,
131
135
  version,
132
136
  fee,
133
137
  v2RouterAddress: config.v2RouterAddress,
@@ -137,7 +141,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
137
141
  profitAmount = calculateProfitAmount(profitNotional, config.userType);
138
142
  }
139
143
  else {
140
- const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, quoteToken, profitNotional, version, fee);
144
+ const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, effectiveQuoteToken, profitNotional, version, fee);
141
145
  profitAmount = calculateProfitAmount(estimatedBNBValue, config.userType);
142
146
  }
143
147
  const distributableAmount = estimatedOutput - (useNativeToken ? profitAmount : 0n);
@@ -295,7 +299,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
295
299
  transferTxs = await Promise.all(buyers.map((buyer, i) => {
296
300
  const transferData = erc20Interface.encodeFunctionData('transfer', [buyer.address, transferAmountsWei[i]]);
297
301
  return seller.signTransaction({
298
- to: quoteToken,
302
+ to: effectiveQuoteToken,
299
303
  data: transferData,
300
304
  value: 0n,
301
305
  nonce: transferNonces[i],
@@ -333,7 +337,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
333
337
  // 2. 构建 ERC20 多跳链
334
338
  const erc20HopChains = await Promise.all(hopPaths.map((path, i) => {
335
339
  const payerNonce = sellerNonce + i; // ✅ payer 每个 buyer 只用 1 个 nonce
336
- return buildERC20HopChain(seller, path, quoteToken, transferAmountsWei[i], gasPrice, context.chainId, txType, payerNonce);
340
+ return buildERC20HopChain(seller, path, effectiveQuoteToken, transferAmountsWei[i], gasPrice, context.chainId, txType, payerNonce);
337
341
  }));
338
342
  const erc20Txs = erc20HopChains.flat();
339
343
  sellerNonce += buyers.length; // ✅ payer 只增加 buyers.length 个 nonce
@@ -6,12 +6,17 @@ import { getGasLimit } from '../../../core/gas/bundle-gas.js';
6
6
  import { ethers, Wallet } from 'ethers';
7
7
  import { calculateSellAmount } from '../../../utils/swap-helpers.js';
8
8
  import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
9
- import { BRIBE_GAS_LIMIT, ZERO_ADDRESS, buildNoncePlanFromNonces, buildSwapTransactions, WBNB_ADDRESS, calculateBuyerBudget, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, planNonces, quoteSellOutput, applyPancakeSlippage, quoteBuyOutput, validateFinalBalances, } from './bundle-swap-helpers.js';
9
+ import { BRIBE_GAS_LIMIT, buildNoncePlanFromNonces, buildSwapTransactions, WBNB_ADDRESS, calculateBuyerBudget, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, planNonces, quoteSellOutput, applyPancakeSlippage, quoteBuyOutput, resolvePancakeQuoteMode, taxedSellQuoteInput, sequentialBuyAmountOutMins, validateFinalBalances, } from './bundle-swap-helpers.js';
10
10
  export async function pancakeBundleSwapMerkle(params) {
11
11
  const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKey, tokenAddress, routeParams, config, quoteToken, quoteTokenDecimals = 18, startNonces, // ✅ 可选:前端预获取的 nonces
12
- } = params;
13
- // ✅ 判断是否使用原生代币(BNB)或 ERC20 代币(如 USDT)
14
- const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
12
+ taxRateBps = 0, } = params;
13
+ const quoteMode = resolvePancakeQuoteMode({
14
+ quoteToken,
15
+ routeParams,
16
+ wrappedNative: WBNB_ADDRESS,
17
+ });
18
+ const useNativeToken = quoteMode.useNativeToken;
19
+ const effectiveQuoteToken = quoteMode.quoteToken;
15
20
  const context = createPancakeContext(config);
16
21
  const seller = new Wallet(sellerPrivateKey, context.provider);
17
22
  const buyer = new Wallet(buyerPrivateKey, context.provider);
@@ -28,7 +33,7 @@ export async function pancakeBundleSwapMerkle(params) {
28
33
  const { amount: sellAmountWei, decimals } = sellAmountResult;
29
34
  const quoteResult = await quoteSellOutput({
30
35
  routeParams,
31
- sellAmountWei,
36
+ sellAmountWei: taxedSellQuoteInput(sellAmountWei, taxRateBps),
32
37
  provider: context.provider,
33
38
  v2RouterAddress: config.v2RouterAddress,
34
39
  });
@@ -37,7 +42,7 @@ export async function pancakeBundleSwapMerkle(params) {
37
42
  quotedBNBOut: quoteResult.estimatedBNBOut,
38
43
  reserveGasBNB: config.reserveGasBNB,
39
44
  useNativeToken,
40
- quoteToken,
45
+ quoteToken: effectiveQuoteToken,
41
46
  quoteTokenDecimals,
42
47
  provider: context.provider,
43
48
  });
@@ -48,6 +53,7 @@ export async function pancakeBundleSwapMerkle(params) {
48
53
  v2RouterAddress: config.v2RouterAddress,
49
54
  });
50
55
  const slippageBps = config.slippageBps ?? 100;
56
+ const buyMinOut = sequentialBuyAmountOutMins([quotedBuyOut], taxRateBps, slippageBps)[0] ?? 0n;
51
57
  const swapUnsigned = await buildSwapTransactions({
52
58
  routeParams,
53
59
  sellAmountWei,
@@ -58,7 +64,7 @@ export async function pancakeBundleSwapMerkle(params) {
58
64
  useNativeToken,
59
65
  v2RouterAddress: config.v2RouterAddress,
60
66
  sellAmountOutMin: applyPancakeSlippage(quoteResult.estimatedBNBOut, slippageBps),
61
- buyAmountOutMin: applyPancakeSlippage(quotedBuyOut, slippageBps),
67
+ buyAmountOutMin: buyMinOut,
62
68
  });
63
69
  const version = routeParams.routeType === 'v2' ? 'v2' : 'v3';
64
70
  const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
@@ -67,7 +73,7 @@ export async function pancakeBundleSwapMerkle(params) {
67
73
  tokenAddress,
68
74
  sellAmountWei,
69
75
  sellProceedsWei: quoteResult.estimatedBNBOut,
70
- quoteTokenIn: useNativeToken ? WBNB_ADDRESS : quoteToken,
76
+ quoteTokenIn: useNativeToken ? WBNB_ADDRESS : effectiveQuoteToken,
71
77
  version,
72
78
  fee,
73
79
  v2RouterAddress: config.v2RouterAddress,
@@ -77,7 +83,7 @@ export async function pancakeBundleSwapMerkle(params) {
77
83
  profitAmount = calculateProfitAmount(profitNotional, config.userType);
78
84
  }
79
85
  else {
80
- const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, quoteToken, profitNotional, version, fee);
86
+ const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, effectiveQuoteToken, profitNotional, version, fee);
81
87
  profitAmount = calculateProfitAmount(estimatedBNBValue, config.userType);
82
88
  }
83
89
  // ✅ 获取贿赂金额
@@ -73,6 +73,7 @@ describe('OpenFour create value', () => {
73
73
  createFee: parseEther('0.01'),
74
74
  presaleQuote: parseEther('0.2'),
75
75
  quoteAsset: wbnb,
76
+ validTimestamp: 0n,
76
77
  };
77
78
  expect(isOpenFourPresaleNative({ ...decoded, wrappedNative: wbnb })).toBe(true);
78
79
  expect(computeOpenFourCreateValueWei(decoded, wbnb)).toBe(parseEther('0.21'));
@@ -82,6 +83,7 @@ describe('OpenFour create value', () => {
82
83
  createFee: parseEther('0.01'),
83
84
  presaleQuote: parseEther('100'),
84
85
  quoteAsset: usdt,
86
+ validTimestamp: 0n,
85
87
  };
86
88
  expect(isOpenFourPresaleNative({ ...decoded, wrappedNative: wbnb })).toBe(false);
87
89
  expect(computeOpenFourCreateValueWei(decoded, wbnb)).toBe(parseEther('0.01'));
@@ -2,8 +2,13 @@ export type OpenFourDecodedCreateArg = {
2
2
  createFee: bigint;
3
3
  presaleQuote: bigint;
4
4
  quoteAsset: string;
5
+ validTimestamp: bigint;
5
6
  };
7
+ /** Four 签名在链上只活到 validTimestamp;提交前至少留这么多秒,否则重拉 createArg。 */
8
+ export declare const OPENFOUR_CREATE_MIN_REMAIN_SEC = 15n;
9
+ export declare const OPENFOUR_CREATE_EXPIRED_MESSAGE = "OpenFour \u53D1\u5E01\u7B7E\u540D\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u53D1\u5C04\u3002Four \u7684 validTimestamp \u5F88\u77ED\uFF0C\u63A5\u53E3\u6162\u65F6\u7B7E\u540D\u5230\u8FBE\u94FE\u4E0A\u5C31\u5DF2\u7ECF\u5931\u6548\u3002";
6
10
  export declare function decodeOpenFourCreateArg(rawCreateArg: string): OpenFourDecodedCreateArg;
11
+ export declare function shouldRefreshOpenFourCreateArg(validTimestamp: bigint, chainTimestamp: bigint, minRemainSec?: bigint): boolean;
7
12
  export declare function isOpenFourPresaleNative(params: {
8
13
  presaleQuote: bigint;
9
14
  quoteAsset: string;
@@ -40,6 +40,9 @@ const ABI_13 = [
40
40
  }),
41
41
  ];
42
42
  const coder = AbiCoder.defaultAbiCoder();
43
+ /** Four 签名在链上只活到 validTimestamp;提交前至少留这么多秒,否则重拉 createArg。 */
44
+ export const OPENFOUR_CREATE_MIN_REMAIN_SEC = 15n;
45
+ export const OPENFOUR_CREATE_EXPIRED_MESSAGE = 'OpenFour 发币签名已过期,请再点一次发射。Four 的 validTimestamp 很短,接口慢时签名到达链上就已经失效。';
43
46
  function toBigInt(value) {
44
47
  if (value == null)
45
48
  return 0n;
@@ -60,8 +63,12 @@ export function decodeOpenFourCreateArg(rawCreateArg) {
60
63
  createFee: toBigInt(row.createFee),
61
64
  presaleQuote: toBigInt(row.presaleQuote),
62
65
  quoteAsset: String(row.initParams?.quoteAsset ?? ''),
66
+ validTimestamp: toBigInt(row.validTimestamp),
63
67
  };
64
68
  }
69
+ export function shouldRefreshOpenFourCreateArg(validTimestamp, chainTimestamp, minRemainSec = OPENFOUR_CREATE_MIN_REMAIN_SEC) {
70
+ return validTimestamp < chainTimestamp + minRemainSec;
71
+ }
65
72
  export function isOpenFourPresaleNative(params) {
66
73
  if (params.presaleQuote <= 0n)
67
74
  return false;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
1
+ import { AbiCoder, ParamType, ZeroAddress, ZeroHash } from 'ethers';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { OPENFOUR_CREATE_MIN_REMAIN_SEC, decodeOpenFourCreateArg, shouldRefreshOpenFourCreateArg, } from './create-arg.js';
4
+ const INIT = [
5
+ { name: 'name', type: 'string' },
6
+ { name: 'symbol', type: 'string' },
7
+ { name: 'tokenUri', type: 'string' },
8
+ { name: 'maxSupply', type: 'uint256' },
9
+ { name: 'saleAmount', type: 'uint256' },
10
+ { name: 'raiseAmount', type: 'uint256' },
11
+ { name: 'quoteAsset', type: 'address' },
12
+ { name: 'tokenSalt', type: 'bytes32' },
13
+ { name: 'tokenParams', type: 'bytes' },
14
+ { name: 'vaultParams', type: 'bytes' },
15
+ { name: 'curveParams', type: 'bytes' },
16
+ { name: 'tradeParams', type: 'bytes' },
17
+ { name: 'migrateParams', type: 'bytes' },
18
+ { name: 'customDataParams', type: 'bytes' },
19
+ ];
20
+ const ABI = [
21
+ ParamType.from({
22
+ type: 'tuple',
23
+ components: [
24
+ { name: 'requestId', type: 'uint256' },
25
+ { name: 'presetId', type: 'uint256' },
26
+ { name: 'validTimestamp', type: 'uint256' },
27
+ { name: 'createFee', type: 'uint256' },
28
+ { name: 'presaleQuote', type: 'uint256' },
29
+ { name: 'antiSniperEnabled', type: 'bool' },
30
+ { name: 'initParams', type: 'tuple', components: INIT },
31
+ ],
32
+ }),
33
+ ];
34
+ function encodeArg(validTimestamp, quoteAsset = ZeroAddress) {
35
+ return AbiCoder.defaultAbiCoder().encode(ABI, [
36
+ [
37
+ 1n,
38
+ 2n,
39
+ validTimestamp,
40
+ 0n,
41
+ 10000000000000000n,
42
+ false,
43
+ [
44
+ 'test',
45
+ 'test',
46
+ 'https://example.com/t.json',
47
+ 1n,
48
+ 1n,
49
+ 1n,
50
+ quoteAsset,
51
+ ZeroHash,
52
+ '0x',
53
+ '0x',
54
+ '0x',
55
+ '0x',
56
+ '0x',
57
+ '0x',
58
+ ],
59
+ ],
60
+ ]);
61
+ }
62
+ describe('decodeOpenFourCreateArg', () => {
63
+ it('reads validTimestamp and quoteAsset', () => {
64
+ const quote = '0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1';
65
+ const decoded = decodeOpenFourCreateArg(encodeArg(1789979444n, quote));
66
+ expect(decoded.validTimestamp).toBe(1789979444n);
67
+ expect(decoded.presaleQuote).toBe(10000000000000000n);
68
+ expect(decoded.quoteAsset.toLowerCase()).toBe(quote.toLowerCase());
69
+ });
70
+ });
71
+ describe('shouldRefreshOpenFourCreateArg', () => {
72
+ it('refreshes when the deadline is already past', () => {
73
+ expect(shouldRefreshOpenFourCreateArg(100n, 101n)).toBe(true);
74
+ });
75
+ it('refreshes when remaining time is shorter than the submit buffer', () => {
76
+ expect(shouldRefreshOpenFourCreateArg(100n + OPENFOUR_CREATE_MIN_REMAIN_SEC - 1n, 100n)).toBe(true);
77
+ });
78
+ it('keeps a signature with enough remaining time', () => {
79
+ expect(shouldRefreshOpenFourCreateArg(100n + OPENFOUR_CREATE_MIN_REMAIN_SEC, 100n)).toBe(false);
80
+ });
81
+ });
@@ -1,4 +1,4 @@
1
1
  export type { OpenFourAddresses, OpenFourCreateReq, OpenFourInitParams, OpenFourModuleSchema, OpenFourParamDescriptor, OpenFourPresetSchemas, OpenFourTemplate, OpenFourTemplateConfig, } from './types.js';
2
2
  export { collectOpenFourSchemaParams, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, } from './schema.js';
3
- export { computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, type OpenFourDecodedCreateArg, } from './create-arg.js';
3
+ export { OPENFOUR_CREATE_EXPIRED_MESSAGE, OPENFOUR_CREATE_MIN_REMAIN_SEC, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, shouldRefreshOpenFourCreateArg, type OpenFourDecodedCreateArg, } from './create-arg.js';
4
4
  export { loadOpenFourAddresses, loadOpenFourPresetSchemas } from './registry.js';
@@ -1,3 +1,3 @@
1
1
  export { collectOpenFourSchemaParams, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, } from './schema.js';
2
- export { computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, } from './create-arg.js';
2
+ export { OPENFOUR_CREATE_EXPIRED_MESSAGE, OPENFOUR_CREATE_MIN_REMAIN_SEC, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, shouldRefreshOpenFourCreateArg, } from './create-arg.js';
3
3
  export { loadOpenFourAddresses, loadOpenFourPresetSchemas } from './registry.js';
@@ -15,7 +15,7 @@ export { ensureSellApprovalV1, checkSellApprovalV1, ensureSellApprovalV2, checkS
15
15
  export { parseFourError, type FourErrorCode } from '../utils/errors.js';
16
16
  export { getTokenManagerV1, getTokenManagerV2, getTokenManagerHelper3, getTokenManagerV1Writer, getTokenManagerV2Writer, getTokenManagerHelper3Writer, getTokenManagerAddress, type ChainName, } from '../utils/contract-factory.js';
17
17
  export { FourClient, buildLoginMessage, buildCreateTokenReq, type FourConfig, type GenerateNonceReq, type LoginReq, type CreateTokenReq, type CreateTokenCustom, type CreateTokenResp, type FourTokenTaxInfo, type FourRaisedToken, type FourTokenLabel, } from '../core/clients/four.js';
18
- export { collectOpenFourSchemaParams, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, isOpenFourPresaleNative, loadOpenFourAddresses, loadOpenFourPresetSchemas, type OpenFourAddresses, type OpenFourCreateReq, type OpenFourDecodedCreateArg, type OpenFourInitParams, type OpenFourModuleSchema, type OpenFourParamDescriptor, type OpenFourPresetSchemas, type OpenFourTemplate, type OpenFourTemplateConfig, } from '../domains/openfour/index.js';
18
+ export { OPENFOUR_CREATE_EXPIRED_MESSAGE, OPENFOUR_CREATE_MIN_REMAIN_SEC, collectOpenFourSchemaParams, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, isOpenFourPresaleNative, shouldRefreshOpenFourCreateArg, loadOpenFourAddresses, loadOpenFourPresetSchemas, type OpenFourAddresses, type OpenFourCreateReq, type OpenFourDecodedCreateArg, type OpenFourInitParams, type OpenFourModuleSchema, type OpenFourParamDescriptor, type OpenFourPresetSchemas, type OpenFourTemplate, type OpenFourTemplateConfig, } from '../domains/openfour/index.js';
19
19
  export { FOUR_TOKEN_LABELS, FOUR_BNB_RAISED_TOKEN_FALLBACK, parseRaisedTokens, publishedRaisedTokens, pickRaisedToken, isNativeRaisedToken, computeFourCreateValueWei, raisedTokenTradingFeeBps, } from '../domains/four/raised-token.js';
20
20
  export { createTokenOnChain, tryBuy, trySell, buyTokenWithFunds, sellToken, tradeBuy, tradeSell, type CreateOnChainParams, type TryBuyResult, type TrySellResult, } from '../domains/four/contracts/tm.js';
21
21
  export { TM1, type FourChainV1 } from '../domains/four/contracts/tm1.js';
@@ -50,7 +50,7 @@ checkAllowanceRaw, approveTokenRaw, checkAllowanceBatchRaw, approveTokenBatchRaw
50
50
  export { parseFourError } from '../utils/errors.js';
51
51
  export { getTokenManagerV1, getTokenManagerV2, getTokenManagerHelper3, getTokenManagerV1Writer, getTokenManagerV2Writer, getTokenManagerHelper3Writer, getTokenManagerAddress, } from '../utils/contract-factory.js';
52
52
  export { FourClient, buildLoginMessage, buildCreateTokenReq, } from '../core/clients/four.js';
53
- export { collectOpenFourSchemaParams, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, isOpenFourPresaleNative, loadOpenFourAddresses, loadOpenFourPresetSchemas, } from '../domains/openfour/index.js';
53
+ export { OPENFOUR_CREATE_EXPIRED_MESSAGE, OPENFOUR_CREATE_MIN_REMAIN_SEC, collectOpenFourSchemaParams, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, isOpenFourPresaleNative, shouldRefreshOpenFourCreateArg, loadOpenFourAddresses, loadOpenFourPresetSchemas, } from '../domains/openfour/index.js';
54
54
  export { FOUR_TOKEN_LABELS, FOUR_BNB_RAISED_TOKEN_FALLBACK, parseRaisedTokens, publishedRaisedTokens, pickRaisedToken, isNativeRaisedToken, computeFourCreateValueWei, raisedTokenTradingFeeBps, } from '../domains/four/raised-token.js';
55
55
  export { createTokenOnChain, tryBuy, trySell, buyTokenWithFunds, sellToken, tradeBuy, tradeSell, } from '../domains/four/contracts/tm.js';
56
56
  export { TM1 } from '../domains/four/contracts/tm1.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "4.3.14",
3
+ "version": "4.3.15",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",