├── 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 | Centered Image 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 | --------------------------------------------------------------------------------