four-flap-meme-sdk 4.3.14 → 4.3.16

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.
Files changed (31) hide show
  1. package/dist/bundle/four-meme/core/create-openfour.js +19 -2
  2. package/dist/bundle/four-meme/utils-disperse.js +2 -2
  3. package/dist/bundle/four-meme/utils-pairwise.js +2 -2
  4. package/dist/bundle/four-meme/utils-sweep/erc20-no-hop.js +2 -2
  5. package/dist/bundle/four-meme/utils-sweep/with-hops.js +3 -3
  6. package/dist/bundle/internal.d.ts +1 -1
  7. package/dist/bundle/internal.js +8 -11
  8. package/dist/chains/bsc/pancake/bundle-swap-types.d.ts +2 -0
  9. package/dist/chains/bsc/pancake/quote.d.ts +14 -0
  10. package/dist/chains/bsc/pancake/quote.js +31 -1
  11. package/dist/chains/bsc/pancake/quote.test.js +21 -1
  12. package/dist/chains/bsc/pancake/swap-batch.d.ts +1 -0
  13. package/dist/chains/bsc/pancake/swap-batch.js +14 -8
  14. package/dist/chains/bsc/pancake/swap-quick.js +16 -12
  15. package/dist/chains/bsc/pancake/swap-single.js +15 -9
  16. package/dist/core/constants/gas-erc20-floor.test.d.ts +1 -0
  17. package/dist/core/constants/gas-erc20-floor.test.js +24 -0
  18. package/dist/core/constants/gas.d.ts +2 -0
  19. package/dist/core/constants/gas.js +5 -0
  20. package/dist/core/constants/index.d.ts +1 -1
  21. package/dist/core/constants/index.js +1 -1
  22. package/dist/domains/four/tax-token.test.js +2 -0
  23. package/dist/domains/openfour/create-arg.d.ts +7 -0
  24. package/dist/domains/openfour/create-arg.js +11 -0
  25. package/dist/domains/openfour/create-arg.test.d.ts +1 -0
  26. package/dist/domains/openfour/create-arg.test.js +91 -0
  27. package/dist/domains/openfour/index.d.ts +1 -1
  28. package/dist/domains/openfour/index.js +1 -1
  29. package/dist/exports/root-foundations.d.ts +1 -1
  30. package/dist/exports/root-foundations.js +1 -1
  31. package/package.json +1 -1
@@ -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, isOpenFourCreateArgExpired, 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 (isOpenFourCreateArgExpired(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));
@@ -1,6 +1,6 @@
1
1
  import { ethers, Wallet } from 'ethers';
