four-flap-meme-sdk 4.3.1 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,7 +57,7 @@ export async function pancakeBundleBuyFirstMultiWallet(params) {
57
57
  const deadline = BigInt(getDeadline());
58
58
  const bribeAmount = config.bribeAmount && config.bribeAmount > 0 ? ethers.parseEther(String(config.bribeAmount)) : 0n;
59
59
  const needBribeTx = bribeAmount > 0n;
60
- // ✅ 第一批并行:报价 + Gas价格 + 所有钱包 nonces
60
+ // 报价 + Gas 并行;nonce 必须在报价之后再取,避免 quote 期间链上 nonce 已前进
61
61
  // 预先计算所有唯一钱包地址(去重)
62
62
  const allWallets = [...sellers, ...buyers];
63
63
  const uniqueWallets = allWallets.filter((w, i) => {
@@ -65,7 +65,7 @@ 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, noncesArray] = await Promise.all([
68
+ const [quoteResult, gasPrice] = await Promise.all([
69
69
  quoteTokenOutput({
70
70
  routeParams,
71
71
  buyerFundsWei: totalFundsWei,
@@ -73,13 +73,7 @@ export async function pancakeBundleBuyFirstMultiWallet(params) {
73
73
  v2RouterOverride: config.v2RouterAddress,
74
74
  }),
75
75
  getGasPrice(context.provider, config),
76
- nonceManager.getNextNoncesForWallets(uniqueWallets),
77
76
  ]);
78
- // 构建 nonces Map
79
- const noncesMap = new Map();
80
- uniqueWallets.forEach((wallet, i) => {
81
- noncesMap.set(wallet.address.toLowerCase(), noncesArray[i]);
82
- });
83
77
  // ✅ 将总金额平均分配给买方
84
78
  const buyAmountsWei = splitAmount(totalFundsWei, buyCount);
85
79
  // ✅ 税币支持:买入时会被扣税,实际获得的代币 = 报价 * (1 - 税率)
@@ -102,6 +96,11 @@ export async function pancakeBundleBuyFirstMultiWallet(params) {
102
96
  const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
103
97
  profitAmount = await quoteRoundTripProfitToNative(context.provider, quoteToken, totalFundsWei, totalFundsWei, 'BSC', version, fee, config.v2RouterAddress);
104
98
  }
99
+ const noncesArray = await nonceManager.getNextNoncesForWallets(uniqueWallets);
100
+ const noncesMap = new Map();
101
+ uniqueWallets.forEach((wallet, i) => {
102
+ noncesMap.set(wallet.address.toLowerCase(), noncesArray[i]);
103
+ });
105
104
  // ✅ 第三批并行:构建并签名所有交易
106
105
  const allTransactions = [];
107
106
  // 1. 贿赂交易(由主卖方支付)- 先处理以确定 nonce 偏移
@@ -11,6 +11,7 @@ export declare class NonceManager {
11
11
  constructor(provider: JsonRpcProvider);
12
12
  private withAddressLock;
13
13
  private getChainId;
14
+ private readOnchainNonce;
14
15
  getNextNonce(wallet: Wallet | string): Promise<number>;
15
16
  getNextNonceBatch(wallet: Wallet | string, count: number): Promise<number[]>;
16
17
  clearTemp(): void;
@@ -77,6 +77,13 @@ export class NonceManager {
77
77
  }
78
78
  return this.chainIdPromise;
79
79
  }
80
+ async readOnchainNonce(address) {
81
+ const [latest, pending] = await Promise.all([
82
+ this.provider.getTransactionCount(address, 'latest'),
83
+ this.provider.getTransactionCount(address, 'pending'),
84
+ ]);
85
+ return Math.max(Number(latest) || 0, Number(pending) || 0);
86
+ }
80
87
  async getNextNonce(wallet) {
81
88
  const address = typeof wallet === 'string' ? wallet : wallet.address;
82
89
  const key = address.toLowerCase();
@@ -90,7 +97,7 @@ export class NonceManager {
90
97
  setGlobalNextNonce(chainId, key, cachedNonce + 1);
91
98
  return cachedNonce;
92
99
  }
93
- const onchainNonce = await this.provider.getTransactionCount(address, 'pending');
100
+ const onchainNonce = await this.readOnchainNonce(address);
94
101
  let effectiveNonce = onchainNonce;
95
102
  if (flaky) {
96
103
  const globalNext = getGlobalNextNonce(chainId, key);
@@ -118,7 +125,7 @@ export class NonceManager {
118
125
  startNonce = cachedNonce;
119
126
  }
120
127
  else {
121
- startNonce = await this.provider.getTransactionCount(address, 'pending');
128
+ startNonce = await this.readOnchainNonce(address);
122
129
  }
123
130
  if (flaky) {
124
131
  const globalNext = getGlobalNextNonce(chainId, key);
@@ -170,23 +177,30 @@ export class NonceManager {
170
177
  let queryResults;
171
178
  const uniqueQueries = Array.from(needQueryByKey.values()).map((entries) => entries[0]);
172
179
  try {
173
- const batchRequests = uniqueQueries.map(({ address }, idx) => ({
174
- method: 'eth_getTransactionCount',
175
- params: [address, 'pending'],
176
- id: idx + 1,
177
- jsonrpc: '2.0',
178
- }));
180
+ const batchRequests = uniqueQueries.flatMap(({ address }, idx) => [
181
+ {
182
+ method: 'eth_getTransactionCount',
183
+ params: [address, 'latest'],
184
+ id: idx * 2 + 1,
185
+ jsonrpc: '2.0',
186
+ },
187
+ {
188
+ method: 'eth_getTransactionCount',
189
+ params: [address, 'pending'],
190
+ id: idx * 2 + 2,
191
+ jsonrpc: '2.0',
192
+ },
193
+ ]);
179
194
  const rawResults = await asBatchSendProvider(this.provider)._send(batchRequests);
180
- queryResults = rawResults.map((r) => {
181
- if (r.result) {
182
- return parseInt(r.result, 16);
183
- }
184
- return 0;
185
- });
195
+ const byId = new Map();
196
+ for (const row of rawResults) {
197
+ const id = Number(row.id);
198
+ byId.set(id, row.result ? parseInt(row.result, 16) : 0);
199
+ }
200
+ queryResults = uniqueQueries.map((_, idx) => Math.max(byId.get(idx * 2 + 1) || 0, byId.get(idx * 2 + 2) || 0));
186
201
  }
187
202
  catch {
188
- const queryPromises = uniqueQueries.map(({ address }) => this.provider.getTransactionCount(address, 'pending'));
189
- queryResults = await Promise.all(queryPromises);
203
+ queryResults = await Promise.all(uniqueQueries.map(({ address }) => this.readOnchainNonce(address)));
190
204
  }
191
205
  for (let i = 0; i < uniqueQueries.length; i++) {
192
206
  const { address } = uniqueQueries[i];
@@ -0,0 +1,6 @@
1
+ import { type TokenStateV8 } from '../flap/portal.js';
2
+ export declare function isFlapInnerMarket(status: unknown): boolean;
3
+ /** 1–10 当百分数;≤10000 当 bps(Four feeRate 10000=100%)。 */
4
+ export declare function normalizeFeeToBps(raw: number): number | undefined;
5
+ export declare function flapV8TaxBps(st: TokenStateV8): number;
6
+ export declare function readBscOuterTaxBps(token: string, rpcUrl: string): Promise<number | undefined>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * LP inspect 税率:Flap 真实税 / Four TaxToken / 外盘常见 view。
3
+ * 非 Flap 币 Portal 槽位常残留 100 bps,不能当 Pancake 税币税率。
4
+ */
5
+ import { Contract } from 'ethers';
6
+ import { ZERO_ADDRESS } from '../../core/constants/index.js';
7
+ import { TokenStatus } from '../flap/portal.js';
8
+ import { getTaxTokenInfo, isTaxToken } from '../four/tax-token.js';
9
+ import { getProvider } from './inspect-helpers.js';
10
+ const GENERIC_TAX_FNS = [
11
+ 'feeRate',
12
+ 'buyTax',
13
+ 'sellTax',
14
+ 'buyFee',
15
+ 'sellFee',
16
+ 'taxFee',
17
+ 'totalBuyFee',
18
+ 'totalSellFee',
19
+ ];
20
+ const GENERIC_TAX_ABI = GENERIC_TAX_FNS.map((name) => `function ${name}() view returns (uint256)`);
21
+ export function isFlapInnerMarket(status) {
22
+ const n = Number(status);
23
+ return n === TokenStatus.Tradable || n === TokenStatus.InDuel;
24
+ }
25
+ function isZeroAddress(value) {
26
+ const s = String(value || '').toLowerCase();
27
+ return !s || s === ZERO_ADDRESS.toLowerCase();
28
+ }
29
+ /** 1–10 当百分数;≤10000 当 bps(Four feeRate 10000=100%)。 */
30
+ export function normalizeFeeToBps(raw) {
31
+ if (!Number.isFinite(raw) || raw <= 0)
32
+ return undefined;
33
+ if (raw <= 10)
34
+ return Math.round(raw * 100);
35
+ if (raw <= 10000)
36
+ return Math.round(raw);
37
+ if (raw > 1e14 && raw <= 1e18) {
38
+ const bps = Math.round((raw / 1e18) * 10000);
39
+ return bps > 0 && bps <= 10000 ? bps : undefined;
40
+ }
41
+ return undefined;
42
+ }
43
+ export function flapV8TaxBps(st) {
44
+ const status = Number(st.status);
45
+ if (status === TokenStatus.Tradable || status === TokenStatus.InDuel) {
46
+ return Math.max(Number(st.buyTaxRate ?? 0n), Number(st.sellTaxRate ?? 0n));
47
+ }
48
+ if (status !== TokenStatus.DEX)
49
+ return 0;
50
+ if (isZeroAddress(st.pool) || isZeroAddress(st.quoteTokenAddress))
51
+ return 0;
52
+ return Math.max(Number(st.buyTaxRate ?? 0n), Number(st.sellTaxRate ?? 0n));
53
+ }
54
+ async function readGenericTokenTaxBps(token, provider) {
55
+ const tokenContract = new Contract(token, GENERIC_TAX_ABI, provider);
56
+ const reads = await Promise.allSettled(GENERIC_TAX_FNS.map(async (name) => {
57
+ const value = await tokenContract.getFunction(name).staticCall();
58
+ return normalizeFeeToBps(Number(value));
59
+ }));
60
+ let maxBps = 0;
61
+ for (const item of reads) {
62
+ if (item.status !== 'fulfilled' || typeof item.value !== 'number')
63
+ continue;
64
+ if (item.value > maxBps)
65
+ maxBps = item.value;
66
+ }
67
+ return maxBps > 0 ? maxBps : undefined;
68
+ }
69
+ export async function readBscOuterTaxBps(token, rpcUrl) {
70
+ try {
71
+ if (await isTaxToken(token, rpcUrl)) {
72
+ const fee = Number((await getTaxTokenInfo(token, rpcUrl)).config.feeRate);
73
+ const bps = normalizeFeeToBps(fee);
74
+ if (bps)
75
+ return bps;
76
+ }
77
+ }
78
+ catch {
79
+ /* 非 Four TaxToken */
80
+ }
81
+ return readGenericTokenTaxBps(token, getProvider(rpcUrl));
82
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ZERO_ADDRESS } from '../../core/constants/index.js';
3
+ import { TokenStatus } from '../flap/portal.js';
4
+ import { flapV8TaxBps, isFlapInnerMarket, normalizeFeeToBps } from './inspect-tax.js';
5
+ function v8(partial) {
6
+ return {
7
+ reserve: 0n,
8
+ circulatingSupply: 0n,
9
+ price: 0n,
10
+ tokenVersion: 6,
11
+ r: 0n,
12
+ h: 0n,
13
+ k: 0n,
14
+ dexSupplyThresh: 0n,
15
+ quoteTokenAddress: ZERO_ADDRESS,
16
+ nativeToQuoteSwapEnabled: false,
17
+ extensionID: '0x0000000000000000000000000000000000000000000000000000000000000000',
18
+ buyTaxRate: 0n,
19
+ sellTaxRate: 0n,
20
+ taxRate: 0n,
21
+ pool: ZERO_ADDRESS,
22
+ progress: 0n,
23
+ lpFeeProfile: 0,
24
+ dexId: 0,
25
+ ...partial,
26
+ };
27
+ }
28
+ describe('normalizeFeeToBps', () => {
29
+ it('treats 1–10 as percent', () => {
30
+ expect(normalizeFeeToBps(5)).toBe(500);
31
+ expect(normalizeFeeToBps(1)).toBe(100);
32
+ });
33
+ it('keeps Four feeRate / bps as-is up to 10000', () => {
34
+ expect(normalizeFeeToBps(500)).toBe(500);
35
+ expect(normalizeFeeToBps(0)).toBeUndefined();
36
+ });
37
+ });
38
+ describe('flapV8TaxBps', () => {
39
+ it('reads inner flap tax', () => {
40
+ expect(isFlapInnerMarket(TokenStatus.Tradable)).toBe(true);
41
+ expect(flapV8TaxBps(v8({
42
+ status: TokenStatus.Tradable,
43
+ buyTaxRate: 100n,
44
+ sellTaxRate: 500n,
45
+ }))).toBe(500);
46
+ });
47
+ it('ignores DEX leftover 100 bps when pool/quote are zero', () => {
48
+ expect(flapV8TaxBps(v8({
49
+ status: TokenStatus.DEX,
50
+ buyTaxRate: 100n,
51
+ sellTaxRate: 100n,
52
+ }))).toBe(0);
53
+ });
54
+ it('keeps graduated flap tax when pool and quote exist', () => {
55
+ expect(flapV8TaxBps(v8({
56
+ status: TokenStatus.DEX,
57
+ pool: '0x1111111111111111111111111111111111111111',
58
+ quoteTokenAddress: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
59
+ buyTaxRate: 100n,
60
+ sellTaxRate: 500n,
61
+ }))).toBe(500);
62
+ });
63
+ });
@@ -7,30 +7,12 @@
7
7
  import { Contract, formatEther } from 'ethers';
8
8
  import { ADDRESSES, ZERO_ADDRESS } from '../../core/constants/index.js';
9
9
  import { Helper3 } from '../four/contracts/helper3.js';
10
- import { FlapPortal, TokenStatus, } from '../flap/portal.js';
11
- import { getTaxTokenInfo, isTaxToken } from '../four/tax-token.js';
10
+ import { FlapPortal } from '../flap/portal.js';
12
11
  import { DEFAULT_V3_FEE_TIERS } from './lp-inspect-helpers.js';
13
12
  import { getChainConfig } from './registry.js';
14
13
  import { determinePlatform, formatBalance, getProvider, getTokenDecimals, normalizeLiquidity, queryV2Pairs, queryV3Pools, } from './inspect-helpers.js';
15
- // ============================================================================
16
- // 常量(从公共模块导入)
17
- // ============================================================================
14
+ import { flapV8TaxBps, isFlapInnerMarket, readBscOuterTaxBps } from './inspect-tax.js';
18
15
  const MULTICALL3_ADDRESS = ADDRESSES.BSC.Multicall3;
19
- /** 内盘可交易:1=Tradable,2=InDuel。0=无效(非 Flap 币常见)、3=已杀死、4=已毕业外盘,都不能当内盘提前返回。 */
20
- function isFlapInnerMarket(status) {
21
- const n = Number(status);
22
- return n === TokenStatus.Tradable || n === TokenStatus.InDuel;
23
- }
24
- /** BSC V8 买卖税。只有真实 Flap 状态才读买卖税,避免非 Flap 币 slot 残留 100 bps。 */
25
- function flapV8TaxBps(st) {
26
- const status = Number(st.status);
27
- if (status !== TokenStatus.Tradable &&
28
- status !== TokenStatus.InDuel &&
29
- status !== TokenStatus.DEX) {
30
- return 0;
31
- }
32
- return Math.max(Number(st.buyTaxRate ?? 0n), Number(st.sellTaxRate ?? 0n));
33
- }
34
16
  // ============================================================================
35
17
  // 主查询函数
36
18
  // ============================================================================
@@ -207,17 +189,9 @@ export async function inspectTokenLP(token, opts) {
207
189
  const offerTokens = formatBalance(info.offers, shouldFormat);
208
190
  const lastPrice = formatBalance(info.lastPrice, shouldFormat);
209
191
  result.four = { helper: ADDRESSES.BSC.TokenManagerHelper3, reserveNative, offerTokens, lastPrice };
210
- try {
211
- if (await isTaxToken(token, opts.rpcUrl)) {
212
- const fee = Number((await getTaxTokenInfo(token, opts.rpcUrl)).config.feeRate);
213
- const taxRate = fee > 0 && fee <= 10 ? fee * 100 : fee;
214
- if (taxRate > 0)
215
- result.taxRate = taxRate;
216
- }
217
- }
218
- catch {
219
- // 非 TaxToken 或读费率失败
220
- }
192
+ const innerTax = await readBscOuterTaxBps(token, opts.rpcUrl);
193
+ if (innerTax)
194
+ result.taxRate = innerTax;
221
195
  return result;
222
196
  }
223
197
  }
@@ -351,10 +325,14 @@ export async function inspectTokenLP(token, opts) {
351
325
  // 确定平台类型
352
326
  result.platform = determinePlatform(result.allPools);
353
327
  // ========================================
354
- // 4. 外盘税率查询(仅 Flap 链)
328
+ // 4. 外盘税率:合约 view 优先,Flap Portal 只认真实毕业盘
355
329
  // ========================================
356
- // 对于已毕业到外盘的代币,尝试从 Flap Portal 获取税率
357
- if (opts.flapChain && !result.flap && result.platform !== 'UNKNOWN') {
330
+ if (opts.chain === 'BSC' && !result.taxRate) {
331
+ const outerTax = await readBscOuterTaxBps(token, opts.rpcUrl);
332
+ if (outerTax)
333
+ result.taxRate = outerTax;
334
+ }
335
+ if (opts.flapChain && !result.flap && !result.taxRate && result.platform !== 'UNKNOWN') {
358
336
  try {
359
337
  const flap = new FlapPortal({ chain: opts.flapChain, rpcUrl: opts.rpcUrl });
360
338
  let taxRate = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "4.3.1",
3
+ "version": "4.3.2",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",