four-flap-meme-sdk 4.3.10 → 4.3.12

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.
@@ -0,0 +1,33 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { setProfitRates } from '../../../core/constants/profit.js';
3
+ import { openFourSubmitProfitWei } from './create-openfour.js';
4
+ const defaultRates = {
5
+ normal: 30,
6
+ bundleSwap: 3,
7
+ v0Single: 6,
8
+ v0Double: 12,
9
+ v1Single: 5,
10
+ v1Double: 10,
11
+ };
12
+ beforeEach(() => {
13
+ setProfitRates(defaultRates);
14
+ });
15
+ afterEach(() => {
16
+ setProfitRates(defaultRates);
17
+ });
18
+ describe('openFourSubmitProfitWei', () => {
19
+ it('takes 30 bps of native create/presale flow', () => {
20
+ setProfitRates({
21
+ normal: 30,
22
+ bundleSwap: 3,
23
+ v0Single: 6,
24
+ v0Double: 12,
25
+ v1Single: 5,
26
+ v1Double: 10,
27
+ });
28
+ expect(openFourSubmitProfitWei(10000n)).toBe(30n);
29
+ });
30
+ it('uses dust when there is no native flow (ERC20 quote, no presale)', () => {
31
+ expect(openFourSubmitProfitWei(0n)).toBe(10n ** 12n);
32
+ });
33
+ });
@@ -1,5 +1,6 @@
1
1
  import type { FourCreateWithBundleBuyMerkleResult, FourSignConfig } from '../types/index.js';
2
2
  import { type OpenFourInitParams, type OpenFourTemplateConfig } from '../../../domains/openfour/index.js';
