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.
- package/dist/chains/bsc/pancake/bundle-buy-first-multi-wallet.js +7 -8
- package/dist/core/nonce/nonce-manager.d.ts +1 -0
- package/dist/core/nonce/nonce-manager.js +30 -16
- package/dist/domains/lp-inspect/inspect-tax.d.ts +6 -0
- package/dist/domains/lp-inspect/inspect-tax.js +82 -0
- package/dist/domains/lp-inspect/inspect-tax.test.d.ts +1 -0
- package/dist/domains/lp-inspect/inspect-tax.test.js +63 -0
- package/dist/domains/lp-inspect/inspect.js +12 -34
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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.
|
|
328
|
+
// 4. 外盘税率:合约 view 优先,Flap Portal 只认真实毕业盘
|
|
355
329
|
// ========================================
|
|
356
|
-
|
|
357
|
-
|
|
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;
|