2
2
  import { getOptimizedGasPrice, NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../utils/bundle-helpers.js';
3
- import { calculateTransferFee, GAS_LIMITS, isBscChain } from '../../core/constants/index.js';
3
+ import { calculateTransferFee, floorErc20TransferGasLimit, GAS_LIMITS, isBscChain } from '../../core/constants/index.js';
4
4
  import { getGasPriceConfig, shouldExtractProfit, getProfitRecipient, getBribeAmount } from '../config-helpers.js';
5
5
  import { getErc20Decimals as _getErc20Decimals, generateHopWallets as _generateHopWallets, normalizeAmounts as _normalizeAmounts, calculateGasLimit as _calculateGasLimit, isNativeTokenAddress as _isNativeTokenAddress, } from '../internal.js';
6
6
  import { normalizeItemsOrRecipientsAmounts, resolveHopCountInput, resolveProvidedHops, resolveSignContext, signBribeTransaction, } from '../../core/sign-context-helpers.js';
@@ -138,7 +138,7 @@ export async function disperseWithBundleMerkle(params) {
138
138
  // - 原生代币转账:贿赂档
139
139
  // - ERC20 转账:走 GAS_LIMITS(含股票币代理)
140
140
  const nativeTransferGasLimit = GAS_LIMITS.BRIBE;
141
- const erc20TransferGasLimit = GAS_LIMITS.ERC20_TRANSFER;
141
+ const erc20TransferGasLimit = floorErc20TransferGasLimit(finalGasLimit);
142
142
  // ✅ 原生代币多跳的 gas 费(每跳只需要 21000)
143
143
  const nativeHopGasFee = nativeTransferGasLimit * gasPrice;
144
144
  // ✅ ERC20 多跳时,中转钱包需要执行 2 笔交易(转 gas + 转 ERC20)
@@ -1,6 +1,6 @@
1
1
  import { ethers, Wallet } from 'ethers';
2
2
  import { getOptimizedGasPrice, NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFields, } from '../../utils/bundle-helpers.js';
3
- import { BLOCKRAZOR_BUILDER_EOA, calculateTransferFee, CHAINS, GAS_LIMITS, isBscChain } from '../../core/constants/index.js';
3
+ import { BLOCKRAZOR_BUILDER_EOA, calculateTransferFee, CHAINS, floorErc20TransferGasLimit, isBscChain, } from '../../core/constants/index.js';
4
4
  import { getTxType, getGasPriceConfig, shouldExtractProfit, getProfitRecipient, getBribeAmount, } from '../config-helpers.js';
5
5
  import { getErc20Decimals as _getErc20Decimals, generateHopWallets as _generateHopWallets, calculateGasLimit as _calculateGasLimit, isNativeTokenAddress as _isNativeTokenAddress, } from '../internal.js';
6
6
  import { createSignProvider, resolveProvidedHops } from '../../core/sign-context-helpers.js';
@@ -48,7 +48,7 @@ export async function pairwiseTransferWithBundleMerkle(params) {
48
48
  const finalGasLimit = _calculateGasLimit(config, isNative, hasHops, maxHopCount);
49
49
  const nativeGasLimit = (config.prefer21000ForNative ?? false) ? 21000n : finalGasLimit;
50
50
  const nativeTransferGasLimit = 21000n;
51
- const erc20TransferGasLimit = config.gasLimit ? BigInt(config.gasLimit) : GAS_LIMITS.ERC20_TRANSFER;
51
+ const erc20TransferGasLimit = floorErc20TransferGasLimit(config.gasLimit);
52
52
  const [gasPrice, decimals] = await Promise.all([
53
53
  getOptimizedGasPrice(provider, getGasPriceConfig(config)),
54
54
  isNative
@@ -4,7 +4,7 @@
4
4
  import { ethers, Wallet } from 'ethers';
5
5
  import { NonceManager, getOptimizedGasPrice, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
6
6
  import { getGasPriceConfig } from '../../config-helpers.js';
7
- import { calculateTransferFee } from '../../../core/constants/index.js';
7
+ import { calculateTransferFee, floorErc20TransferGasLimit } from '../../../core/constants/index.js';
8
8
  import { batchGetBalances as _batchGetBalances, getErc20Decimals as _getErc20Decimals } from '../../internal.js';
9
9
  import { clampRatio } from './context.js';
10
10
  export async function sweepErc20NoHop(ctx) {
@@ -134,7 +134,7 @@ export async function sweepErc20NoHop(ctx) {
134
134
  value: 0n,
135
135
  nonce,
136
136
  gasPrice,
137
- gasLimit: finalGasLimit,
137
+ gasLimit: floorErc20TransferGasLimit(finalGasLimit),
138
138
  chainId: BigInt(chainIdNum),
139
139
  type: txType,
140
140
  });
@@ -4,7 +4,7 @@
4
4
  import { ethers, Wallet } from 'ethers';
5
5
  import { NonceManager, getOptimizedGasPrice, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
6
6
  import { getGasPriceConfig } from '../../config-helpers.js';
7
- import { calculateTransferFee, GAS_LIMITS } from '../../../core/constants/index.js';
7
+ import { calculateTransferFee, floorErc20TransferGasLimit, GAS_LIMITS } from '../../../core/constants/index.js';
8
8
  import { batchGetBalances as _batchGetBalances, getErc20Decimals as _getErc20Decimals } from '../../internal.js';
9
9
  import { clampRatio } from './context.js';
10
10
  export async function sweepWithHops(ctx, preparedHops) {
@@ -38,7 +38,7 @@ export async function sweepWithHops(ctx, preparedHops) {
38
38
  const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
39
39
  // ✅ Gas limit 设置(与分散函数保持一致)
40
40
  const nativeTransferGasLimit = GAS_LIMITS.BRIBE;
41
- const erc20TransferGasLimit = GAS_LIMITS.ERC20_TRANSFER;
41
+ const erc20TransferGasLimit = floorErc20TransferGasLimit(finalGasLimit);
42
42
  // ✅ 原生代币多跳的 gas 费(每跳只需要 21000)
43
43
  const nativeHopGasFee = nativeTransferGasLimit * gasPrice;
44
44
  // ✅ ERC20 多跳时,中转钱包需要执行 2 笔交易(转 gas + 转 ERC20)
@@ -124,7 +124,7 @@ export async function sweepWithHops(ctx, preparedHops) {
124
124
  value: 0n,
125
125
  nonce,
126
126
  gasPrice,
127
- gasLimit: finalGasLimit,
127
+ gasLimit: erc20TransferGasLimit,
128
128
  chainId: BigInt(chainIdNum),
129
129
  type: txType,
130
130
  });
@@ -11,6 +11,6 @@ export declare function normalizeAmounts(recipients: string[], amount?: AmountLi
11
11
  export declare function batchGetBalances(provider: JsonRpcProvider, addresses: string[], tokenAddress?: string, chainId?: number): Promise<bigint[]>;
12
12
  export declare function estimateErc20TransferGas(provider: JsonRpcProvider, tokenAddress: string, from: string, to: string, amount: bigint, bufferPercent?: number): Promise<bigint>;
13
13
  export declare function estimateMaxErc20TransferGas(provider: JsonRpcProvider, tokenAddress: string, from: string, recipients: string[], amounts: bigint[], bufferPercent?: number): Promise<bigint>;
14
- export declare function calculateGasLimit(config: BundleGasConfig, isNative: boolean, hasHops: boolean, hopCount?: number): bigint;
14
+ export declare function calculateGasLimit(config: BundleGasConfig, isNative: boolean, _hasHops: boolean, _hopCount?: number): bigint;
15
15
  export declare function isNativeTokenAddress(tokenAddress?: string): boolean;
16
16
  export type { AmountLike, BundleGasConfig } from './types/index.js';
@@ -1,6 +1,6 @@
1
1
  import { ethers, Wallet } from 'ethers';
2
2
  import { MULTICALL3_ABI, ERC20_BALANCE_ABI } from '../abis/common.js';
3
- import { CHAINS, GAS_LIMITS, getMulticall3Address } from '../core/constants/index.js';
3
+ import { CHAINS, GAS_LIMITS, floorErc20TransferGasLimit, getMulticall3Address } from '../core/constants/index.js';
4
4
  export { getErc20Decimals, getErc20DecimalsMerkle } from '../core/erc20/decimals.js';
5
5
  function mapAggregate3Results(results, decode, fallback) {
6
6
  return results.map((result) => {
@@ -129,20 +129,17 @@ export async function estimateMaxErc20TransferGas(provider, tokenAddress, from,
129
129
  return 52000n;
130
130
  }
131
131
  }
132
- export function calculateGasLimit(config, isNative, hasHops, hopCount = 0) {
132
+ export function calculateGasLimit(config, isNative, _hasHops, _hopCount = 0) {
133
+ if (!isNative) {
134
+ return floorErc20TransferGasLimit(config.gasLimit);
135
+ }
133
136
  if (config.gasLimit !== undefined) {
134
137
  return BigInt(config.gasLimit);
135
138
  }
136
139
  const isXLayer = config.chainId === CHAINS.XLAYER.chainId;
137
- const baseGas = isNative
138
- ? isXLayer
139
- ? Number(GAS_LIMITS.XLAYER_NATIVE_TRANSFER)
140
- : Number(GAS_LIMITS.NATIVE_TRANSFER)
141
- : 46815;
142
- if (hasHops && hopCount > 0) {
143
- const multiplier = config.gasLimitMultiplier ?? 1.1;
144
- return BigInt(Math.ceil(baseGas * multiplier));
145
- }
140
+ const baseGas = isXLayer
141
+ ? Number(GAS_LIMITS.XLAYER_NATIVE_TRANSFER)
142
+ : Number(GAS_LIMITS.NATIVE_TRANSFER);
146
143
  const multiplier = config.gasLimitMultiplier ?? 1.1;
147
144
  return BigInt(Math.ceil(baseGas * multiplier));
148
145
  }
@@ -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
  // ✅ 获取贿赂金额
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateGasLimit } from '../../bundle/internal.js';
3
+ import { GAS_LIMITS, floorErc20TransferGasLimit } from './gas.js';
4
+ describe('floorErc20TransferGasLimit', () => {
5
+ it('raises USDT-era 65000 to SPCXB-safe 120000', () => {
6
+ expect(floorErc20TransferGasLimit(65000)).toBe(GAS_LIMITS.ERC20_TRANSFER);
7
+ expect(floorErc20TransferGasLimit(65000n)).toBe(120000n);
8
+ });
9
+ it('keeps higher user limits for tax tokens', () => {
10
+ expect(floorErc20TransferGasLimit(150000)).toBe(150000n);
11
+ expect(floorErc20TransferGasLimit(300000n)).toBe(300000n);
12
+ });
13
+ it('defaults to ERC20_TRANSFER', () => {
14
+ expect(floorErc20TransferGasLimit()).toBe(120000n);
15
+ });
16
+ });
17
+ describe('bundle calculateGasLimit ERC20 floor', () => {
18
+ it('does not honor 65000 for ERC20 collect', () => {
19
+ expect(calculateGasLimit({ gasLimit: 65000 }, false, false)).toBe(120000n);
20
+ });
21
+ it('keeps native collect at 21055', () => {
22
+ expect(calculateGasLimit({ gasLimit: 21055 }, true, false)).toBe(21055n);
23
+ });
24
+ });
@@ -82,3 +82,5 @@ export declare function calculateGasLimit(chainId: number, isNative: boolean, ha
82
82
  * @param isNative 是否原生代币
83
83
  */
84
84
  export declare function getTransferGasLimit(chainId: number, isNative: boolean): bigint;
85
+ /** 归集/分发 ERC20:UI 常传 65000(USDT),SPCXB/税币 transfer 会 OOG。 */
86
+ export declare function floorErc20TransferGasLimit(requested?: number | bigint): bigint;
@@ -119,3 +119,8 @@ export function getTransferGasLimit(chainId, isNative) {
119
119
  }
120
120
  return GAS_LIMITS.ERC20_TRANSFER;
121
121
  }
122
+ /** 归集/分发 ERC20:UI 常传 65000(USDT),SPCXB/税币 transfer 会 OOG。 */
123
+ export function floorErc20TransferGasLimit(requested) {
124
+ const n = requested === undefined ? GAS_LIMITS.ERC20_TRANSFER : BigInt(requested);
125
+ return n < GAS_LIMITS.ERC20_TRANSFER ? GAS_LIMITS.ERC20_TRANSFER : n;
126
+ }
@@ -9,7 +9,7 @@
9
9
  * const wbnb = ADDRESSES.BSC.WBNB;
10
10
  */
11
11
  export { CHAINS, CHAIN_ID_MAP, getChainById, getChainName, getDefaultRpcUrl, isKnownChain, isBscChain, isXLayerChain, isMonadChain, isEniChain, isRobinhoodChain, getConfirmationTimeout, type ChainKey, type ChainConfig, } from './chains.js';
12
- export { GAS_LIMITS, GAS_CONFIG, V3_FEE_TIERS, calculateGasLimit, getTransferGasLimit, type GasLimitKey, type V3FeeTier, } from './gas.js';
12
+ export { GAS_LIMITS, GAS_CONFIG, V3_FEE_TIERS, calculateGasLimit, getTransferGasLimit, floorErc20TransferGasLimit, type GasLimitKey, type V3FeeTier, } from './gas.js';
13
13
  export { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS, getQuoteChainKey, type QuoteConfigChain } from './quote.js';
14
14
  export { applySlippageBps, reverseSlippageBps } from './slippage.js';
15
15
  /** Flap 链标识(与 `FlapPortal` 一致,供子路径 `./constants` 使用) */
@@ -15,7 +15,7 @@ export { CHAINS, CHAIN_ID_MAP, getChainById, getChainName, getDefaultRpcUrl, isK
15
15
  // ============================================================================
16
16
  // Gas 常量
17
17
  // ============================================================================
18
- export { GAS_LIMITS, GAS_CONFIG, V3_FEE_TIERS, calculateGasLimit, getTransferGasLimit, } from './gas.js';
18
+ export { GAS_LIMITS, GAS_CONFIG, V3_FEE_TIERS, calculateGasLimit, getTransferGasLimit, floorErc20TransferGasLimit, } from './gas.js';
19
19
  export { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS, getQuoteChainKey } from './quote.js';
20
20
  export { applySlippageBps, reverseSlippageBps } from './slippage.js';
21
21
  // ============================================================================
@@ -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,15 @@ 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 = 90n;
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;
12
+ /** 合约是 block.timestamp > validTimestamp 才回滚;相等这一秒仍有效。 */
13
+ export declare function isOpenFourCreateArgExpired(validTimestamp: bigint, chainTimestamp: bigint): boolean;
7
14
  export declare function isOpenFourPresaleNative(params: {
8
15
  presaleQuote: bigint;
9
16
  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 = 90n;
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,16 @@ 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
+ }
72
+ /** 合约是 block.timestamp > validTimestamp 才回滚;相等这一秒仍有效。 */
73
+ export function isOpenFourCreateArgExpired(validTimestamp, chainTimestamp) {
74
+ return validTimestamp < chainTimestamp;
75
+ }
65
76
  export function isOpenFourPresaleNative(params) {
66
77
  if (params.presaleQuote <= 0n)
67
78
  return false;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ import { AbiCoder, ParamType, ZeroAddress, ZeroHash } from 'ethers';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { OPENFOUR_CREATE_MIN_REMAIN_SEC, decodeOpenFourCreateArg, isOpenFourCreateArgExpired, 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
+ });
82
+ describe('isOpenFourCreateArgExpired', () => {
83
+ it('treats equal timestamps as still valid', () => {
84
+ expect(isOpenFourCreateArgExpired(100n, 100n)).toBe(false);
85
+ });
86
+ it('treats the dumped Royalty+SPCXB deadline as expired at 17:30 CST', () => {
87
+ const validTimestamp = 1789982212n;
88
+ const reportedAt = BigInt(Math.floor(Date.parse('2026-09-21T09:30:00.000Z') / 1000));
89
+ expect(isOpenFourCreateArgExpired(validTimestamp, reportedAt)).toBe(true);
90
+ });
91
+ });
@@ -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, isOpenFourCreateArgExpired, 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, isOpenFourCreateArgExpired, 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.16",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",