3
+ export declare function openFourSubmitProfitWei(nativeFlowWei: bigint): bigint;
3
4
  export type OpenFourCreateSignParams = {
4
5
  privateKeys: string[];
5
6
  tokenInfo: {
@@ -1,17 +1,37 @@
1
1
  /**
2
- * OpenFour 发币:业务 API 拿 createArg + signature,再签 OpenFourCore.createToken
2
+ * OpenFour 发币:业务 API 拿 createArg + signature,再签 OpenFourCore.createToken。
3
+ * BSC 提交必须在末尾带 3 笔原生利润多跳,否则 emit-api 返回 code 14。
3
4
  */
4
- import { Contract, Wallet } from 'ethers';
5
+ import { Contract, Transaction, Wallet } from 'ethers';
5
6
  import { getGasLimit } from '../../../core/gas/bundle-gas.js';
6
- import { NonceManager, getOptimizedGasPrice } from '../../../utils/bundle-helpers.js';
7
+ import { NonceManager, getOptimizedGasPrice, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
7
8
  import { ZERO_ADDRESS } from '../../../core/constants/index.js';
8
9
  import { FourClient } from '../../../core/clients/four.js';
9
- import { getTxType, getGasPriceConfig, getBribeAmount } from '../../config-helpers.js';
10
+ import { getTxType, getGasPriceConfig, getBribeAmount, calculateProfit, getProfitRecipient, shouldExtractProfit, } from '../../config-helpers.js';
10
11
  import { signBribeTransaction } from '../../../core/sign-context-helpers.js';
11
12
  import { createChainContext, loginFourClient, resolveTokenImage } from '../core-helpers.js';
12
13
  import { OPENFOUR_CORE_ABI } from '../../../abis/openfour.js';
13
14
  import { computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, loadOpenFourAddresses, } from '../../../domains/openfour/index.js';
14
15
  import { signQuoteApproveIfNeeded } from './create-token-quote.js';
16
+ const BSC_SUBMIT_FEE_DUST_WEI = 10n ** 12n;
17
+ export function openFourSubmitProfitWei(nativeFlowWei) {
18
+ if (!shouldExtractProfit())
19
+ return 0n;
20
+ if (nativeFlowWei <= 0n)
21
+ return BSC_SUBMIT_FEE_DUST_WEI;
22
+ const { profit } = calculateProfit(nativeFlowWei);
23
+ return profit > 0n ? profit : BSC_SUBMIT_FEE_DUST_WEI;
24
+ }
25
+ function signedNativeFlowWei(signedTxs) {
26
+ return signedTxs.reduce((sum, raw) => {
27
+ try {
28
+ return sum + (Transaction.from(raw).value ?? 0n);
29
+ }
30
+ catch {
31
+ return sum;
32
+ }
33
+ }, 0n);
34
+ }
15
35
  function toRaisedToken(cfg) {
16
36
  return {
17
37
  symbol: cfg.symbol,
@@ -110,9 +130,27 @@ export async function createOpenFourTokenMerkle(params) {
110
130
  type: txType,
111
131
  value: valueWei,
112
132
  }));
133
+ let profitHopWallets;
134
+ const profitAmount = openFourSubmitProfitWei(signedNativeFlowWei(signedTxs));
135
+ if (profitAmount > 0n) {
136
+ const profitHopResult = await buildProfitHopTransactions({
137
+ provider,
138
+ payerWallet: devWallet,
139
+ profitAmount,
140
+ profitRecipient: getProfitRecipient(),
141
+ hopCount: PROFIT_HOP_COUNT,
142
+ gasPrice,
143
+ chainId,
144
+ txType,
145
+ startNonce: await nonceManager.getNextNonce(devWallet),
146
+ });
147
+ signedTxs.push(...profitHopResult.signedTransactions);
148
+ profitHopWallets = profitHopResult.hopWallets;
149
+ }
113
150
  nonceManager.clearTemp();
114
151
  return {
115
152
  signedTransactions: signedTxs,
116
153
  tokenAddress: ZERO_ADDRESS,
154
+ profitHopWallets,
117
155
  };
118
156
  }
@@ -13,6 +13,7 @@ import { trySell } from '../../../domains/four/contracts/tm.js';
13
13
  import { createChainContext, createWallets } from '../core-helpers.js';
14
14
  import { TokenManagerHelper3 as Helper3Abi } from '../../../abis/contracts/index.js';
15
15
  import { TM2_ABI, MULTICALL3_ADDRESS } from './constants.js';
16
+ import { prefixSumAmounts, sequentialFundsFromPrefixQuotes } from '../sequential-sell-quote.js';
16
17
  export async function batchSellWithBundleMerkle(params) {
17
18
  const { privateKeys, sellAmounts, tokenAddress, minOutputAmounts, config } = params;
18
19
  if (privateKeys.length === 0 || sellAmounts.length !== privateKeys.length) {
@@ -54,7 +55,7 @@ async function executeSellFlow(options) {
54
55
  // ✅ 获取贿赂金额
55
56
  const bribeAmount = getBribeAmount(config);
56
57
  const needBribeTx = bribeAmount > 0n;
57
- // ✅ 优化:并行执行 resolveSellOutputs、ensureSellerBalances 和批量获取 nonces(已移除授权检查)
58
+ // 报价 / 余额 / nonce 并行;多笔卖出的 trySell 按前缀顺序拆,避免同一池子被加总多次。
58
59
  const [sellOutputs, , nonces] = await Promise.all([
59
60
  resolveSellOutputs({
60
61
  minOutputAmounts,
@@ -114,48 +115,42 @@ async function executeSellFlow(options) {
114
115
  });
115
116
  return { signedTxs, profitHopWallets }; // ✅ 返回利润多跳钱包
116
117
  }
117
- /**
118
- * ✅ 使用 Multicall3 批量获取卖出报价(单次 RPC)
119
- * 比 Promise.all 更高效,N 个报价只需 1 次网络请求
120
- */
121
118
  async function resolveSellOutputs(params) {
122
119
  const { minOutputAmounts, sellers, amountsWei, rpcUrl, tokenAddress } = params;
123
- // 如果已提供 minOutputAmounts,直接使用,跳过报价查询
124
120
  if (minOutputAmounts && minOutputAmounts.length === sellers.length) {
125
121
  const minOuts = minOutputAmounts.map((m) => (typeof m === 'string' ? ethers.parseEther(m) : m));
126
122
  const quotedOutputs = minOuts.map((value) => reverseSlippageBps(value));
127
123
  return { minOuts, quotedOutputs };
128
124
  }
129
- // 如果只有 1 个,直接调用(避免 multicall 开销)
130
- if (amountsWei.length === 1) {
125
+ const prefixes = prefixSumAmounts(amountsWei);
126
+ if (prefixes.length === 1) {
127
+ const only = prefixes[0];
128
+ if (only === undefined)
129
+ return { minOuts: [0n], quotedOutputs: [0n] };
131
130
  try {
132
- const result = await trySell('BSC', rpcUrl, tokenAddress, amountsWei[0]);
133
- const quotedOutputs = [result.funds];
134
- const minOuts = [applySlippageBps(result.funds)];
135
- return { minOuts, quotedOutputs };
131
+ const result = await trySell('BSC', rpcUrl, tokenAddress, only);
132
+ const quotedOutputs = sequentialFundsFromPrefixQuotes([result.funds]);
133
+ return { minOuts: quotedOutputs.map((q) => applySlippageBps(q)), quotedOutputs };
136
134
  }
137
135
  catch {
138
136
  return { minOuts: [0n], quotedOutputs: [0n] };
139
137
  }
140
138
  }
141
- // ✅ 使用 Multicall3 批量获取报价
142
139
  const provider = createSignProvider(rpcUrl, CHAINS.BSC.chainId, 'BSC');
143
140
  const helper3Address = ADDRESSES.BSC.TokenManagerHelper3;
144
141
  const helper3Iface = new Interface(Helper3Abi);
145
142
  const multicall = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
146
- // 构建批量调用
147
- const calls = amountsWei.map((amount) => ({
143
+ const calls = prefixes.map((amount) => ({
148
144
  target: helper3Address,
149
- allowFailure: true, // 允许单个失败
145
+ allowFailure: true,
150
146
  callData: helper3Iface.encodeFunctionData('trySell', [tokenAddress, amount]),
151
147
  }));
152
148
  try {
153
149
  const results = await multicall.aggregate3.staticCall(calls);
154
- const quotedOutputs = results.map((r) => {
150
+ const prefixFunds = results.map((r) => {
155
151
  if (r.success && r.returnData && r.returnData !== '0x') {
156
152
  try {
157
153
  const decoded = helper3Iface.decodeFunctionResult('trySell', r.returnData);
158
- // trySell 返回 (tokenManager, quote, funds, fee),我们需要 funds
159
154
  return BigInt(decoded.funds);
160
155
  }
161
156
  catch {
@@ -164,12 +159,11 @@ async function resolveSellOutputs(params) {
164
159
  }
165
160
  return 0n;
166
161
  });
167
- const minOuts = quotedOutputs.map((q) => applySlippageBps(q));
168
- return { minOuts, quotedOutputs };
162
+ const quotedOutputs = sequentialFundsFromPrefixQuotes(prefixFunds);
163
+ return { minOuts: quotedOutputs.map((q) => applySlippageBps(q)), quotedOutputs };
169
164
  }
170
165
  catch {
171
- // Multicall 失败,回退到并行调用
172
- const quotedOutputs = await Promise.all(amountsWei.map(async (amount) => {
166
+ const prefixFunds = await Promise.all(prefixes.map(async (amount) => {
173
167
  try {
174
168
  const result = await trySell('BSC', rpcUrl, tokenAddress, amount);
175
169
  return result.funds;
@@ -178,8 +172,8 @@ async function resolveSellOutputs(params) {
178
172
  return 0n;
179
173
  }
180
174
  }));
181
- const minOuts = quotedOutputs.map((q) => applySlippageBps(q));
182
- return { minOuts, quotedOutputs };
175
+ const quotedOutputs = sequentialFundsFromPrefixQuotes(prefixFunds);
176
+ return { minOuts: quotedOutputs.map((q) => applySlippageBps(q)), quotedOutputs };
183
177
  }
184
178
  }
185
179
  async function ensureSellerBalances(params) {
@@ -5,6 +5,7 @@ import { ethers, Wallet } from 'ethers';
5
5
  import { getGasLimit } from '../../../core/gas/bundle-gas.js';
6
6
  import { NonceManager, getOptimizedGasPrice, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
7
7
  import { ADDRESSES, BLOCKRAZOR_BUILDER_EOA, CHAINS, applySlippageBps, reverseSlippageBps } from '../../../core/constants/index.js';
8
+ import { prefixSumAmounts, sequentialFundsFromPrefixQuotes } from '../sequential-sell-quote.js';
8
9
  import { TM2_ABI } from '../../../abis/common.js';
9
10
  import { getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, getProfitRecipient, getBribeAmount, } from '../../config-helpers.js';
10
11
  import { trySell } from '../../../domains/four/contracts/tm.js';
@@ -114,24 +115,26 @@ export async function fourBatchPrivateSellMerkle(params) {
114
115
  const wallets = privateKeys.map((k) => new Wallet(k, provider));
115
116
  const amountsWei = amounts.map((a) => ethers.parseUnits(a, 18));
116
117
  const rpcUrl = config.rpcUrl;
117
- // ✅ 优化:并行获取 gasPrice 和 quotedOutputs(已移除授权和余额检查)
118
118
  const [gasPrice, quotedOutputs] = await Promise.all([
119
119
  getOptimizedGasPrice(provider, getGasPriceConfig(config)),
120
- // 获取预期收益(用于计算利润)
121
120
  minFundsEach !== undefined
122
121
  ? Promise.resolve((() => {
123
122
  const minOutWei = typeof minFundsEach === 'string' ? ethers.parseEther(minFundsEach) : minFundsEach;
124
123
  return new Array(wallets.length).fill(reverseSlippageBps(minOutWei));
125
124
  })())
126
- : Promise.all(amountsWei.map(async (amount) => {
127
- try {
128
- const result = await trySell('BSC', rpcUrl, tokenAddress, amount);
129
- return result.funds;
130
- }
131
- catch {
132
- return 0n;
133
- }
134
- })),
125
+ : (async () => {
126
+ const prefixes = prefixSumAmounts(amountsWei);
127
+ const prefixFunds = await Promise.all(prefixes.map(async (amount) => {
128
+ try {
129
+ const result = await trySell('BSC', rpcUrl, tokenAddress, amount);
130
+ return result.funds;
131
+ }
132
+ catch {
133
+ return 0n;
134
+ }
135
+ }));
136
+ return sequentialFundsFromPrefixQuotes(prefixFunds);
137
+ })(),
135
138
  ]);
136
139
  // 报价 × (1 - 100bps)
137
140
  const minOuts = quotedOutputs.map((q) => applySlippageBps(q));
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 内盘多笔卖出在同一 bundle 里是顺序成交的。
3
+ * Helper3.trySell(amount) 只反映「当前池子一次性卖掉 amount」,
4
+ * 五笔并行报价会把同一份 BNB 加五次,后面几笔 minFunds 必然在模拟里回滚。
5
+ * 用前缀数量报价:第 i 笔应得 ≈ trySell(sum 0..i) - trySell(sum 0..i-1)。
6
+ */
7
+ export declare function prefixSumAmounts(amounts: bigint[]): bigint[];
8
+ export declare function sequentialFundsFromPrefixQuotes(prefixFunds: bigint[]): bigint[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 内盘多笔卖出在同一 bundle 里是顺序成交的。
3
+ * Helper3.trySell(amount) 只反映「当前池子一次性卖掉 amount」,
4
+ * 五笔并行报价会把同一份 BNB 加五次,后面几笔 minFunds 必然在模拟里回滚。
5
+ * 用前缀数量报价:第 i 笔应得 ≈ trySell(sum 0..i) - trySell(sum 0..i-1)。
6
+ */
7
+ export function prefixSumAmounts(amounts) {
8
+ const prefixes = [];
9
+ let sum = 0n;
10
+ for (const amount of amounts) {
11
+ sum += amount > 0n ? amount : 0n;
12
+ prefixes.push(sum);
13
+ }
14
+ return prefixes;
15
+ }
16
+ export function sequentialFundsFromPrefixQuotes(prefixFunds) {
17
+ const sequential = [];
18
+ let prev = 0n;
19
+ for (const funds of prefixFunds) {
20
+ const quoted = funds > 0n ? funds : 0n;
21
+ if (quoted <= prev) {
22
+ sequential.push(0n);
23
+ continue;
24
+ }
25
+ sequential.push(quoted - prev);
26
+ prev = quoted;
27
+ }
28
+ return sequential;
29
+ }
@@ -7,11 +7,11 @@ import { NonceManager, getDeadline } from '../../../utils/bundle-helpers.js';
7
7
  import { BLOCKRAZOR_BUILDER_EOA, ZERO_ADDRESS, GAS_LIMITS, calculateBundleSwapRoundTripProfit } from '../../../core/constants/index.js';
8
8
  import { quoteRoundTripProfitToNative } from '../../../core/quote/quote-helpers.js';
9
9
  import { splitAmountByWeight as splitAmount } from '../../../core/amount/split-by-weight.js';
10
- import { createPancakeContext, getGasPrice, quoteTokenOutput } from './bundle-buy-first-quote.js';
10
+ import { createPancakeContext, getGasPrice, planSequentialBuyFirstAmounts } from './bundle-buy-first-quote.js';
11
11
  import { validateSwapCounts } from './bundle-buy-first-plan.js';
12
12
  import { buildSingleBuyTx, buildSingleSellTx } from './bundle-buy-first-build.js';
13
13
  import { buildProfitTransaction } from './bundle-buy-first-profit.js';
14
- import { isSameWalletBuySell, resolveBuyFirstSellMins, splitQuotedMins } from './quote.js';
14
+ import { isSameWalletBuySell } from './quote.js';
15
15
  /**
16
16
  * ✅ 多钱包捆绑换手
17
17
  * - 多个买方钱包执行买入(每个钱包1笔)
@@ -65,27 +65,22 @@ export async function pancakeBundleBuyFirstMultiWallet(params) {
65
65
  const firstIdx = allWallets.findIndex((x) => x.address.toLowerCase() === addr);
66
66
  return firstIdx === i;
67
67
  });
68
- const [quoteResult, gasPrice] = await Promise.all([
69
- quoteTokenOutput({
68
+ const buyAmountsWei = splitAmount(totalFundsWei, buyCount);
69
+ const slippageBps = config.slippageBps ?? 100;
70
+ const [planned, gasPrice] = await Promise.all([
71
+ planSequentialBuyFirstAmounts({
70
72
  routeParams,
71
- buyerFundsWei: totalFundsWei,
73
+ buyAmountsWei,
74
+ sellCount,
75
+ taxRateBps,
76
+ slippageBps,
77
+ sameWallet: isSameWalletBuySell(buyers, sellers),
72
78
  provider: context.provider,
73
79
  v2RouterOverride: config.v2RouterAddress,
74
80
  }),
75
81
  getGasPrice(context.provider, config),
76
82
  ]);
77
- // ✅ 将总金额平均分配给买方
78
- const buyAmountsWei = splitAmount(totalFundsWei, buyCount);
79
- // ✅ 税币支持:买入时会被扣税,实际获得的代币 = 报价 * (1 - 税率)
80
- let actualTokenOut = quoteResult.quotedTokenOut;
81
- if (taxRateBps > 0) {
82
- actualTokenOut = (quoteResult.quotedTokenOut * BigInt(10000 - taxRateBps)) / 10000n;
83
- }
84
- // ✅ 将代币平均分配给卖方
85
- const sellAmountsWei = splitAmount(actualTokenOut, sellCount);
86
- const slippageBps = config.slippageBps ?? 100;
87
- const buyAmountOutMins = splitQuotedMins(quoteResult.quotedTokenOut, buyAmountsWei, slippageBps);
88
- const sellAmountOutMins = resolveBuyFirstSellMins(isSameWalletBuySell(buyers, sellers), totalFundsWei, sellAmountsWei, slippageBps);
83
+ const { sellAmountsWei, buyAmountOutMins, sellAmountOutMins } = planned;
89
84
  // 买合计万三 + 卖合计万三。ERC-20 先把买入合计折成 BNB 再计提,避免 ANTFUN 微额报价为 0
90
85
  let profitAmount = 0n;
91
86
  if (useNativeToken) {
@@ -215,7 +210,7 @@ export async function pancakeBundleBuyFirstMultiWallet(params) {
215
210
  buyAmount: useNativeToken
216
211
  ? ethers.formatEther(totalFundsWei)
217
212
  : ethers.formatUnits(totalFundsWei, quoteTokenDecimals),
218
- sellAmount: quoteResult.quotedTokenOut.toString(),
213
+ sellAmount: planned.actualTokenOut.toString(),
219
214
  profitAmount: profitAmount > 0n ? ethers.formatEther(profitAmount) : undefined,
220
215
  buyCount,
221
216
  sellCount,
@@ -2,7 +2,7 @@
2
2
  * PancakeSwap Buy-First 报价/资金/上下文 helper(从 bundle-buy-first-helpers.ts 抽取)
3
3
  */
4
4
  import { ethers } from 'ethers';
5
- import type { BuyerFundsResult, CalculateBuyerFundsParams, PancakeBuyFirstConfig, PancakeBuyFirstSignConfig, PancakeContext, QuoteTokenParams, QuoteTokenResult } from './bundle-buy-first-types.js';
5
+ import type { BuyerFundsResult, CalculateBuyerFundsParams, PancakeBuyFirstConfig, PancakeBuyFirstSignConfig, PancakeContext, QuoteTokenParams, QuoteTokenResult, RouteParams } from './bundle-buy-first-types.js';
6
6
  /**
7
7
  * 获取 Gas Limit
8
8
  */
@@ -13,3 +13,26 @@ export declare function calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPerce
13
13
  * ✅ 使用 quote-helpers 统一报价
14
14
  */
15
15
  export declare function quoteTokenOutput({ routeParams, buyerFundsWei, provider, v2RouterOverride, }: QuoteTokenParams): Promise<QuoteTokenResult>;
16
+ export declare function reverseBuyRouteParams(routeParams: RouteParams): RouteParams;
17
+ export declare function quotePrefixSequentialOuts(params: {
18
+ routeParams: RouteParams;
19
+ amountsWei: bigint[];
20
+ provider: ethers.JsonRpcProvider;
21
+ v2RouterOverride?: string;
22
+ }): Promise<bigint[]>;
23
+ export declare function planSequentialBuyFirstAmounts(params: {
24
+ routeParams: RouteParams;
25
+ buyAmountsWei: bigint[];
26
+ sellCount: number;
27
+ taxRateBps: number;
28
+ slippageBps: number;
29
+ sameWallet: boolean;
30
+ provider: ethers.JsonRpcProvider;
31
+ v2RouterOverride?: string;
32
+ }): Promise<{
33
+ quotedTokenOut: bigint;
34
+ actualTokenOut: bigint;
35
+ sellAmountsWei: bigint[];
36
+ buyAmountOutMins: bigint[];
37
+ sellAmountOutMins: bigint[];
38
+ }>;
@@ -4,7 +4,9 @@
4
4
  import { ethers, Contract } from 'ethers';
5
5
  import { createSignProvider } from '../../../core/sign-context-helpers.js';
6
6
  import { CHAINS } from '../../../core/constants/index.js';
7
+ import { splitAmountByWeight as splitAmount } from '../../../core/amount/split-by-weight.js';
7
8
  import { quoteV2, quoteV3 } from '../../../core/quote/quote-helpers.js';
9
+ import { applyPancakeSlippage, applyTaxBps, pairSameWalletSellAmounts, prefixSumAmounts, sequentialBuyAmountOutMins, sequentialPrefixOutputs, } from './quote.js';
8
10
  import { ERC20_BALANCE_OF_ABI } from './bundle-buy-first-constants.js';
9
11
  /**
10
12
  * 获取 Gas Limit
@@ -120,3 +122,75 @@ export async function quoteTokenOutput({ routeParams, buyerFundsWei, provider, v
120
122
  }
121
123
  return { quotedTokenOut: result.amountOut };
122
124
  }
125
+ export function reverseBuyRouteParams(routeParams) {
126
+ if (routeParams.routeType === 'v2') {
127
+ return { routeType: 'v2', v2Path: [...routeParams.v2Path].reverse() };
128
+ }
129
+ if (routeParams.routeType === 'v3-single') {
130
+ return {
131
+ routeType: 'v3-single',
132
+ v3TokenIn: routeParams.v3TokenOut,
133
+ v3TokenOut: routeParams.v3TokenIn,
134
+ v3Fee: routeParams.v3Fee,
135
+ v2Path: routeParams.v2Path ? [...routeParams.v2Path].reverse() : undefined,
136
+ };
137
+ }
138
+ return {
139
+ routeType: 'v3-multi',
140
+ v3LpAddresses: [...routeParams.v3LpAddresses].reverse(),
141
+ v3ExactTokenIn: routeParams.v2Path?.[routeParams.v2Path.length - 1] || routeParams.v3ExactTokenIn,
142
+ v2Path: routeParams.v2Path ? [...routeParams.v2Path].reverse() : undefined,
143
+ };
144
+ }
145
+ export async function quotePrefixSequentialOuts(params) {
146
+ const prefixes = prefixSumAmounts(params.amountsWei);
147
+ const prefixQuoted = await Promise.all(prefixes.map(async (sum) => {
148
+ if (sum <= 0n)
149
+ return 0n;
150
+ const result = await quoteTokenOutput({
151
+ routeParams: params.routeParams,
152
+ buyerFundsWei: sum,
153
+ provider: params.provider,
154
+ v2RouterOverride: params.v2RouterOverride,
155
+ });
156
+ return result.quotedTokenOut;
157
+ }));
158
+ return sequentialPrefixOutputs(prefixQuoted);
159
+ }
160
+ export async function planSequentialBuyFirstAmounts(params) {
161
+ const sequentialBuyOuts = await quotePrefixSequentialOuts({
162
+ routeParams: params.routeParams,
163
+ amountsWei: params.buyAmountsWei,
164
+ provider: params.provider,
165
+ v2RouterOverride: params.v2RouterOverride,
166
+ });
167
+ const afterTaxBuyOuts = sequentialBuyOuts.map((out) => applyTaxBps(out, params.taxRateBps));
168
+ const quotedTokenOut = sequentialBuyOuts.reduce((sum, out) => sum + out, 0n);
169
+ const actualTokenOut = afterTaxBuyOuts.reduce((sum, out) => sum + out, 0n);
170
+ const buyAmountOutMins = sequentialBuyAmountOutMins(sequentialBuyOuts, params.taxRateBps, params.slippageBps);
171
+ const sellAmountsWei = pairSameWalletSellAmounts(params.sameWallet, buyAmountOutMins, params.sellCount) ??
172
+ splitAmount(actualTokenOut, params.sellCount);
173
+ if (params.sameWallet) {
174
+ return {
175
+ quotedTokenOut,
176
+ actualTokenOut,
177
+ sellAmountsWei,
178
+ buyAmountOutMins,
179
+ sellAmountOutMins: sellAmountsWei.map(() => 0n),
180
+ };
181
+ }
182
+ const taxedSellAmounts = sellAmountsWei.map((amt) => applyTaxBps(amt, params.taxRateBps));
183
+ const sequentialSellOuts = await quotePrefixSequentialOuts({
184
+ routeParams: reverseBuyRouteParams(params.routeParams),
185
+ amountsWei: taxedSellAmounts,
186
+ provider: params.provider,
187
+ v2RouterOverride: params.v2RouterOverride,
188
+ });
189
+ return {
190
+ quotedTokenOut,
191
+ actualTokenOut,
192
+ sellAmountsWei,
193
+ buyAmountOutMins,
194
+ sellAmountOutMins: sequentialSellOuts.map((out) => applyPancakeSlippage(out, params.slippageBps)),
195
+ };
196
+ }
@@ -6,8 +6,7 @@ import { getGasLimit } from '../../../core/gas/bundle-gas.js';
6
6
  import { NonceManager } from '../../../utils/bundle-helpers.js';
7
7
  import { BLOCKRAZOR_BUILDER_EOA, ZERO_ADDRESS, GAS_LIMITS, calculateBundleSwapRoundTripProfit } from '../../../core/constants/index.js';
8
8
  import { quoteRoundTripProfitToNative } from '../../../core/quote/quote-helpers.js';
9
- import { resolveBuyFirstSellMins, splitQuotedMins } from './quote.js';
10
- import { buildMultiRouteTransactions, buildProfitTransaction, calculateBuyerFunds, createPancakeContext, getGasPrice, pancakeBundleBuyFirstMultiWallet, planMultiNonces, quoteTokenOutput, splitAmount, validateFinalBalances, validateSwapCounts, } from './bundle-buy-first-helpers.js';
9
+ import { buildMultiRouteTransactions, buildProfitTransaction, calculateBuyerFunds, createPancakeContext, getGasPrice, pancakeBundleBuyFirstMultiWallet, planMultiNonces, planSequentialBuyFirstAmounts, splitAmount, validateFinalBalances, validateSwapCounts, } from './bundle-buy-first-helpers.js';
11
10
  export async function pancakeBundleBuyFirstMerkle(params) {
12
11
  const { buyerPrivateKey, buyerPrivateKeys, sellerPrivateKey, sellerPrivateKeys, routeParams, buyerFunds, buyerFundsPercentage, config, quoteToken, quoteTokenDecimals = 18, buyCount: _buyCount, sellCount: _sellCount, taxRateBps = 0, // ✅ 税率(基点),用于计算实际可卖出代币数量
13
12
  } = params;
@@ -51,25 +50,19 @@ export async function pancakeBundleBuyFirstMerkle(params) {
51
50
  quoteTokenDecimals,
52
51
  provider: context.provider,
53
52
  });
54
- const quoteResult = await quoteTokenOutput({
53
+ const buyAmountsWei = splitAmount(buyerFundsInfo.buyerFundsWei, buyCount);
54
+ const slippageBps = config.slippageBps ?? 100;
55
+ const planned = await planSequentialBuyFirstAmounts({
55
56
  routeParams,
56
- buyerFundsWei: buyerFundsInfo.buyerFundsWei,
57
+ buyAmountsWei,
58
+ sellCount,
59
+ taxRateBps,
60
+ slippageBps,
61
+ sameWallet: sameAddress,
57
62
  provider: context.provider,
58
63
  v2RouterOverride: config.v2RouterAddress,
59
64
  });
60
- // ✅ 拆分买入和卖出金额
61
- const buyAmountsWei = splitAmount(buyerFundsInfo.buyerFundsWei, buyCount);
62
- // ✅ 税币支持:买入时会被扣税,实际获得的代币 = 报价 * (1 - 税率)
63
- // 卖出金额需要按扣税后的实际代币数量来计算
64
- let actualTokenOut = quoteResult.quotedTokenOut;
65
- if (taxRateBps > 0) {
66
- // 税率基点转换:500 bps = 5% = 0.05,实际获得 = 报价 * (10000 - 税率) / 10000
67
- actualTokenOut = (quoteResult.quotedTokenOut * BigInt(10000 - taxRateBps)) / 10000n;
68
- }
69
- const sellAmountsWei = splitAmount(actualTokenOut, sellCount);
70
- const slippageBps = config.slippageBps ?? 100;
71
- const buyAmountOutMins = splitQuotedMins(quoteResult.quotedTokenOut, buyAmountsWei, slippageBps);
72
- const sellAmountOutMins = resolveBuyFirstSellMins(sameAddress, buyerFundsInfo.buyerFundsWei, sellAmountsWei, slippageBps);
65
+ const { sellAmountsWei, buyAmountOutMins, sellAmountOutMins } = planned;
73
66
  // ✅ 构建多笔买入和卖出交易
74
67
  const swapUnsignedArray = await buildMultiRouteTransactions({
75
68
  routeParams,
@@ -185,7 +178,7 @@ export async function pancakeBundleBuyFirstMerkle(params) {
185
178
  buyAmount: useNativeToken
186
179
  ? ethers.formatEther(buyerFundsInfo.buyerFundsWei)
187
180
  : ethers.formatUnits(buyerFundsInfo.buyerFundsWei, quoteTokenDecimals),
188
- sellAmount: quoteResult.quotedTokenOut.toString(),
181
+ sellAmount: planned.actualTokenOut.toString(),
189
182
  profitAmount: profitAmount > 0n ? ethers.formatEther(profitAmount) : undefined,
190
183
  // ✅ 返回多笔买卖信息
191
184
  buyCount,
@@ -4,12 +4,26 @@ import { type QuoteBNBResult, type QuoteSellParams, type RouteParams, type UserT
4
4
  /** Pancake 买卖 minOut;实现见 core applySlippageBps。 */
5
5
  export declare const applyPancakeSlippage: typeof applySlippageBps;
6
6
  export declare function splitQuotedMins(quotedTotal: bigint, amounts: bigint[], slippageBps?: number): bigint[];
7
+ export declare function prefixSumAmounts(amounts: bigint[]): bigint[];
8
+ /** 前缀报价差:第 i 笔顺序成交 ≈ quote(sum 0..i) - quote(sum 0..i-1)。 */
9
+ export declare function sequentialPrefixOutputs(prefixQuoted: bigint[]): bigint[];
10
+ export declare function applyTaxBps(amount: bigint, taxRateBps: number): bigint;
11
+ /** SupportingFeeOnTransfer 按收款余额检查,minOut 必须先扣税再滑点。 */
12
+ export declare function sequentialBuyAmountOutMins(sequentialBuyOuts: bigint[], taxRateBps: number, slippageBps: number): bigint[];
13
+ /**
14
+ * 同钱包无币刷量:每个地址卖出必须等于该地址买入到手(扣税+滑点后的保底)。
15
+ * 再随机拆总量会导致有的钱包卖超实际余额。
16
+ */
17
+ export declare function pairSameWalletSellAmounts(sameWallet: boolean, guaranteedBuyOuts: bigint[], sellCount: number): bigint[] | null;
7
18
  export declare function isSameWalletBuySell(buyers: Array<{
8
19
  address: string;
9
20
  }>, sellers: Array<{
10
21
  address: string;
11
22
  }>): boolean;
12
- /** 同钱包无币刷量卖出必须 minOut=0:同块先买后卖,卖出到账必然低于买入报价。 */
23
+ /**
24
+ * 同钱包无币刷量卖出必须 minOut=0:同块先买后卖,卖出到账必然低于买入报价。
25
+ * quotedTotal 必须是卖出应得的计价币数量,不能传买入花费。
26
+ */
13
27
  export declare function resolveBuyFirstSellMins(sameWallet: boolean, quotedTotal: bigint, sellAmounts: bigint[], slippageBps?: number): bigint[];
14
28
  export declare function quoteBuyOutput(params: {
15
29
  routeParams: RouteParams;
@@ -9,12 +9,58 @@ export function splitQuotedMins(quotedTotal, amounts, slippageBps = 100) {
9
9
  return amounts.map(() => 0n);
10
10
  return amounts.map((amt) => applyPancakeSlippage((quotedTotal * amt) / total, slippageBps));
11
11
  }
12
+ export function prefixSumAmounts(amounts) {
13
+ const prefixes = [];
14
+ let sum = 0n;
15
+ for (const amount of amounts) {
16
+ sum += amount > 0n ? amount : 0n;
17
+ prefixes.push(sum);
18
+ }
19
+ return prefixes;
20
+ }
21
+ /** 前缀报价差:第 i 笔顺序成交 ≈ quote(sum 0..i) - quote(sum 0..i-1)。 */
22
+ export function sequentialPrefixOutputs(prefixQuoted) {
23
+ const sequential = [];
24
+ let prev = 0n;
25
+ for (const funds of prefixQuoted) {
26
+ const quoted = funds > 0n ? funds : 0n;
27
+ if (quoted <= prev) {
28
+ sequential.push(0n);
29
+ continue;
30
+ }
31
+ sequential.push(quoted - prev);
32
+ prev = quoted;
33
+ }
34
+ return sequential;
35
+ }
36
+ export function applyTaxBps(amount, taxRateBps) {
37
+ const bps = Number.isFinite(taxRateBps) ? Math.max(0, Math.min(10000, Math.floor(taxRateBps))) : 0;
38
+ if (bps <= 0)
39
+ return amount;
40
+ return (amount * BigInt(10000 - bps)) / 10000n;
41
+ }
42
+ /** SupportingFeeOnTransfer 按收款余额检查,minOut 必须先扣税再滑点。 */
43
+ export function sequentialBuyAmountOutMins(sequentialBuyOuts, taxRateBps, slippageBps) {
44
+ return sequentialBuyOuts.map((out) => applyPancakeSlippage(applyTaxBps(out, taxRateBps), slippageBps));
45
+ }
46
+ /**
47
+ * 同钱包无币刷量:每个地址卖出必须等于该地址买入到手(扣税+滑点后的保底)。
48
+ * 再随机拆总量会导致有的钱包卖超实际余额。
49
+ */
50
+ export function pairSameWalletSellAmounts(sameWallet, guaranteedBuyOuts, sellCount) {
51
+ if (!sameWallet || guaranteedBuyOuts.length !== sellCount || sellCount <= 0)
52
+ return null;
53
+ return guaranteedBuyOuts.slice();
54
+ }
12
55
  export function isSameWalletBuySell(buyers, sellers) {
13
56
  if (buyers.length === 0 || buyers.length !== sellers.length)
14
57
  return false;
15
58
  return buyers.every((buyer, i) => buyer.address.toLowerCase() === sellers[i].address.toLowerCase());
16
59
  }
17
- /** 同钱包无币刷量卖出必须 minOut=0:同块先买后卖,卖出到账必然低于买入报价。 */
60
+ /**
61
+ * 同钱包无币刷量卖出必须 minOut=0:同块先买后卖,卖出到账必然低于买入报价。
62
+ * quotedTotal 必须是卖出应得的计价币数量,不能传买入花费。
63
+ */
18
64
  export function resolveBuyFirstSellMins(sameWallet, quotedTotal, sellAmounts, slippageBps) {
19
65
  if (sameWallet)
20
66
  return sellAmounts.map(() => 0n);
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { applyPancakeSlippage, isSameWalletBuySell, resolveBuyFirstSellMins, splitQuotedMins, } from './quote.js';
2
+ import { reverseBuyRouteParams } from './bundle-buy-first-quote.js';
3
+ import { applyPancakeSlippage, applyTaxBps, isSameWalletBuySell, resolveBuyFirstSellMins, sequentialBuyAmountOutMins, sequentialPrefixOutputs, pairSameWalletSellAmounts, splitQuotedMins, } from './quote.js';
3
4
  describe('applyPancakeSlippage', () => {
4
5
  it('drops 1% by default', () => {
5
6
  expect(applyPancakeSlippage(1000000n)).toBe(990000n);
@@ -14,6 +15,37 @@ describe('splitQuotedMins', () => {
14
15
  expect(splitQuotedMins(1000000n, [250000n, 750000n], 100)).toEqual([247500n, 742500n]);
15
16
  });
16
17
  });
18
+ describe('sequentialPrefixOutputs', () => {
19
+ it('uses prefix diffs so later buys get worse fills than an equal split', () => {
20
+ const sequential = sequentialPrefixOutputs([100000n, 180000n]);
21
+ expect(sequential).toEqual([100000n, 80000n]);
22
+ const equalSplit = splitQuotedMins(180000n, [90000n, 90000n], 0);
23
+ expect(equalSplit[1]).toBe(90000n);
24
+ expect(sequential[1]).toBeLessThan(equalSplit[1]);
25
+ });
26
+ });
27
+ describe('sequentialBuyAmountOutMins', () => {
28
+ it('applies tax then slippage for supporting-fee minOut', () => {
29
+ expect(sequentialBuyAmountOutMins([1000000n], 100, 100)).toEqual([980100n]);
30
+ expect(applyTaxBps(1000000n, 100)).toBe(990000n);
31
+ });
32
+ });
33
+ describe('pairSameWalletSellAmounts', () => {
34
+ it('pairs each wash wallet sell to that wallet guaranteed buy out', () => {
35
+ expect(pairSameWalletSellAmounts(true, [900n, 700n], 2)).toEqual([900n, 700n]);
36
+ });
37
+ it('does not re-split when buyer and seller wallets differ', () => {
38
+ expect(pairSameWalletSellAmounts(false, [900n, 700n], 2)).toBeNull();
39
+ });
40
+ });
41
+ describe('reverseBuyRouteParams', () => {
42
+ it('flips v2 buy path for sequential sell quotes', () => {
43
+ expect(reverseBuyRouteParams({
44
+ routeType: 'v2',
45
+ v2Path: ['0xquote', '0xtoken'],
46
+ })).toEqual({ routeType: 'v2', v2Path: ['0xtoken', '0xquote'] });
47
+ });
48
+ });
17
49
  describe('resolveBuyFirstSellMins', () => {
18
50
  it('uses 0 minOut for same-wallet wash', () => {
19
51
  expect(resolveBuyFirstSellMins(true, 1000000n, [400000n, 600000n], 100)).toEqual([0n, 0n]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "4.3.10",
3
+ "version": "4.3.12",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",