├── package.json
├── LICENSE
├── README.md
└── src
└── main.js
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spl-creator",
3 | "version": "1.0.0",
4 | "description": "Advanced SPL Token Creator with enhanced UI and error handling",
5 | "main": "src/main.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "node --no-deprecation src/main.js",
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "format": "prettier --write src/**/*.js"
11 | },
12 | "keywords": [
13 | "solana",
14 | "spl",
15 | "token",
16 | "creator",
17 | "blockchain"
18 | ],
19 | "author": "BankkRoll",
20 | "license": "ISC",
21 | "dependencies": {
22 | "@metaplex-foundation/mpl-token-metadata": "^2.13.0",
23 | "@solana/spl-token": "^0.3.9",
24 | "@solana/web3.js": "^1.89.1",
25 | "bs58": "^5.0.0",
26 | "chalk": "^5.3.0",
27 | "figlet": "^1.7.0",
28 | "gradient-string": "^2.0.2",
29 | "inquirer": "^9.2.12",
30 | "nanospinner": "^1.1.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Bankk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | A CLI tool for creating and minting SPL tokens on the Solana blockchain with advanced features and comprehensive error handling.
6 |
7 | ## Getting Started
8 |
9 | 1. **Clone the Repository**
10 |
11 | ```bash
12 | git clone https://github.com/BankkRoll/spl-token-creator.git
13 | cd spl-token-creator
14 | ```
15 |
16 | 2. **Install Dependencies**
17 |
18 | ```bash
19 | npm install
20 | ```
21 |
22 | 3. **Launch the Creator**
23 | ```bash
24 | npm start
25 | ```
26 |
27 | ## Interactive Creation Process
28 |
29 | 1. **Network Selection**
30 |
31 | - Choose between Devnet (testing) and Mainnet (production)
32 | - Automatic network configuration
33 |
34 | 2. **Token Configuration**
35 |
36 | - Decimals: 0-9 (default: 9)
37 | - Total Supply: Any positive number
38 | - Token Name: Up to 32 characters
39 | - Symbol: Up to 10 characters
40 | - Image URL: Valid URL for token image
41 | - Royalty: 0-100% (in basis points)
42 |
43 | 3. **Wallet Authentication**
44 | - Secure secret key input
45 | - Automatic validation
46 | - Local-only processing
47 |
48 | ## Output Information
49 |
50 | The tool provides comprehensive information after successful token creation:
51 |
52 | - **Token Details**
53 |
54 | - Name and Symbol
55 | - Decimals and Total Supply
56 | - Mint Address
57 | - Associated Token Account
58 |
59 | - **Economics**
60 |
61 | - Royalty Percentage
62 | - Transaction Fees
63 | - Network Details
64 |
65 | - **Transaction Information**
66 | - Transaction Hash
67 | - Block Time
68 | - Execution Duration
69 | - Explorer Links
70 |
71 | ## Explorer Integration
72 |
73 | - **Automatic Links Generation**
74 | - Token Explorer URL
75 | - Transaction Explorer URL
76 | - Network-aware URLs (Devnet/Mainnet)
77 |
78 | ## Security Considerations
79 |
80 | - Secret keys are never stored or transmitted
81 | - All transactions are signed locally
82 | - Input validation for all parameters
83 | - Secure error handling
84 | - Network-specific configurations
85 |
86 | ## Technical Details
87 |
88 | - Uses Versioned Transactions
89 | - Implements Metadata Program V3
90 | - Supports Associated Token Accounts
91 | - Handles PDA derivation
92 | - Manages rent exemption
93 | - Implements proper error handling
94 |
95 | ## Error Handling
96 |
97 | The tool includes comprehensive error handling for:
98 |
99 | - Invalid inputs
100 | - Network connection issues
101 | - Transaction failures
102 | - Insufficient balances
103 | - Invalid wallet keys
104 | - Metadata creation errors
105 |
106 | ## License
107 |
108 | MIT License - feel free to use and modify as needed.
109 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // src/main.js
2 | import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID_DIST } from "@metaplex-foundation/mpl-token-metadata/dist/src/generated/index.js";
3 | import { createCreateMetadataAccountV3Instruction } from "@metaplex-foundation/mpl-token-metadata/dist/src/generated/instructions/CreateMetadataAccountV3.js";
4 | import {
5 | createAssociatedTokenAccountInstruction,
6 | createInitializeMintInstruction,
7 | createMintToInstruction,
8 | getAssociatedTokenAddress,
9 | getMinimumBalanceForRentExemptMint,
10 | MINT_SIZE,
11 | TOKEN_PROGRAM_ID,
12 | } from "@solana/spl-token";
13 | import {
14 | clusterApiUrl,
15 | Connection,
16 | Keypair,
17 | PublicKey,
18 | SystemProgram,
19 | TransactionMessage,
20 | VersionedTransaction,
21 | } from "@solana/web3.js";
22 | import bs58 from "bs58";
23 | import chalk from "chalk";
24 | import figlet from "figlet";
25 | import gradient from "gradient-string";
26 | import inquirer from "inquirer";
27 | import { createSpinner } from "nanospinner";
28 |
29 | // Utility Functions
30 | const sleep = (ms = 1000) => new Promise((resolve) => setTimeout(resolve, ms));
31 |
32 | const displayTitle = () => {
33 | console.clear();
34 | const title = gradient.pastel.multiline(
35 | figlet.textSync("SPL Token Creator", {
36 | font: "Standard",
37 | horizontalLayout: "default",
38 | verticalLayout: "default",
39 | }),
40 | );
41 | console.log(title);
42 | console.log(gradient.rainbow("=".repeat(80)));
43 | console.log("\n");
44 | };
45 |
46 | const getNetworkConfig = (network) => {
47 | const config =
48 | network === "mainnet"
49 | ? {
50 | cluster: clusterApiUrl("mainnet-beta"),
51 | name: "Mainnet",
52 | explorerUrl: "https://solscan.io",
53 | symbol: "SOL",
54 | }
55 | : {
56 | cluster: clusterApiUrl("devnet"),
57 | name: "Devnet",
58 | explorerUrl: "https://solscan.io",
59 | symbol: "SOL (Devnet)",
60 | };
61 | return {
62 | ...config,
63 | commitment: "confirmed",
64 | };
65 | };
66 |
67 | const validateDecimals = (value) => {
68 | const parsed = parseInt(value);
69 | if (isNaN(parsed)) return "Please enter a valid number";
70 | if (parsed < 0 || parsed > 9) return "Decimals must be between 0 and 9";
71 | return true;
72 | };
73 |
74 | const validateSupply = (value) => {
75 | const parsed = parseFloat(value);
76 | if (isNaN(parsed)) return "Please enter a valid number";
77 | if (parsed <= 0) return "Supply must be greater than 0";
78 | return true;
79 | };
80 |
81 | const validateTokenName = (value) => {
82 | if (!value.trim()) return "Token name cannot be empty";
83 | if (value.length > 32) return "Token name must be 32 characters or less";
84 | return true;
85 | };
86 |
87 | const validateSymbol = (value) => {
88 | if (!value.trim()) return "Symbol cannot be empty";
89 | if (value.length > 10) return "Symbol must be 10 characters or less";
90 | return true;
91 | };
92 |
93 | const validateImageUrl = (value) => {
94 | try {
95 | new URL(value);
96 | return true;
97 | } catch {
98 | return "Please enter a valid URL";
99 | }
100 | };
101 |
102 | const validateRoyalty = (value) => {
103 | const parsed = parseInt(value);
104 | if (isNaN(parsed)) return "Please enter a valid number";
105 | if (parsed < 0 || parsed > 10000)
106 | return "Royalty must be between 0 and 10000 basis points (0-100%)";
107 | return true;
108 | };
109 |
110 | const askQuestions = async () => {
111 | displayTitle();
112 |
113 | const questions = [
114 | {
115 | type: "list",
116 | name: "network",
117 | message: "Choose the network:",
118 | choices: [
119 | { name: "🔧 Devnet (Testing)", value: "devnet" },
120 | { name: "🌐 Mainnet (Production)", value: "mainnet" },
121 | ],
122 | default: "devnet",
123 | },
124 | {
125 | type: "input",
126 | name: "decimals",
127 | message: "Set the token decimals (0-9):",
128 | default: "9",
129 | validate: validateDecimals,
130 | transformer: (input) => chalk.cyan(input),
131 | },
132 | {
133 | type: "input",
134 | name: "supply",
135 | message: "Set the total token supply:",
136 | default: "1000000",
137 | validate: validateSupply,
138 | transformer: (input) => chalk.cyan(input),
139 | },
140 | {
141 | type: "input",
142 | name: "tokenName",
143 | message: "Token name:",
144 | validate: validateTokenName,
145 | transformer: (input) => chalk.cyan(input),
146 | },
147 | {
148 | type: "input",
149 | name: "symbol",
150 | message: "Token symbol:",
151 | validate: validateSymbol,
152 | transformer: (input) => chalk.green(input.toUpperCase()),
153 | },
154 | {
155 | type: "input",
156 | name: "image",
157 | message: "Token image URL:",
158 | validate: validateImageUrl,
159 | transformer: (input) => chalk.blue(input),
160 | },
161 | {
162 | type: "input",
163 | name: "royalty",
164 | message: "Set the royalty percentage (0-100%):",
165 | default: "0",
166 | validate: validateRoyalty,
167 | transformer: (input) =>
168 | chalk.yellow(`${(parseInt(input) / 100).toFixed(2)}%`),
169 | },
170 | {
171 | type: "password",
172 | name: "secretKey",
173 | message: "Enter your wallet secret key:",
174 | mask: "*",
175 | validate: (value) => {
176 | try {
177 | const decoded = bs58.decode(value);
178 | return decoded.length === 64 || "Invalid secret key length";
179 | } catch {
180 | return "Invalid secret key format";
181 | }
182 | },
183 | },
184 | ];
185 |
186 | return inquirer.prompt(questions);
187 | };
188 |
189 | const createMintTokenTransaction = async (
190 | connection,
191 | payer,
192 | mintKeypair,
193 | token,
194 | tokenMetadata,
195 | destinationWallet,
196 | mintAuthority,
197 | ) => {
198 | try {
199 | const spinner = createSpinner("Preparing transaction...").start();
200 |
201 | const requiredBalance =
202 | await getMinimumBalanceForRentExemptMint(connection);
203 |
204 | // Calculate the metadata PDA
205 | const [metadataPDA] = PublicKey.findProgramAddressSync(
206 | [
207 | Buffer.from("metadata"),
208 | TOKEN_METADATA_PROGRAM_ID_DIST.toBuffer(),
209 | mintKeypair.publicKey.toBuffer(),
210 | ],
211 | TOKEN_METADATA_PROGRAM_ID_DIST,
212 | );
213 |
214 | const tokenATA = await getAssociatedTokenAddress(
215 | mintKeypair.publicKey,
216 | destinationWallet,
217 | );
218 |
219 | const instructions = [
220 | SystemProgram.createAccount({
221 | fromPubkey: payer.publicKey,
222 | newAccountPubkey: mintKeypair.publicKey,
223 | space: MINT_SIZE,
224 | lamports: requiredBalance,
225 | programId: TOKEN_PROGRAM_ID,
226 | }),
227 | createInitializeMintInstruction(
228 | mintKeypair.publicKey,
229 | token.decimals,
230 | mintAuthority,
231 | null,
232 | TOKEN_PROGRAM_ID,
233 | ),
234 | createAssociatedTokenAccountInstruction(
235 | payer.publicKey,
236 | tokenATA,
237 | payer.publicKey,
238 | mintKeypair.publicKey,
239 | ),
240 | createMintToInstruction(
241 | mintKeypair.publicKey,
242 | tokenATA,
243 | mintAuthority,
244 | token.totalSupply * Math.pow(10, token.decimals),
245 | ),
246 | createCreateMetadataAccountV3Instruction(
247 | {
248 | metadata: metadataPDA,
249 | mint: mintKeypair.publicKey,
250 | mintAuthority: mintAuthority,
251 | payer: payer.publicKey,
252 | updateAuthority: mintAuthority,
253 | },
254 | {
255 | createMetadataAccountArgsV3: {
256 | data: {
257 | name: tokenMetadata.name,
258 | symbol: tokenMetadata.symbol,
259 | uri: tokenMetadata.uri,
260 | sellerFeeBasisPoints: tokenMetadata.sellerFeeBasisPoints,
261 | creators: null,
262 | collection: null,
263 | uses: null,
264 | },
265 | isMutable: true,
266 | collectionDetails: null,
267 | },
268 | },
269 | ),
270 | ];
271 |
272 | spinner.success({ text: "Transaction prepared successfully" });
273 |
274 | const latestBlockhash = await connection.getLatestBlockhash("confirmed");
275 | const messageV0 = new TransactionMessage({
276 | payerKey: payer.publicKey,
277 | recentBlockhash: latestBlockhash.blockhash,
278 | instructions,
279 | }).compileToV0Message();
280 |
281 | return new VersionedTransaction(messageV0);
282 | } catch (error) {
283 | console.error(chalk.red("\nError creating transaction:"), error);
284 | throw error;
285 | }
286 | };
287 |
288 | const displaySuccessInfo = async (
289 | connection,
290 | network,
291 | mintKeypair,
292 | txid,
293 | token,
294 | tokenMetadata,
295 | userWallet,
296 | startTime,
297 | ) => {
298 | const endTime = performance.now();
299 | const executionTime = ((endTime - startTime) / 1000).toFixed(2);
300 |
301 | // Get transaction details
302 | const txDetails = await connection.getTransaction(txid, {
303 | maxSupportedTransactionVersion: 0,
304 | });
305 | const fees = txDetails?.meta?.fee || 0;
306 | const blockTime = txDetails?.blockTime || 0;
307 |
308 | // Get token account
309 | const tokenATA = await getAssociatedTokenAddress(
310 | mintKeypair.publicKey,
311 | userWallet.publicKey,
312 | );
313 |
314 | console.log("\n" + gradient.rainbow("=".repeat(80)));
315 | console.log(chalk.bold.green("\n🎉 Token Creation Successful! 🎉\n"));
316 |
317 | console.log(chalk.yellow("📊 Token Details:"));
318 | console.log(chalk.cyan("• Name: "), chalk.white(tokenMetadata.name));
319 | console.log(
320 | chalk.cyan("• Symbol: "),
321 | chalk.white(tokenMetadata.symbol),
322 | );
323 | console.log(chalk.cyan("• Decimals: "), chalk.white(token.decimals));
324 | console.log(
325 | chalk.cyan("• Total Supply: "),
326 | chalk.white(
327 | `${token.totalSupply.toLocaleString()} ${tokenMetadata.symbol}`,
328 | ),
329 | );
330 | console.log(
331 | chalk.cyan("• Mint Address: "),
332 | chalk.yellow(mintKeypair.publicKey.toString()),
333 | );
334 | console.log(
335 | chalk.cyan("• Token Account: "),
336 | chalk.yellow(tokenATA.toString()),
337 | );
338 |
339 | console.log(chalk.yellow("\n💰 Economics:"));
340 | console.log(
341 | chalk.cyan("• Royalty: "),
342 | chalk.white(`${tokenMetadata.sellerFeeBasisPoints / 100}%`),
343 | );
344 | console.log(
345 | chalk.cyan("• Transaction Fee:"),
346 | chalk.white(`${fees / 1e9} ${network.symbol}`),
347 | );
348 |
349 | console.log(chalk.yellow("\n🔍 Transaction Info:"));
350 | console.log(chalk.cyan("• TX Hash: "), chalk.yellow(txid));
351 | console.log(
352 | chalk.cyan("• Block Time: "),
353 | chalk.white(new Date(blockTime * 1000).toLocaleString()),
354 | );
355 | console.log(
356 | chalk.cyan("• Execution Time:"),
357 | chalk.white(`${executionTime} seconds`),
358 | );
359 |
360 | console.log(chalk.yellow("\n🔗 Links:"));
361 | const tokenUrl =
362 | network.name === "Mainnet"
363 | ? `${network.explorerUrl}/token/${mintKeypair.publicKey.toString()}`
364 | : `${network.explorerUrl}/token/${mintKeypair.publicKey.toString()}?cluster=devnet`;
365 | const txUrl =
366 | network.name === "Mainnet"
367 | ? `${network.explorerUrl}/tx/${txid}`
368 | : `${network.explorerUrl}/tx/${txid}?cluster=devnet`;
369 |
370 | console.log(chalk.cyan("• Token Explorer:"), chalk.blue(tokenUrl));
371 | console.log(chalk.cyan("• TX Explorer: "), chalk.blue(txUrl));
372 |
373 | console.log("\n" + gradient.rainbow("=".repeat(80)) + "\n");
374 | };
375 |
376 | const main = async () => {
377 | try {
378 | const startTime = performance.now();
379 | const answers = await askQuestions();
380 | console.log("\n");
381 |
382 | const network = getNetworkConfig(answers.network);
383 | const spinner = createSpinner(`Connecting to ${network.name}...`).start();
384 |
385 | const connection = new Connection(network.cluster, network.commitment);
386 | await sleep(1000);
387 | spinner.success({ text: `Connected to ${network.name}` });
388 |
389 | const userWallet = Keypair.fromSecretKey(bs58.decode(answers.secretKey));
390 | console.log(
391 | chalk.green("\nWallet address:"),
392 | chalk.cyan(userWallet.publicKey.toString()),
393 | );
394 |
395 | const token = {
396 | decimals: parseInt(answers.decimals),
397 | totalSupply: parseFloat(answers.supply),
398 | };
399 |
400 | const tokenMetadata = {
401 | name: answers.tokenName,
402 | symbol: answers.symbol,
403 | uri: answers.image,
404 | sellerFeeBasisPoints: parseInt(answers.royalty),
405 | creators: null,
406 | collection: null,
407 | uses: null,
408 | };
409 |
410 | console.log(chalk.yellow("\nToken Configuration:"));
411 | console.log(chalk.cyan("- Name:"), tokenMetadata.name);
412 | console.log(chalk.cyan("- Symbol:"), tokenMetadata.symbol);
413 | console.log(chalk.cyan("- Decimals:"), token.decimals);
414 | console.log(chalk.cyan("- Total Supply:"), token.totalSupply);
415 | console.log(chalk.cyan("- Image URL:"), tokenMetadata.uri);
416 | console.log(
417 | chalk.cyan("- Royalty:"),
418 | `${tokenMetadata.sellerFeeBasisPoints / 100}%\n`,
419 | );
420 |
421 | const confirmSpinner = createSpinner("Creating token...").start();
422 | const mintKeypair = Keypair.generate();
423 |
424 | const transaction = await createMintTokenTransaction(
425 | connection,
426 | userWallet,
427 | mintKeypair,
428 | token,
429 | tokenMetadata,
430 | userWallet.publicKey,
431 | userWallet.publicKey,
432 | );
433 |
434 | transaction.sign([userWallet, mintKeypair]);
435 |
436 | const txid = await connection.sendTransaction(transaction);
437 | await connection.confirmTransaction({
438 | signature: txid,
439 | blockhash: transaction.message.recentBlockhash,
440 | lastValidBlockHeight: (await connection.getBlockHeight()) + 150,
441 | });
442 |
443 | confirmSpinner.success({ text: "Token created successfully!" });
444 |
445 | await displaySuccessInfo(
446 | connection,
447 | network,
448 | mintKeypair,
449 | txid,
450 | token,
451 | tokenMetadata,
452 | userWallet,
453 | startTime,
454 | );
455 | } catch (error) {
456 | console.error(chalk.red("\nError:"), error.message);
457 | process.exit(1);
458 | }
459 | };
460 |
461 | main();
462 |
--------------------------------------------------------------------------------