four-flap-meme-sdk 4.3.9 → 4.3.11

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.
@@ -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
+ }
@@ -10,7 +10,7 @@ describe('FourClient private API rate-limit fallback', () => {
10
10
  vi.stubGlobal('fetch', vi.fn(async (input) => {
11
11
  const url = String(input);
12
12
  calls.push(url);
13
- if (url.includes('bscfourapi.emit.tools')) {
13
+ if (url.includes('bscfourapi.emit.tools') || url.includes('api.emit.tools')) {
14
14
  return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests, banned for 15 minutes' }), {
15
15
  status: 429,
16
16
  });
@@ -49,6 +49,25 @@ describe('FourClient private API rate-limit fallback', () => {
49
49
  expect(calls.some((url) => url.includes('example.com'))).toBe(true);
50
50
  expect(calls.some((url) => url.includes('bscfourapi.emit.tools'))).toBe(true);
51
51
  });
52
+ it('retries generateNonce on EC2 after Cloudflare proxies are banned', async () => {
53
+ const calls = [];
54
+ vi.stubGlobal('fetch', vi.fn(async (input) => {
55
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : String(input);
56
+ calls.push(url);
57
+ if (url.includes('api.emit.tools')) {
58
+ return new Response(JSON.stringify({ code: 0, data: 'nonce-ec2' }));
59
+ }
60
+ return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests, banned for 15 minutes' }), {
61
+ status: 429,
62
+ });
63
+ }));
64
+ await expect(new FourClient().generateNonce({
65
+ accountAddress: `0x${'11'.repeat(20)}`,
66
+ verifyType: 'LOGIN',
67
+ networkCode: 'BSC',
68
+ })).resolves.toBe('nonce-ec2');
69
+ expect(calls.some((url) => url.includes('api.emit.tools'))).toBe(true);
70
+ });
52
71
  it('retries generateNonce when the proxy returns HTML', async () => {
53
72
  vi.stubGlobal('fetch', vi.fn(async (input) => {
54
73
  const url = String(input);
@@ -58,6 +77,11 @@ describe('FourClient private API rate-limit fallback', () => {
58
77
  headers: { 'Content-Type': 'text/html' },
59
78
  });
60
79
  }
80
+ if (url.includes('api.emit.tools')) {
81
+ return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests, banned for 15 minutes' }), {
82
+ status: 429,
83
+ });
84
+ }
61
85
  return new Response(JSON.stringify({ code: 0, data: 'nonce-2' }));
62
86
  }));
63
87
  await expect(new FourClient().generateNonce({
@@ -109,8 +109,10 @@ async function hashBlob(blob) {
109
109
  }
110
110
  /** Four.meme 限流:-1038 每日上传配额,-1013 15 分钟封禁 */
111
111
  const RATE_LIMIT_CODES = new Set([-1038, -1013]);
112
- /** Cloudflare Worker 代理(独立出口 IP,主线路被封时换这条) */
112
+ /** Cloudflare Worker 代理(与 Pages 同源反代同属 CF 出口,可能连坐) */
113
113
  const FOUR_MEME_PROXY_API = 'https://bscfourapi.emit.tools';
114
+ /** emit-api EC2 反代(新加坡出口,CF 被 -1013 后换这条) */
115
+ const FOUR_MEME_EC2_PROXY_API = 'https://api.emit.tools/v1/four-meme-api';
114
116
  /**
115
117
  * Four.meme 官方 API。浏览器走这条才是用户自己的 IP,不会被共用代理连坐封禁。
116
118
  * 官网 CORS 允许 Authorization,不允许 meme-web-access;浏览器直连时改用前者。
@@ -121,7 +123,13 @@ export class FourClient {
121
123
  uploadUrls;
122
124
  constructor(cfg) {
123
125
  this.baseUrl = cfg?.baseUrl || FOUR_MEME_PROXY_API;
124
- const urls = [this.baseUrl, ...(cfg?.uploadFallbackUrls || []), FOUR_MEME_PROXY_API, FOUR_MEME_OFFICIAL_API];
126
+ const urls = [
127
+ this.baseUrl,
128
+ ...(cfg?.uploadFallbackUrls || []),
129
+ FOUR_MEME_PROXY_API,
130
+ FOUR_MEME_EC2_PROXY_API,
131
+ FOUR_MEME_OFFICIAL_API,
132
+ ];
125
133
  // 浏览器先打官网(用户 IP);Node 仍先走调用方传入的代理
126
134
  this.uploadUrls = this.isBrowserEnv()
127
135
  ? [...new Set([FOUR_MEME_OFFICIAL_API, ...urls])]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "4.3.9",
3
+ "version": "4.3.11",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",