@four-meme/four-meme-ai 1.0.4 → 1.0.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.
@@ -1,251 +1,321 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Four.meme - create token API flow (nonce → login → upload image → create).
4
- * Outputs createArg and signature (hex) for use with create-token-chain.ts.
5
- *
6
- * Usage:
7
- * npx tsx create-token-api.ts <imagePath> <name> <shortName> <desc> <label> [taxOptions.json]
8
- *
9
- * Env: PRIVATE_KEY (wallet private key, no 0x prefix ok)
10
- * Optional env: WEB_URL, TWITTER_URL, TELEGRAM_URL, PRE_SALE ("0"), FEE_PLAN ("false")
11
- * Tax token: pass path to a JSON file with "tokenTaxInfo" as last arg, or set TAX_TOKEN=1 and
12
- * TAX_FEE_RATE (1|3|5|10), TAX_BURN_RATE, TAX_DIVIDE_RATE, TAX_LIQUIDITY_RATE, TAX_RECIPIENT_RATE,
13
- * TAX_RECIPIENT_ADDRESS, TAX_MIN_SHARING (e.g. 100000). burn+divide+liquidity+recipient must = 100.
14
- *
15
- * Labels: Meme | AI | Defi | Games | Infra | De-Sci | Social | Depin | Charity | Others
16
- */
17
-
18
- import { privateKeyToAccount } from 'viem/accounts';
19
- import { readFileSync, existsSync } from 'node:fs';
20
- import { basename } from 'node:path';
21
-
22
- const API_BASE = 'https://four.meme/meme-api/v1';
23
- const NETWORK_CODE = 'BSC';
24
-
25
- function toHex(value: string): string {
26
- if (value.startsWith('0x')) return value;
27
- if (/^[0-9a-fA-F]+$/.test(value)) return '0x' + value;
28
- const buf = Buffer.from(value, 'base64');
29
- return '0x' + buf.toString('hex');
30
- }
31
-
32
- async function main() {
33
- const privateKey = process.env.PRIVATE_KEY;
34
- if (!privateKey) {
35
- console.error('Set PRIVATE_KEY');
36
- process.exit(1);
37
- }
38
- const pk = privateKey.startsWith('0x') ? (privateKey as `0x${string}`) : (`0x${privateKey}` as `0x${string}`);
39
- const account = privateKeyToAccount(pk);
40
- const address = account.address;
41
-
42
- const imagePath = process.argv[2];
43
- const name = process.argv[3];
44
- const shortName = process.argv[4];
45
- const desc = process.argv[5];
46
- const label = process.argv[6];
47
- const taxOptionsPath = process.argv[7]; // optional JSON file with tokenTaxInfo
48
-
49
- if (!imagePath || !name || !shortName || !desc || !label) {
50
- console.error(
51
- 'Usage: npx tsx create-token-api.ts <imagePath> <name> <shortName> <desc> <label> [taxOptions.json]'
52
- );
53
- console.error('Example: npx tsx create-token-api.ts ./logo.png MyToken MTK "My desc" AI');
54
- console.error('Tax token: add path to JSON with tokenTaxInfo, or use TAX_* env vars (see SKILL.md).');
55
- process.exit(1);
56
- }
57
- if (!existsSync(imagePath)) {
58
- console.error('Image file not found:', imagePath);
59
- process.exit(1);
60
- }
61
-
62
- const validLabels = ['Meme', 'AI', 'Defi', 'Games', 'Infra', 'De-Sci', 'Social', 'Depin', 'Charity', 'Others'];
63
- const labelNorm = validLabels.find((l) => l.toLowerCase() === label.toLowerCase());
64
- if (!labelNorm) {
65
- console.error('Invalid label. Use one of:', validLabels.join(', '));
66
- process.exit(1);
67
- }
68
- const labelCanonical = labelNorm; // API 要求与列表完全一致(含大小写)
69
-
70
- // 1. Get nonce
71
- const nonceRes = await fetch(`${API_BASE}/private/user/nonce/generate`, {
72
- method: 'POST',
73
- headers: { 'Content-Type': 'application/json' },
74
- body: JSON.stringify({
75
- accountAddress: address,
76
- verifyType: 'LOGIN',
77
- networkCode: NETWORK_CODE,
78
- }),
79
- });
80
- const nonceData = await nonceRes.json();
81
- if (nonceData.code !== '0' && nonceData.code !== 0) {
82
- throw new Error('Nonce failed: ' + JSON.stringify(nonceData));
83
- }
84
- const nonce = nonceData.data;
85
-
86
- // 2. Sign and login
87
- const message = `You are sign in Meme ${nonce}`;
88
- const signature = await account.signMessage({ message });
89
-
90
- const loginRes = await fetch(`${API_BASE}/private/user/login/dex`, {
91
- method: 'POST',
92
- headers: { 'Content-Type': 'application/json' },
93
- body: JSON.stringify({
94
- region: 'WEB',
95
- langType: 'EN',
96
- loginIp: '',
97
- inviteCode: '',
98
- verifyInfo: {
99
- address,
100
- networkCode: NETWORK_CODE,
101
- signature,
102
- verifyType: 'LOGIN',
103
- },
104
- walletName: 'MetaMask',
105
- }),
106
- });
107
- const loginData = await loginRes.json();
108
- if (loginData.code !== '0' && loginData.code !== 0) {
109
- throw new Error('Login failed: ' + JSON.stringify(loginData));
110
- }
111
- const accessToken = loginData.data;
112
-
113
- // 3. Upload image
114
- const imageBuffer = readFileSync(imagePath);
115
- const form = new FormData();
116
- form.append('file', new Blob([imageBuffer]), basename(imagePath));
117
-
118
- const uploadRes = await fetch(`${API_BASE}/private/token/upload`, {
119
- method: 'POST',
120
- headers: { 'meme-web-access': accessToken },
121
- body: form as unknown as BodyInit,
122
- });
123
- const uploadData = await uploadRes.json();
124
- if (uploadData.code !== '0' && uploadData.code !== 0) {
125
- throw new Error('Upload failed: ' + JSON.stringify(uploadData));
126
- }
127
- const imgUrl = uploadData.data;
128
-
129
- // 4. Public config for raisedToken (data[]: symbol, symbolAddress, totalBAmount, status=PUBLISH|INIT, ...)
130
- const configRes = await fetch(`${API_BASE}/public/config`);
131
- if (!configRes.ok) {
132
- throw new Error('Public config request failed: ' + configRes.status + ' ' + configRes.statusText);
133
- }
134
- const configData = await configRes.json();
135
- if (configData.code !== '0' && configData.code !== 0) {
136
- throw new Error('Invalid public config response: ' + JSON.stringify(configData));
137
- }
138
- const symbols = configData.data;
139
- if (!Array.isArray(symbols) || symbols.length === 0) {
140
- throw new Error('Invalid public config (no raisedToken): ' + JSON.stringify(configData));
141
- }
142
- // Prefer BNB with status PUBLISH for BSC; else first PUBLISH; else first item
143
- const published = symbols.filter((c: { status?: string }) => c.status === 'PUBLISH');
144
- const list = published.length > 0 ? published : symbols;
145
- const config =
146
- list.find((c: { symbol?: string }) => c.symbol === 'BNB') ?? list[0];
147
- const raisedToken = config;
148
- if (!raisedToken || !raisedToken.symbol) {
149
- throw new Error('Invalid public config (no raisedToken): ' + JSON.stringify(configData));
150
- }
151
-
152
- // 5. Build create body and optional tokenTaxInfo
153
- // raisedAmount / totalSupply / saleRate 等固定参数从 raisedToken 或文档固定值对齐 API-CreateToken.02-02-2026.md
154
- const launchTime = Date.now();
155
- const totalSupply =
156
- typeof (raisedToken as { totalAmount?: string | number }).totalAmount !== 'undefined'
157
- ? Number((raisedToken as { totalAmount?: string | number }).totalAmount)
158
- : 1000000000;
159
- const raisedAmount =
160
- typeof (raisedToken as { totalBAmount?: string | number }).totalBAmount !== 'undefined'
161
- ? Number((raisedToken as { totalBAmount?: string | number }).totalBAmount)
162
- : 24;
163
- const body: Record<string, unknown> = {
164
- name,
165
- shortName,
166
- desc,
167
- totalSupply,
168
- raisedAmount,
169
- saleRate:
170
- typeof (raisedToken as { saleRate?: string | number }).saleRate !== 'undefined'
171
- ? Number((raisedToken as { saleRate?: string | number }).saleRate)
172
- : 0.8,
173
- reserveRate: 0,
174
- imgUrl,
175
- raisedToken,
176
- launchTime,
177
- funGroup: false,
178
- label: labelCanonical,
179
- lpTradingFee: 0.0025,
180
- webUrl: process.env.WEB_URL ?? '',
181
- twitterUrl: process.env.TWITTER_URL ?? '',
182
- telegramUrl: process.env.TELEGRAM_URL ?? '',
183
- preSale: process.env.PRE_SALE ?? '0',
184
- clickFun: false,
185
- symbol: (raisedToken as { symbol: string }).symbol,
186
- dexType: 'PANCAKE_SWAP',
187
- rushMode: false,
188
- onlyMPC: false,
189
- feePlan: process.env.FEE_PLAN === 'true',
190
- };
191
-
192
- let tokenTaxInfo: Record<string, unknown> | null = null;
193
- if (taxOptionsPath && taxOptionsPath.endsWith('.json') && existsSync(taxOptionsPath)) {
194
- const taxOpts = JSON.parse(readFileSync(taxOptionsPath, 'utf8'));
195
- if (taxOpts.tokenTaxInfo && typeof taxOpts.tokenTaxInfo === 'object') {
196
- tokenTaxInfo = taxOpts.tokenTaxInfo as Record<string, unknown>;
197
- }
198
- }
199
- if (!tokenTaxInfo && process.env.TAX_TOKEN === '1') {
200
- const feeRate = Number(process.env.TAX_FEE_RATE ?? 5);
201
- const burnRate = Number(process.env.TAX_BURN_RATE ?? 0);
202
- const divideRate = Number(process.env.TAX_DIVIDE_RATE ?? 0);
203
- const liquidityRate = Number(process.env.TAX_LIQUIDITY_RATE ?? 100);
204
- const recipientRate = Number(process.env.TAX_RECIPIENT_RATE ?? 0);
205
- const recipientAddress = process.env.TAX_RECIPIENT_ADDRESS ?? '';
206
- const minSharing = Number(process.env.TAX_MIN_SHARING ?? 100000);
207
- const sum = burnRate + divideRate + liquidityRate + recipientRate;
208
- if (sum !== 100) {
209
- throw new Error(`Tax rates must sum to 100 (burn+divide+liquidity+recipient). Got ${sum}.`);
210
- }
211
- if (![1, 3, 5, 10].includes(feeRate)) {
212
- throw new Error('TAX_FEE_RATE must be 1, 3, 5, or 10.');
213
- }
214
- tokenTaxInfo = {
215
- feeRate,
216
- burnRate,
217
- divideRate,
218
- liquidityRate,
219
- recipientRate,
220
- recipientAddress,
221
- minSharing,
222
- };
223
- }
224
- if (tokenTaxInfo) {
225
- body.tokenTaxInfo = tokenTaxInfo;
226
- }
227
-
228
- const createRes = await fetch(`${API_BASE}/private/token/create`, {
229
- method: 'POST',
230
- headers: {
231
- 'meme-web-access': accessToken,
232
- 'Content-Type': 'application/json',
233
- },
234
- body: JSON.stringify(body),
235
- });
236
- const createData = await createRes.json();
237
- if (createData.code !== '0' && createData.code !== 0) {
238
- throw new Error('Create API failed: ' + JSON.stringify(createData));
239
- }
240
- const { createArg: rawArg, signature: rawSig } = createData.data;
241
- const createArgHex = toHex(rawArg);
242
- const signatureHex = toHex(rawSig);
243
-
244
- const out = { createArg: createArgHex, signature: signatureHex };
245
- console.log(JSON.stringify(out, null, 2));
246
- }
247
-
248
- main().catch((e) => {
249
- console.error(e.message || e);
250
- process.exit(1);
251
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Four.meme - create token API flow (nonce → login → upload image → create).
4
+ * Outputs createArg and signature (hex) for use with create-token-chain.ts.
5
+ *
6
+ * Usage: all options as --key=value
7
+ * npx tsx create-token-api.ts --image=./logo.png --name=MyToken --short-name=MTK --desc="My desc" --label=AI [options]
8
+ *
9
+ * Required: --image= --name= --short-name= --desc= --label=
10
+ * Optional: --web-url= --twitter-url= --telegram-url= (omit if empty); --pre-sale=0 (in BNB/ether, e.g. 0.001); --fee-plan=false --tax-options=<path>
11
+ * Tax token: --tax-options=tax.json or --tax-token --tax-fee-rate=5 ... (burn+divide+liquidity+recipient=100)
12
+ * Labels: Meme | AI | Defi | Games | Infra | De-Sci | Social | Depin | Charity | Others
13
+ * Env: PRIVATE_KEY
14
+ */
15
+
16
+ import { createPublicClient, http, parseAbi } from 'viem';
17
+ import { privateKeyToAccount } from 'viem/accounts';
18
+ import { bsc } from 'viem/chains';
19
+ import { readFileSync, existsSync } from 'node:fs';
20
+ import { basename } from 'node:path';
21
+
22
+ const API_BASE = 'https://four.meme/meme-api/v1';
23
+ const TOKEN_MANAGER2_BSC = '0x5c952063c7fc8610FFDB798152D69F0B9550762b' as const;
24
+ const TM2_ABI = parseAbi([
25
+ 'function _launchFee() view returns (uint256)',
26
+ 'function _tradingFeeRate() view returns (uint256)',
27
+ ]);
28
+ const NETWORK_CODE = 'BSC';
29
+
30
+ /** Get option from argv: --key=value or --key value; fallback to env (key as UPPER_SNAKE). */
31
+ function getOpt(key: string, defaultValue: string): string {
32
+ const prefix = key + '=';
33
+ for (let i = 2; i < process.argv.length; i++) {
34
+ const arg = process.argv[i];
35
+ if (arg === key && i + 1 < process.argv.length) return process.argv[i + 1];
36
+ if (arg.startsWith(prefix)) return arg.slice(prefix.length);
37
+ }
38
+ return process.env[key.replace(/-/g, '_').toUpperCase()] ?? defaultValue;
39
+ }
40
+
41
+ /** Get boolean option from argv: --fee-plan or --fee-plan=true; fallback to env. */
42
+ function getOptBool(key: string, defaultValue: boolean): boolean {
43
+ const prefix = key + '=';
44
+ for (let i = 2; i < process.argv.length; i++) {
45
+ const arg = process.argv[i];
46
+ if (arg === key) return true;
47
+ if (arg.startsWith(prefix)) {
48
+ const v = arg.slice(prefix.length).toLowerCase();
49
+ return v === '1' || v === 'true' || v === 'yes';
50
+ }
51
+ }
52
+ const envKey = key.replace(/-/g, '_').toUpperCase();
53
+ if (process.env[envKey] !== undefined) {
54
+ const v = process.env[envKey]!.toLowerCase();
55
+ return v === '1' || v === 'true' || v === 'yes';
56
+ }
57
+ return defaultValue;
58
+ }
59
+
60
+ function toHex(value: string): string {
61
+ if (value.startsWith('0x')) return value;
62
+ if (/^[0-9a-fA-F]+$/.test(value)) return '0x' + value;
63
+ const buf = Buffer.from(value, 'base64');
64
+ return '0x' + buf.toString('hex');
65
+ }
66
+
67
+ async function main() {
68
+ const privateKey = process.env.PRIVATE_KEY;
69
+ if (!privateKey) {
70
+ console.error('Set PRIVATE_KEY');
71
+ process.exit(1);
72
+ }
73
+ const pk = privateKey.startsWith('0x') ? (privateKey as `0x${string}`) : (`0x${privateKey}` as `0x${string}`);
74
+ const account = privateKeyToAccount(pk);
75
+ const address = account.address;
76
+
77
+ const imagePath = getOpt('--image', '');
78
+ const name = getOpt('--name', '');
79
+ const shortName = getOpt('--short-name', '');
80
+ const desc = getOpt('--desc', '');
81
+ const label = getOpt('--label', '');
82
+ const taxOptionsPath = getOpt('--tax-options', '');
83
+
84
+ if (!imagePath || !name || !shortName || !desc || !label) {
85
+ console.error(
86
+ 'Usage: npx tsx create-token-api.ts --image=<path> --name= --short-name= --desc= --label= [options]'
87
+ );
88
+ console.error('Example: npx tsx create-token-api.ts --image=./logo.png --name=MyToken --short-name=MTK --desc="My desc" --label=AI');
89
+ console.error('Required: --image= --name= --short-name= --desc= --label=');
90
+ console.error('Optional: --web-url= --twitter-url= --telegram-url= --pre-sale=0 --fee-plan=false --tax-options=<path>');
91
+ process.exit(1);
92
+ }
93
+ if (!existsSync(imagePath)) {
94
+ console.error('Image file not found:', imagePath);
95
+ process.exit(1);
96
+ }
97
+
98
+ const validLabels = ['Meme', 'AI', 'Defi', 'Games', 'Infra', 'De-Sci', 'Social', 'Depin', 'Charity', 'Others'];
99
+ const labelNorm = validLabels.find((l) => l.toLowerCase() === label.toLowerCase());
100
+ if (!labelNorm) {
101
+ console.error('Invalid label. Use one of:', validLabels.join(', '));
102
+ process.exit(1);
103
+ }
104
+ const labelCanonical = labelNorm; // API expects exact label from list (case-sensitive)
105
+
106
+ // 1. Get nonce
107
+ const nonceRes = await fetch(`${API_BASE}/private/user/nonce/generate`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({
111
+ accountAddress: address,
112
+ verifyType: 'LOGIN',
113
+ networkCode: NETWORK_CODE,
114
+ }),
115
+ });
116
+ const nonceData = await nonceRes.json();
117
+ if (nonceData.code !== '0' && nonceData.code !== 0) {
118
+ throw new Error('Nonce failed: ' + JSON.stringify(nonceData));
119
+ }
120
+ const nonce = nonceData.data;
121
+
122
+ // 2. Sign and login
123
+ const message = `You are sign in Meme ${nonce}`;
124
+ const signature = await account.signMessage({ message });
125
+
126
+ const loginRes = await fetch(`${API_BASE}/private/user/login/dex`, {
127
+ method: 'POST',
128
+ headers: { 'Content-Type': 'application/json' },
129
+ body: JSON.stringify({
130
+ region: 'WEB',
131
+ langType: 'EN',
132
+ loginIp: '',
133
+ inviteCode: '',
134
+ verifyInfo: {
135
+ address,
136
+ networkCode: NETWORK_CODE,
137
+ signature,
138
+ verifyType: 'LOGIN',
139
+ },
140
+ walletName: 'MetaMask',
141
+ }),
142
+ });
143
+ const loginData = await loginRes.json();
144
+ if (loginData.code !== '0' && loginData.code !== 0) {
145
+ throw new Error('Login failed: ' + JSON.stringify(loginData));
146
+ }
147
+ const accessToken = loginData.data;
148
+
149
+ // 3. Upload image
150
+ const imageBuffer = readFileSync(imagePath);
151
+ const form = new FormData();
152
+ form.append('file', new Blob([imageBuffer]), basename(imagePath));
153
+
154
+ const uploadRes = await fetch(`${API_BASE}/private/token/upload`, {
155
+ method: 'POST',
156
+ headers: { 'meme-web-access': accessToken },
157
+ body: form as unknown as BodyInit,
158
+ });
159
+ const uploadData = await uploadRes.json();
160
+ if (uploadData.code !== '0' && uploadData.code !== 0) {
161
+ throw new Error('Upload failed: ' + JSON.stringify(uploadData));
162
+ }
163
+ const imgUrl = uploadData.data;
164
+
165
+ // 4. Public config for raisedToken (data[]: symbol, symbolAddress, totalBAmount, status=PUBLISH|INIT, ...)
166
+ const configRes = await fetch(`${API_BASE}/public/config`);
167
+ if (!configRes.ok) {
168
+ throw new Error('Public config request failed: ' + configRes.status + ' ' + configRes.statusText);
169
+ }
170
+ const configData = await configRes.json();
171
+ if (configData.code !== '0' && configData.code !== 0) {
172
+ throw new Error('Invalid public config response: ' + JSON.stringify(configData));
173
+ }
174
+ const symbols = configData.data;
175
+ if (!Array.isArray(symbols) || symbols.length === 0) {
176
+ throw new Error('Invalid public config (no raisedToken): ' + JSON.stringify(configData));
177
+ }
178
+ // Prefer BNB with status PUBLISH for BSC; else first PUBLISH; else first item
179
+ const published = symbols.filter((c: { status?: string }) => c.status === 'PUBLISH');
180
+ const list = published.length > 0 ? published : symbols;
181
+ const config =
182
+ list.find((c: { symbol?: string }) => c.symbol === 'BNB') ?? list[0];
183
+ const raisedToken = config;
184
+ if (!raisedToken || !raisedToken.symbol) {
185
+ throw new Error('Invalid public config (no raisedToken): ' + JSON.stringify(configData));
186
+ }
187
+
188
+ // 5. Build create body and optional tokenTaxInfo
189
+ // raisedAmount / totalSupply / saleRate from raisedToken or docs (API-CreateToken)
190
+ const launchTime = Date.now();
191
+ const totalSupply =
192
+ typeof (raisedToken as { totalAmount?: string | number }).totalAmount !== 'undefined'
193
+ ? Number((raisedToken as { totalAmount?: string | number }).totalAmount)
194
+ : 1000000000;
195
+ const raisedAmount =
196
+ typeof (raisedToken as { totalBAmount?: string | number }).totalBAmount !== 'undefined'
197
+ ? Number((raisedToken as { totalBAmount?: string | number }).totalBAmount)
198
+ : 24;
199
+ const body: Record<string, unknown> = {
200
+ name,
201
+ shortName,
202
+ desc,
203
+ totalSupply,
204
+ raisedAmount,
205
+ saleRate:
206
+ typeof (raisedToken as { saleRate?: string | number }).saleRate !== 'undefined'
207
+ ? Number((raisedToken as { saleRate?: string | number }).saleRate)
208
+ : 0.8,
209
+ reserveRate: 0,
210
+ imgUrl,
211
+ raisedToken,
212
+ launchTime,
213
+ funGroup: false,
214
+ label: labelCanonical,
215
+ lpTradingFee: 0.0025,
216
+ preSale: getOpt('--pre-sale', '0'),
217
+ clickFun: false,
218
+ symbol: (raisedToken as { symbol: string }).symbol,
219
+ dexType: 'PANCAKE_SWAP',
220
+ rushMode: false,
221
+ onlyMPC: false,
222
+ feePlan: getOptBool('--fee-plan', false),
223
+ };
224
+ const webUrl = getOpt('--web-url', '');
225
+ const twitterUrl = getOpt('--twitter-url', '');
226
+ const telegramUrl = getOpt('--telegram-url', '');
227
+ if (webUrl != null && webUrl !== '') body.webUrl = webUrl;
228
+ if (twitterUrl != null && twitterUrl !== '') body.twitterUrl = twitterUrl;
229
+ if (telegramUrl != null && telegramUrl !== '') body.telegramUrl = telegramUrl;
230
+
231
+ let tokenTaxInfo: Record<string, unknown> | null = null;
232
+ if (taxOptionsPath && existsSync(taxOptionsPath)) {
233
+ const taxOpts = JSON.parse(readFileSync(taxOptionsPath, 'utf8'));
234
+ if (taxOpts.tokenTaxInfo && typeof taxOpts.tokenTaxInfo === 'object') {
235
+ tokenTaxInfo = taxOpts.tokenTaxInfo as Record<string, unknown>;
236
+ }
237
+ }
238
+ const taxFromCli =
239
+ getOptBool('--tax-token', false) ||
240
+ getOpt('--tax-fee-rate', '') !== '' ||
241
+ process.env.TAX_TOKEN === '1';
242
+ if (!tokenTaxInfo && taxFromCli) {
243
+ const feeRate = Number(getOpt('--tax-fee-rate', '5'));
244
+ const burnRate = Number(getOpt('--tax-burn-rate', '0'));
245
+ const divideRate = Number(getOpt('--tax-divide-rate', '0'));
246
+ const liquidityRate = Number(getOpt('--tax-liquidity-rate', '100'));
247
+ const recipientRate = Number(getOpt('--tax-recipient-rate', '0'));
248
+ const recipientAddress = getOpt('--tax-recipient-address', '');
249
+ const minSharing = Number(getOpt('--tax-min-sharing', '100000'));
250
+ const sum = burnRate + divideRate + liquidityRate + recipientRate;
251
+ if (sum !== 100) {
252
+ throw new Error(`Tax rates must sum to 100 (burn+divide+liquidity+recipient). Got ${sum}.`);
253
+ }
254
+ if (![1, 3, 5, 10].includes(feeRate)) {
255
+ throw new Error('TAX_FEE_RATE must be 1, 3, 5, or 10.');
256
+ }
257
+ tokenTaxInfo = {
258
+ feeRate,
259
+ burnRate,
260
+ divideRate,
261
+ liquidityRate,
262
+ recipientRate,
263
+ recipientAddress,
264
+ minSharing,
265
+ };
266
+ }
267
+ if (tokenTaxInfo) {
268
+ body.tokenTaxInfo = tokenTaxInfo;
269
+ }
270
+
271
+ const createRes = await fetch(`${API_BASE}/private/token/create`, {
272
+ method: 'POST',
273
+ headers: {
274
+ 'meme-web-access': accessToken,
275
+ 'Content-Type': 'application/json',
276
+ },
277
+ body: JSON.stringify(body),
278
+ });
279
+ const createData = await createRes.json();
280
+ if (createData.code !== '0' && createData.code !== 0) {
281
+ throw new Error('Create API failed: ' + JSON.stringify(createData));
282
+ }
283
+ const { createArg: rawArg, signature: rawSig } = createData.data;
284
+ const createArgHex = toHex(rawArg);
285
+ const signatureHex = toHex(rawSig);
286
+
287
+ // Estimate required value (CREATION_FEE_WEI) for createToken tx
288
+ const rpcUrl = process.env.BSC_RPC_URL || 'https://bsc-dataseed.binance.org';
289
+ const client = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
290
+ const launchFee = await client.readContract({
291
+ address: TOKEN_MANAGER2_BSC,
292
+ abi: TM2_ABI,
293
+ functionName: '_launchFee',
294
+ });
295
+ const preSaleStr = String(body.preSale ?? '0');
296
+ // API preSale is in ether (BNB); convert to wei for value calculation
297
+ const presaleWei = BigInt(Math.round(parseFloat(preSaleStr || '0') * 1e18));
298
+ const quoteIsBnb = (raisedToken as { symbol?: string }).symbol === 'BNB';
299
+ let requiredValueWei = launchFee;
300
+ if (presaleWei > 0n && quoteIsBnb) {
301
+ const feeRate = await client.readContract({
302
+ address: TOKEN_MANAGER2_BSC,
303
+ abi: TM2_ABI,
304
+ functionName: '_tradingFeeRate',
305
+ });
306
+ const tradingFee = (presaleWei * feeRate) / 10000n;
307
+ requiredValueWei = launchFee + presaleWei + tradingFee;
308
+ }
309
+ const creationFeeWei = requiredValueWei.toString();
310
+
311
+ const out = { createArg: createArgHex, signature: signatureHex, creationFeeWei };
312
+ console.log(JSON.stringify(out, null, 2));
313
+ console.error(
314
+ `\n→ For create-token-chain pass --value=${creationFeeWei} (or more).`
315
+ );
316
+ }
317
+
318
+ main().catch((e) => {
319
+ console.error(e.message || e);
320
+ process.exit(1);
321
+ });