four-flap-meme-sdk 4.3.4 → 4.3.6
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/bundle/four-meme/core/create-token.js +22 -9
- package/dist/core/clients/four-client-fallback.test.d.ts +1 -0
- package/dist/core/clients/four-client-fallback.test.js +44 -0
- package/dist/core/clients/four.d.ts +5 -7
- package/dist/core/clients/four.js +52 -54
- package/dist/domains/flap/portal-create-token.js +15 -9
- package/dist/domains/flap/portal-create-token.test.d.ts +1 -0
- package/dist/domains/flap/portal-create-token.test.js +32 -0
- package/package.json +1 -1
|
@@ -12,6 +12,19 @@ import { createChainContext, loginFourClient, resolveTokenImage } from '../core-
|
|
|
12
12
|
import { TM2_ABI } from './constants.js';
|
|
13
13
|
import { fourCreateApiBody, fourCreateValueWei, readLaunchFeeWei, resolveCreateRaisedToken, signQuoteApproveIfNeeded, } from './create-token-quote.js';
|
|
14
14
|
import { isNativeRaisedToken } from '../../../domains/four/raised-token.js';
|
|
15
|
+
function launchProfitIfConfigured(buyAmount, nativeRaised) {
|
|
16
|
+
if (!nativeRaised || buyAmount <= 0n || !shouldExtractProfit())
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const profit = calculateProfit(buyAmount).profit;
|
|
20
|
+
if (profit <= 0n)
|
|
21
|
+
return null;
|
|
22
|
+
return { profit, recipient: getProfitRecipient() };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
15
28
|
export async function createTokenWithBundleBuyMerkle(params) {
|
|
16
29
|
const { privateKeys, buyAmounts, tokenInfo, config } = params;
|
|
17
30
|
if (privateKeys.length !== 1) {
|
|
@@ -31,6 +44,10 @@ export async function createTokenWithBundleBuyMerkle(params) {
|
|
|
31
44
|
? String(tokenInfo.preSale)
|
|
32
45
|
: String(buyAmounts[0] ?? '0');
|
|
33
46
|
const raisedToken = await resolveCreateRaisedToken(fourClient, tokenInfo);
|
|
47
|
+
const originalBuyAmount = isNativeRaisedToken(raisedToken)
|
|
48
|
+
? ethers.parseEther(effectivePreSaleStr || '0')
|
|
49
|
+
: 0n;
|
|
50
|
+
const launchProfit = launchProfitIfConfigured(originalBuyAmount, isNativeRaisedToken(raisedToken));
|
|
34
51
|
const createResp = await fourClient.createToken(accessToken, fourCreateApiBody({
|
|
35
52
|
name: tokenInfo.name,
|
|
36
53
|
shortName: tokenInfo.symbol,
|
|
@@ -51,11 +68,7 @@ export async function createTokenWithBundleBuyMerkle(params) {
|
|
|
51
68
|
const txType = getTxType(config);
|
|
52
69
|
const signedTxs = [];
|
|
53
70
|
const tmCreateAddr = ADDRESSES.BSC.TokenManagerOriginal;
|
|
54
|
-
const
|
|
55
|
-
const originalBuyAmount = isNativeRaisedToken(raisedToken)
|
|
56
|
-
? ethers.parseEther(effectivePreSaleStr || '0')
|
|
57
|
-
: 0n;
|
|
58
|
-
const profitAmount = extractProfit ? calculateProfit(originalBuyAmount).profit : 0n;
|
|
71
|
+
const profitAmount = launchProfit?.profit ?? 0n;
|
|
59
72
|
const bribeAmount = getBribeAmount(config);
|
|
60
73
|
const needBribeTx = bribeAmount > 0n;
|
|
61
74
|
const tmCreate = new ethers.Contract(tmCreateAddr, TM2_ABI, devWallet);
|
|
@@ -92,13 +105,13 @@ export async function createTokenWithBundleBuyMerkle(params) {
|
|
|
92
105
|
};
|
|
93
106
|
signedTxs.push(await devWallet.signTransaction(createTxRequest));
|
|
94
107
|
let profitHopWallets;
|
|
95
|
-
if (
|
|
108
|
+
if (launchProfit && profitAmount > 0n) {
|
|
96
109
|
const profitNonce = await nonceManager.getNextNonce(devWallet);
|
|
97
110
|
const profitHopResult = await buildProfitHopTransactions({
|
|
98
111
|
provider,
|
|
99
112
|
payerWallet: devWallet,
|
|
100
113
|
profitAmount,
|
|
101
|
-
profitRecipient:
|
|
114
|
+
profitRecipient: launchProfit.recipient,
|
|
102
115
|
hopCount: PROFIT_HOP_COUNT,
|
|
103
116
|
gasPrice,
|
|
104
117
|
chainId,
|
|
@@ -109,11 +122,11 @@ export async function createTokenWithBundleBuyMerkle(params) {
|
|
|
109
122
|
profitHopWallets = profitHopResult.hopWallets;
|
|
110
123
|
}
|
|
111
124
|
nonceManager.clearTemp();
|
|
112
|
-
const metadata =
|
|
125
|
+
const metadata = launchProfit && profitAmount > 0n
|
|
113
126
|
? {
|
|
114
127
|
totalBuyAmount: ethers.formatEther(originalBuyAmount),
|
|
115
128
|
profitAmount: ethers.formatEther(profitAmount),
|
|
116
|
-
profitRecipient:
|
|
129
|
+
profitRecipient: launchProfit.recipient,
|
|
117
130
|
buyerCount: 1,
|
|
118
131
|
}
|
|
119
132
|
: undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { FourClient } from './four.js';
|
|
3
|
+
describe('FourClient private API rate-limit fallback', () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.unstubAllGlobals();
|
|
6
|
+
});
|
|
7
|
+
it('retries generateNonce on the official API after -1013', async () => {
|
|
8
|
+
const calls = [];
|
|
9
|
+
vi.stubGlobal('fetch', vi.fn(async (input) => {
|
|
10
|
+
const url = String(input);
|
|
11
|
+
calls.push(url);
|
|
12
|
+
if (url.includes('bscfourapi.emit.tools')) {
|
|
13
|
+
return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests, banned for 15 minutes' }), {
|
|
14
|
+
status: 429,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return new Response(JSON.stringify({ code: 0, data: 'nonce-1' }));
|
|
18
|
+
}));
|
|
19
|
+
const nonce = await new FourClient().generateNonce({
|
|
20
|
+
accountAddress: `0x${'11'.repeat(20)}`,
|
|
21
|
+
verifyType: 'LOGIN',
|
|
22
|
+
networkCode: 'BSC',
|
|
23
|
+
});
|
|
24
|
+
expect(nonce).toBe('nonce-1');
|
|
25
|
+
expect(calls.some((url) => url.includes('four.meme'))).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it('retries generateNonce when the proxy returns HTML', async () => {
|
|
28
|
+
vi.stubGlobal('fetch', vi.fn(async (input) => {
|
|
29
|
+
const url = String(input);
|
|
30
|
+
if (url.includes('bscfourapi.emit.tools')) {
|
|
31
|
+
return new Response('<!doctype html><html></html>', {
|
|
32
|
+
status: 200,
|
|
33
|
+
headers: { 'Content-Type': 'text/html' },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return new Response(JSON.stringify({ code: 0, data: 'nonce-2' }));
|
|
37
|
+
}));
|
|
38
|
+
await expect(new FourClient().generateNonce({
|
|
39
|
+
accountAddress: `0x${'11'.repeat(20)}`,
|
|
40
|
+
verifyType: 'LOGIN',
|
|
41
|
+
networkCode: 'BSC',
|
|
42
|
+
})).resolves.toBe('nonce-2');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -11,10 +11,8 @@ export type FourConfig = {
|
|
|
11
11
|
*/
|
|
12
12
|
baseUrl?: string;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 当主 baseUrl 触发 IP 限流(code -1038)时,自动依次尝试备用端点。
|
|
17
|
-
* 每个 URL 格式同 baseUrl,例如 'https://bscfourapi2.emit.tools'
|
|
14
|
+
* 图片上传 / 登录 / 发币 API 备用 URL。
|
|
15
|
+
* 主 baseUrl 触发 -1038 / -1013 时依次尝试。
|
|
18
16
|
*/
|
|
19
17
|
uploadFallbackUrls?: string[];
|
|
20
18
|
};
|
|
@@ -113,6 +111,8 @@ export declare class FourClient {
|
|
|
113
111
|
private baseUrl;
|
|
114
112
|
private uploadUrls;
|
|
115
113
|
constructor(cfg?: FourConfig);
|
|
114
|
+
private attachApiCode;
|
|
115
|
+
private postPrivateJson;
|
|
116
116
|
generateNonce(req: GenerateNonceReq): Promise<string>;
|
|
117
117
|
loginDex(body: LoginReq): Promise<string>;
|
|
118
118
|
/**
|
|
@@ -127,15 +127,13 @@ export declare class FourClient {
|
|
|
127
127
|
* 执行单次上传请求
|
|
128
128
|
*/
|
|
129
129
|
private doUpload;
|
|
130
|
-
/**
|
|
131
|
-
* 判断是否为 IP 限流错误(code: -1038)
|
|
132
|
-
*/
|
|
133
130
|
private isRateLimitError;
|
|
134
131
|
/**
|
|
135
132
|
* 判断是否为 CORS 或网络错误
|
|
136
133
|
* 浏览器直连官方 API 时常见(fetch 会抛出 TypeError: Failed to fetch)
|
|
137
134
|
*/
|
|
138
135
|
private isCorsOrNetworkError;
|
|
136
|
+
private isInvalidJsonError;
|
|
139
137
|
private getFilenameFromBlob;
|
|
140
138
|
createToken(accessToken: string, req: CreateTokenReq): Promise<CreateTokenResp>;
|
|
141
139
|
getPublicConfig(): Promise<FourPublicConfig>;
|
|
@@ -107,13 +107,11 @@ async function hashBlob(blob) {
|
|
|
107
107
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
108
108
|
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
109
109
|
}
|
|
110
|
-
/**
|
|
111
|
-
const
|
|
110
|
+
/** Four.meme 限流:-1038 每日上传配额,-1013 15 分钟封禁 */
|
|
111
|
+
const RATE_LIMIT_CODES = new Set([-1038, -1013]);
|
|
112
112
|
/**
|
|
113
|
-
* Four.meme 官方 API
|
|
114
|
-
*
|
|
115
|
-
* - 在 Node.js/服务端环境中:无 CORS 限制,直连走用户自己的 IP,不消耗 Worker 配额
|
|
116
|
-
* - 在浏览器环境中:可能被 CORS 拦截,但作为最后的 fallback 仍值得尝试
|
|
113
|
+
* Four.meme 官方 API。nonce/login 浏览器可直连;create/upload 需 `meme-web-access`,
|
|
114
|
+
* 官方 CORS 不放行该头,浏览器应走自有代理。
|
|
117
115
|
*/
|
|
118
116
|
const FOUR_MEME_OFFICIAL_API = 'https://four.meme/meme-api';
|
|
119
117
|
export class FourClient {
|
|
@@ -133,30 +131,49 @@ export class FourClient {
|
|
|
133
131
|
// 去重(如果用户 baseUrl 就是官方地址,避免重复请求)
|
|
134
132
|
this.uploadUrls = [...new Set(this.uploadUrls)];
|
|
135
133
|
}
|
|
134
|
+
attachApiCode(err, code) {
|
|
135
|
+
const n = Number(code);
|
|
136
|
+
if (Number.isFinite(n))
|
|
137
|
+
err.apiCode = n;
|
|
138
|
+
return err;
|
|
139
|
+
}
|
|
140
|
+
async postPrivateJson(path, failLabel, init) {
|
|
141
|
+
let lastError = null;
|
|
142
|
+
for (let i = 0; i < this.uploadUrls.length; i++) {
|
|
143
|
+
const baseUrl = this.uploadUrls[i];
|
|
144
|
+
try {
|
|
145
|
+
const r = await fetch(`${baseUrl}${path}`, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: { 'Content-Type': 'application/json', ...(init.headers || {}) },
|
|
148
|
+
body: init.body,
|
|
149
|
+
});
|
|
150
|
+
const j = (await r.json());
|
|
151
|
+
if (j.code !== '0' && j.code !== 0) {
|
|
152
|
+
throw this.attachApiCode(new Error(`${failLabel}: ${JSON.stringify(j)}`), j.code);
|
|
153
|
+
}
|
|
154
|
+
return j.data;
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
const err = e instanceof Error ? e : new Error(getErrorMessageFromUnknown(e));
|
|
158
|
+
lastError = err;
|
|
159
|
+
const canRetry = i < this.uploadUrls.length - 1;
|
|
160
|
+
if (canRetry && (this.isRateLimitError(err) || this.isCorsOrNetworkError(err) || this.isInvalidJsonError(err))) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw lastError ?? new Error(failLabel);
|
|
167
|
+
}
|
|
136
168
|
async generateNonce(req) {
|
|
137
|
-
|
|
138
|
-
method: 'POST',
|
|
139
|
-
headers: { 'Content-Type': 'application/json' },
|
|
169
|
+
return this.postPrivateJson('/v1/private/user/nonce/generate', 'generateNonce failed', {
|
|
140
170
|
body: JSON.stringify(req),
|
|
141
171
|
});
|
|
142
|
-
const j = await r.json();
|
|
143
|
-
// code 可能是字符串 '0' 或数字 0
|
|
144
|
-
if (j.code !== '0' && j.code !== 0) {
|
|
145
|
-
throw new Error(`generateNonce failed: ${JSON.stringify(j)}`);
|
|
146
|
-
}
|
|
147
|
-
return j.data;
|
|
148
172
|
}
|
|
149
173
|
async loginDex(body) {
|
|
150
|
-
|
|
151
|
-
method: 'POST',
|
|
152
|
-
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
return this.postPrivateJson('/v1/private/user/login/dex', 'login failed', {
|
|
153
175
|
body: JSON.stringify(body),
|
|
154
176
|
});
|
|
155
|
-
const j = await r.json();
|
|
156
|
-
if (j.code !== '0' && j.code !== 0) {
|
|
157
|
-
throw new Error(`login failed: ${JSON.stringify(j)}`);
|
|
158
|
-
}
|
|
159
|
-
return j.data; // access_token
|
|
160
177
|
}
|
|
161
178
|
/**
|
|
162
179
|
* 上传图片到 Four.meme
|
|
@@ -189,10 +206,6 @@ export class FourClient {
|
|
|
189
206
|
lastError = err;
|
|
190
207
|
const canRetry = i < this.uploadUrls.length - 1;
|
|
191
208
|
if (this.isRateLimitError(err) && canRetry) {
|
|
192
|
-
// IP 限流 → 切下一个端点
|
|
193
|
-
const nextUrl = this.uploadUrls[i + 1];
|
|
194
|
-
const nextLabel = nextUrl === FOUR_MEME_OFFICIAL_API ? '官方直连' : `备用端点 #${i + 2}`;
|
|
195
|
-
console.warn(`[FourClient] 端点 ${baseUrl} 触发 IP 限流(100次/天),切换到${nextLabel}...`);
|
|
196
209
|
continue;
|
|
197
210
|
}
|
|
198
211
|
if (this.isCorsOrNetworkError(err) && canRetry) {
|
|
@@ -207,8 +220,7 @@ export class FourClient {
|
|
|
207
220
|
// 所有端点都失败
|
|
208
221
|
const isRateLimit = lastError && this.isRateLimitError(lastError);
|
|
209
222
|
if (isRateLimit) {
|
|
210
|
-
throw new Error(
|
|
211
|
-
`请等待次日重置,或在 FourConfig.uploadFallbackUrls 中添加不同 IP 的备用代理。` +
|
|
223
|
+
throw new Error(`图片上传失败:Four.meme 限流(每日配额或 15 分钟封禁)。请稍后再试。` +
|
|
212
224
|
`\n原始错误: ${getErrorMessageFromUnknown(lastError)}`);
|
|
213
225
|
}
|
|
214
226
|
if (lastError !== null && lastError !== undefined)
|
|
@@ -234,17 +246,12 @@ export class FourClient {
|
|
|
234
246
|
}
|
|
235
247
|
return j.data; // imgUrl
|
|
236
248
|
}
|
|
237
|
-
/**
|
|
238
|
-
* 判断是否为 IP 限流错误(code: -1038)
|
|
239
|
-
*/
|
|
240
249
|
isRateLimitError(e) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
'apiCode' in e &&
|
|
244
|
-
e.apiCode === RATE_LIMIT_CODE) {
|
|
250
|
+
const code = Number(e.apiCode);
|
|
251
|
+
if (RATE_LIMIT_CODES.has(code))
|
|
245
252
|
return true;
|
|
246
|
-
|
|
247
|
-
return
|
|
253
|
+
const msg = getErrorMessageFromUnknown(e);
|
|
254
|
+
return RATE_LIMIT_CODES.has(Number(msg.match(/"code"\s*:\s*(-?\d+)/)?.[1])) || /too many requests|banned for/i.test(msg);
|
|
248
255
|
}
|
|
249
256
|
/**
|
|
250
257
|
* 判断是否为 CORS 或网络错误
|
|
@@ -256,6 +263,9 @@ export class FourClient {
|
|
|
256
263
|
const msg = getErrorMessageFromUnknown(e).toLowerCase();
|
|
257
264
|
return msg.includes('failed to fetch') || msg.includes('network') || msg.includes('cors');
|
|
258
265
|
}
|
|
266
|
+
isInvalidJsonError(e) {
|
|
267
|
+
return e instanceof SyntaxError || /unexpected token|not valid json/i.test(getErrorMessageFromUnknown(e));
|
|
268
|
+
}
|
|
259
269
|
getFilenameFromBlob(blob) {
|
|
260
270
|
const mimeToExt = {
|
|
261
271
|
'image/jpeg': 'jpg',
|
|
@@ -271,16 +281,10 @@ export class FourClient {
|
|
|
271
281
|
async createToken(accessToken, req) {
|
|
272
282
|
const raisedToken = req.raisedToken ?? (await this.resolveRaisedToken(req.symbol));
|
|
273
283
|
const body = buildCreateTokenReq(req, raisedToken);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
headers: { 'Content-Type': 'application/json', 'meme-web-access': accessToken },
|
|
284
|
+
return this.postPrivateJson('/v1/private/token/create', 'createToken failed', {
|
|
285
|
+
headers: { 'meme-web-access': accessToken },
|
|
277
286
|
body: JSON.stringify(body),
|
|
278
287
|
});
|
|
279
|
-
const j = await r.json();
|
|
280
|
-
if (j.code !== '0' && j.code !== 0) {
|
|
281
|
-
throw new Error(`createToken failed: ${JSON.stringify(j)}`);
|
|
282
|
-
}
|
|
283
|
-
return j.data;
|
|
284
288
|
}
|
|
285
289
|
async getPublicConfig() {
|
|
286
290
|
const urls = [...new Set([this.baseUrl, FOUR_MEME_OFFICIAL_API])];
|
|
@@ -392,16 +396,10 @@ export class FourClient {
|
|
|
392
396
|
return last;
|
|
393
397
|
}
|
|
394
398
|
async createOpenFourToken(accessToken, req) {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
headers: { 'Content-Type': 'application/json', 'meme-web-access': accessToken },
|
|
399
|
+
const data = await this.postPrivateJson('/v1/private/token_template/token/create', 'createOpenFourToken failed', {
|
|
400
|
+
headers: { 'meme-web-access': accessToken },
|
|
398
401
|
body: JSON.stringify(req),
|
|
399
402
|
});
|
|
400
|
-
const j = await r.json();
|
|
401
|
-
if (j.code !== '0' && j.code !== 0) {
|
|
402
|
-
throw new Error(`createOpenFourToken failed: ${JSON.stringify(j)}`);
|
|
403
|
-
}
|
|
404
|
-
const data = j.data;
|
|
405
403
|
const first = Array.isArray(data) ? data[0] : data;
|
|
406
404
|
return first;
|
|
407
405
|
}
|
|
@@ -97,7 +97,12 @@ function resolveCreateQuoteAmt(params) {
|
|
|
97
97
|
function resolveUseV7Portal(params) {
|
|
98
98
|
return !!(params.useV7Portal ?? params.useV4Portal ?? params.useClInfinityLaunch);
|
|
99
99
|
}
|
|
100
|
-
/**
|
|
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
|
|
110
|
-
|
|
114
|
+
const quoteToken = params.quoteToken || ZERO_ADDRESS;
|
|
115
|
+
const dividendFeeConfig = {
|
|
116
|
+
feeType: FeeType.DIVIDEND,
|
|
111
117
|
bps: 10000,
|
|
112
|
-
marketingAddress:
|
|
113
|
-
dividendToken:
|
|
114
|
-
minimumShareBalance:
|
|
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
|
|
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: [
|
|
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 —
|
|
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
|
+
});
|