four-flap-meme-sdk 4.3.5 → 4.3.7

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.
@@ -24,6 +24,30 @@ describe('FourClient private API rate-limit fallback', () => {
24
24
  expect(nonce).toBe('nonce-1');
25
25
  expect(calls.some((url) => url.includes('four.meme'))).toBe(true);
26
26
  });
27
+ it('retries generateNonce on bscfourapi after custom baseUrl -1013', async () => {
28
+ const calls = [];
29
+ vi.stubGlobal('fetch', vi.fn(async (input) => {
30
+ const url = String(input);
31
+ calls.push(url);
32
+ if (url.includes('example.com')) {
33
+ return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests, banned for 15 minutes, remain 493s' }), { status: 429 });
34
+ }
35
+ if (url.includes('bscfourapi.emit.tools')) {
36
+ return new Response(JSON.stringify({ code: 0, data: 'nonce-proxy' }));
37
+ }
38
+ return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests, banned for 15 minutes' }), {
39
+ status: 429,
40
+ });
41
+ }));
42
+ const nonce = await new FourClient({ baseUrl: 'https://example.com/four-meme-api' }).generateNonce({
43
+ accountAddress: `0x${'11'.repeat(20)}`,
44
+ verifyType: 'LOGIN',
45
+ networkCode: 'BSC',
46
+ });
47
+ expect(nonce).toBe('nonce-proxy');
48
+ expect(calls.some((url) => url.includes('example.com'))).toBe(true);
49
+ expect(calls.some((url) => url.includes('bscfourapi.emit.tools'))).toBe(true);
50
+ });
27
51
  it('retries generateNonce when the proxy returns HTML', async () => {
28
52
  vi.stubGlobal('fetch', vi.fn(async (input) => {
29
53
  const url = String(input);
@@ -109,6 +109,8 @@ 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,主线路被封时换这条) */
113
+ const FOUR_MEME_PROXY_API = 'https://bscfourapi.emit.tools';
112
114
  /**
113
115
  * Four.meme 官方 API。nonce/login 浏览器可直连;create/upload 需 `meme-web-access`,
114
116
  * 官方 CORS 不放行该头,浏览器应走自有代理。
@@ -119,17 +121,11 @@ export class FourClient {
119
121
  uploadUrls;
120
122
  constructor(cfg) {
121
123
  // 默认使用 Cloudflare Workers 代理,已配置 CORS,浏览器可直接使用
122
- this.baseUrl = cfg?.baseUrl || 'https://bscfourapi.emit.tools';
123
- // 上传端点列表:主 URL + 用户自定义备用 URL + 官方 API(兜底)
124
- const fallbacks = cfg?.uploadFallbackUrls || [];
124
+ this.baseUrl = cfg?.baseUrl || FOUR_MEME_PROXY_API;
125
+ // 主 URL → 自定义备用 → emit 代理(换 IP)→ 官方直连(Node 无 CORS)
125
126
  this.uploadUrls = [
126
- this.baseUrl,
127
- ...fallbacks,
128
- // ✅ 官方 API 始终作为最后兜底(Node.js 环境无 CORS 限制,走用户自己 IP)
129
- FOUR_MEME_OFFICIAL_API,
127
+ ...new Set([this.baseUrl, ...(cfg?.uploadFallbackUrls || []), FOUR_MEME_PROXY_API, FOUR_MEME_OFFICIAL_API]),
130
128
  ];
131
- // 去重(如果用户 baseUrl 就是官方地址,避免重复请求)
132
- this.uploadUrls = [...new Set(this.uploadUrls)];
133
129
  }
134
130
  attachApiCode(err, code) {
135
131
  const n = Number(code);
@@ -287,7 +283,7 @@ export class FourClient {
287
283
  });
288
284
  }
289
285
  async getPublicConfig() {
290
- const urls = [...new Set([this.baseUrl, FOUR_MEME_OFFICIAL_API])];
286
+ const urls = this.uploadUrls;
291
287
  let last;
292
288
  for (const url of urls) {
293
289
  try {
@@ -344,7 +340,7 @@ export class FourClient {
344
340
  return (j.data ?? j);
345
341
  }
346
342
  async searchTokenTemplates() {
347
- const urls = [...new Set([this.baseUrl, FOUR_MEME_OFFICIAL_API])];
343
+ const urls = this.uploadUrls;
348
344
  let last = [];
349
345
  for (const url of urls) {
350
346
  try {
@@ -374,7 +370,7 @@ export class FourClient {
374
370
  return list.filter((t) => String(t.status || '').toUpperCase() === 'PUBLISHED');
375
371
  }
376
372
  async getTokenTemplateConfig(templateId) {
377
- const urls = [...new Set([this.baseUrl, FOUR_MEME_OFFICIAL_API])];
373
+ const urls = this.uploadUrls;
378
374
  let last = [];
379
375
  for (const url of urls) {
380
376
  try {
@@ -97,7 +97,12 @@ function resolveCreateQuoteAmt(params) {
97
97
  function resolveUseV7Portal(params) {
98
98
  return !!(params.useV7Portal ?? params.useV4Portal ?? params.useClInfinityLaunch);
99
99
  }
100
- /** Flap 文档:TOKEN_V3_PERMIT 须 migratorType=3、feeConfigs 单槽 MARKETING_OR_VAULT 10000 bps */
100
+ /**
101
+ * 标准 V3 / TOKEN_V3_PERMIT:对齐 Flap 官网 live newTokenV7。
102
+ * 主网 Portal v5.23.1 拒 MARKETING_OR_VAULT(NewTokenV7RuleViolation(8)),
103
+ * 只收单槽 DIVIDEND 10000 bps、dividendToken == quoteToken(原生为 address(0))。
104
+ */
105
+ const V7_DIVIDEND_MIN_SHARE = 10000n * 10n ** 18n;
101
106
  function buildNewTokenV7PermitParams(params) {
102
107
  const emptyFeeConfig = {
103
108
  feeType: FeeType.NONE,
@@ -106,12 +111,13 @@ function buildNewTokenV7PermitParams(params) {
106
111
  dividendToken: ZeroAddress,
107
112
  minimumShareBalance: 0n,
108
113
  };
109
- const marketingFeeConfig = {
110
- feeType: FeeType.MARKETING_OR_VAULT,
114
+ const quoteToken = params.quoteToken || ZERO_ADDRESS;
115
+ const dividendFeeConfig = {
116
+ feeType: FeeType.DIVIDEND,
111
117
  bps: 10000,
112
- marketingAddress: params.beneficiary,
113
- dividendToken: ZeroAddress,
114
- minimumShareBalance: 0n,
118
+ marketingAddress: ZeroAddress,
119
+ dividendToken: quoteToken,
120
+ minimumShareBalance: V7_DIVIDEND_MIN_SHARE,
115
121
  };
116
122
  return {
117
123
  name: params.tokenInfo.name,
@@ -120,7 +126,7 @@ function buildNewTokenV7PermitParams(params) {
120
126
  dexThresh: (params.dexThresh ?? 1) & 0xff,
121
127
  salt: params.salt ?? zeroBytes32(),
122
128
  migratorType: MigratorType.PCS_INFINITY_CL_MIGRATOR,
123
- quoteToken: params.quoteToken || ZERO_ADDRESS,
129
+ quoteToken,
124
130
  quoteAmt: resolveCreateQuoteAmt(params),
125
131
  permitData: '0x',
126
132
  extensionID: params.extensionID ?? zeroBytes32(),
@@ -132,7 +138,7 @@ function buildNewTokenV7PermitParams(params) {
132
138
  antiFarmerDuration: 0n,
133
139
  commissionReceiver: ZeroAddress,
134
140
  tokenVersion: TokenVersion.TOKEN_V3_PERMIT,
135
- feeConfigs: [marketingFeeConfig, emptyFeeConfig, emptyFeeConfig, emptyFeeConfig],
141
+ feeConfigs: [dividendFeeConfig, emptyFeeConfig, emptyFeeConfig, emptyFeeConfig],
136
142
  };
137
143
  }
138
144
  export async function populateCreateTokenTransaction(params) {
@@ -197,7 +203,7 @@ export async function populateCreateTokenTransaction(params) {
197
203
  sellTaxRate,
198
204
  }, { mktBps: 10000, deflationBps: 0, dividendBps: 0, lpBps: 0 }));
199
205
  }
200
- // 3) 标准 V3 / TOKEN_V3_PERMIT(newTokenV7 — 与 Flap 官网 / Portal v5.8.6+ 一致;V4 已 FeatureDisabled)
206
+ // 3) 标准 V3 / TOKEN_V3_PERMIT(newTokenV7 — 对齐官网 live 单槽 DIVIDEND;V4 已 FeatureDisabled)
201
207
  if (resolveUseV7Portal(params)) {
202
208
  return params.originalPortal.newTokenV7.populateTransaction(buildNewTokenV7PermitParams(params));
203
209
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { Contract, Interface, Wallet, ZeroAddress } from 'ethers';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { FLAP_PORTAL_ABI } from '../../abis/flap/portal-merkle.js';
4
+ import { ZERO_ADDRESS } from '../../core/constants/index.js';
5
+ import { FeeType, MigratorType, TokenVersion } from './portal.js';
6
+ import { populateCreateTokenTransaction } from './portal-create-token.js';
7
+ describe('populateCreateTokenTransaction V7 tax-free', () => {
8
+ it('uses DIVIDEND 10000 bps like flap.sh, not MARKETING_OR_VAULT', async () => {
9
+ const wallet = Wallet.createRandom();
10
+ const portal = new Contract(ZeroAddress, FLAP_PORTAL_ABI, wallet);
11
+ const tx = await populateCreateTokenTransaction({
12
+ chain: 'BSC',
13
+ devWallet: wallet,
14
+ originalPortal: portal,
15
+ tokenInfo: { name: 'zzz', symbol: 'zzz', meta: 'cid' },
16
+ beneficiary: wallet.address,
17
+ useV7Portal: true,
18
+ });
19
+ const parsed = new Interface(FLAP_PORTAL_ABI).parseTransaction({ data: tx.data });
20
+ expect(parsed?.name).toBe('newTokenV7');
21
+ const p = parsed.args[0];
22
+ expect(Number(p[5])).toBe(MigratorType.PCS_INFINITY_CL_MIGRATOR);
23
+ expect(Number(p[17])).toBe(TokenVersion.TOKEN_V3_PERMIT);
24
+ expect(p[16]).toBe(ZERO_ADDRESS);
25
+ const fee0 = p[18][0];
26
+ expect(Number(fee0[0])).toBe(FeeType.DIVIDEND);
27
+ expect(Number(fee0[1])).toBe(10000);
28
+ expect(fee0[2]).toBe(ZERO_ADDRESS);
29
+ expect(fee0[3]).toBe(ZERO_ADDRESS);
30
+ expect(fee0[4]).toBe(10000n * 10n ** 18n);
31
+ });
32
+ });
@@ -63,4 +63,13 @@ describe('flapV8TaxBps', () => {
63
63
  sellTaxRate: 500n,
64
64
  }))).toBe(500);
65
65
  });
66
+ it('keeps graduated flap 1 percent tax', () => {
67
+ expect(flapV8TaxBps(v8({
68
+ status: TokenStatus.DEX,
69
+ pool: '0x17Ae5fb34fE0198Ecac25f210B25bA67A2d6a746',
70
+ quoteTokenAddress: '0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1',
71
+ buyTaxRate: 100n,
72
+ sellTaxRate: 100n,
73
+ }))).toBe(100);
74
+ });
66
75
  });
@@ -342,13 +342,15 @@ export async function inspectTokenLP(token, opts) {
342
342
  if (opts.flapChain === 'BSC') {
343
343
  const stV8 = await flap.getTokenV8Safe(token);
344
344
  taxRate = flapV8TaxBps(stV8);
345
+ // V8 已把「无池残留 100」收成 0;毕业盘真实 1%(100 bps)要留下
346
+ if (taxRate > 0)
347
+ result.taxRate = taxRate;
345
348
  }
346
349
  else {
347
350
  const stV7 = await flap.getTokenV7(token);
348
351
  taxRate = Number(stV7.taxRate ?? 0n);
349
- }
350
- if (taxRate > 0 && taxRate !== 100) {
351
- result.taxRate = taxRate;
352
+ if (taxRate > 0 && taxRate !== 100)
353
+ result.taxRate = taxRate;
352
354
  }
353
355
  }
354
356
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "4.3.5",
3
+ "version": "4.3.7",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",