feat: SPL Token Launcher (TOKEN FORGE) on LAB

This commit is contained in:
jae 2026-04-06 01:02:43 +00:00
parent 7611698889
commit 55c8f499f1
4 changed files with 1467 additions and 0 deletions

View file

@ -35,6 +35,13 @@
box-shadow: 0 4px 20px rgba(20, 241, 149, 0.15); box-shadow: 0 4px 20px rgba(20, 241, 149, 0.15);
background: rgba(20, 20, 20, 0.95); background: rgba(20, 20, 20, 0.95);
} }
.deploy-card--amber {
border-left-color: rgba(245, 166, 35, 0.6);
}
.deploy-card--amber:hover {
border-left-color: #F5A623;
box-shadow: 0 4px 20px rgba(245, 166, 35, 0.15);
}
.deploy-card-status { .deploy-card-status {
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem; font-size: 0.6rem;
@ -125,6 +132,18 @@
</div> </div>
<div class="deploy-card-url">jaeswift.xyz/soldomains</div> <div class="deploy-card-url">jaeswift.xyz/soldomains</div>
</a> </a>
<a href="/tokenlauncher" class="deploy-card deploy-card--amber">
<div class="deploy-card-status" style="color: #F5A623;">◆ EXPERIMENTAL</div>
<div class="deploy-card-title">TOKEN FORGE</div>
<div class="deploy-card-desc">Deploy custom SPL tokens on Solana. Set name, symbol, supply, and metadata — launch in one transaction.</div>
<div class="deploy-card-tags">
<span class="deploy-tag" style="color: rgba(245, 166, 35, 0.8); background: rgba(245, 166, 35, 0.08); border-color: rgba(245, 166, 35, 0.2);">SOLANA</span>
<span class="deploy-tag" style="color: rgba(245, 166, 35, 0.8); background: rgba(245, 166, 35, 0.08); border-color: rgba(245, 166, 35, 0.2);">SPL</span>
<span class="deploy-tag" style="color: rgba(245, 166, 35, 0.8); background: rgba(245, 166, 35, 0.08); border-color: rgba(245, 166, 35, 0.2);">TOKENS</span>
</div>
<div class="deploy-card-url">jaeswift.xyz/tokenlauncher</div>
</a>
</div> </div>
</section> </section>

599
css/tokenlauncher.css Normal file
View file

@ -0,0 +1,599 @@
/* TOKEN FORGE — SPL Token Launcher */
:root {
--tf-amber: #F5A623;
--tf-amber-bright: #FFB800;
--tf-amber-dim: rgba(245, 166, 35, 0.6);
--tf-amber-glow: rgba(245, 166, 35, 0.15);
--tf-amber-border: rgba(245, 166, 35, 0.3);
--tf-amber-bg: rgba(245, 166, 35, 0.08);
--tf-red: #FF4444;
--tf-green: #14F195;
}
.tf-container {
max-width: clamp(900px, 80vw, 1400px);
margin: 0 auto;
padding: 0 2rem 3rem;
}
/* ── Wallet Bar ────────────────────────────────────────────── */
.tf-wallet-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding: 0.75rem 1.25rem;
background: rgba(16, 16, 16, 0.6);
border: 1px solid var(--tf-amber-border);
}
.tf-wallet-status {
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.4);
letter-spacing: 1px;
}
.tf-wallet-status.connected {
color: var(--tf-amber);
}
.tf-wallet-btn {
background: var(--tf-amber-bg);
border: 1px solid var(--tf-amber-border);
color: var(--tf-amber);
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
letter-spacing: 2px;
padding: 0.5rem 1rem;
cursor: pointer;
transition: all 0.25s ease;
}
.tf-wallet-btn:hover {
background: rgba(245, 166, 35, 0.25);
border-color: var(--tf-amber);
}
.tf-wallet-btn.disconnect {
border-color: rgba(255, 50, 50, 0.3);
color: rgba(255, 50, 50, 0.7);
background: rgba(255, 50, 50, 0.08);
}
.tf-wallet-btn.disconnect:hover {
border-color: rgba(255, 50, 50, 0.6);
background: rgba(255, 50, 50, 0.15);
}
/* ── Form ──────────────────────────────────────────────────── */
.tf-form {
background: rgba(16, 16, 16, 0.85);
border: 1px solid var(--border);
border-left: 3px solid var(--tf-amber-dim);
padding: 2rem;
margin-bottom: 1.5rem;
}
.tf-form-title {
font-family: 'Orbitron', monospace;
font-size: 0.75rem;
color: var(--tf-amber);
letter-spacing: 3px;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(245, 166, 35, 0.15);
}
.tf-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem;
}
@media (max-width: 700px) {
.tf-form-grid { grid-template-columns: 1fr; }
}
.tf-field {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.tf-field.full-width {
grid-column: 1 / -1;
}
.tf-label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 2px;
}
.tf-label-hint {
font-size: 0.5rem;
color: rgba(255, 255, 255, 0.25);
letter-spacing: 0.5px;
margin-top: 0.15rem;
}
.tf-input,
.tf-select,
.tf-textarea {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(245, 166, 35, 0.15);
color: #ffffff;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
padding: 0.65rem 0.85rem;
outline: none;
transition: all 0.25s ease;
letter-spacing: 0.5px;
}
.tf-input:focus,
.tf-select:focus,
.tf-textarea:focus {
border-color: var(--tf-amber-dim);
box-shadow: 0 0 15px rgba(245, 166, 35, 0.1);
}
.tf-input::placeholder,
.tf-textarea::placeholder {
color: rgba(255, 255, 255, 0.15);
}
.tf-select {
appearance: none;
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23F5A623' viewBox='0 0 16 16'%3E%3Cpath d='M8 12L2 6h12z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2rem;
}
.tf-select option {
background: #111;
color: #fff;
}
.tf-textarea {
resize: vertical;
min-height: 60px;
}
/* ── Checkboxes ────────────────────────────────────────────── */
.tf-checkboxes {
grid-column: 1 / -1;
display: flex;
gap: 2rem;
margin-top: 0.5rem;
flex-wrap: wrap;
}
.tf-checkbox-wrap {
display: flex;
align-items: flex-start;
gap: 0.5rem;
cursor: pointer;
}
.tf-checkbox-wrap input[type="checkbox"] {
appearance: none;
width: 16px;
height: 16px;
border: 1px solid var(--tf-amber-border);
background: rgba(0, 0, 0, 0.4);
cursor: pointer;
position: relative;
flex-shrink: 0;
margin-top: 1px;
}
.tf-checkbox-wrap input[type="checkbox"]:checked {
background: var(--tf-amber-bg);
border-color: var(--tf-amber);
}
.tf-checkbox-wrap input[type="checkbox"]:checked::after {
content: '\2713';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--tf-amber);
font-size: 0.65rem;
}
.tf-checkbox-text {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.tf-checkbox-label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 1px;
}
.tf-checkbox-rec {
font-family: 'JetBrains Mono', monospace;
font-size: 0.5rem;
color: var(--tf-amber-dim);
letter-spacing: 0.5px;
}
/* ── Fee Display ───────────────────────────────────────────── */
.tf-fee-section {
grid-column: 1 / -1;
background: rgba(245, 166, 35, 0.04);
border: 1px solid rgba(245, 166, 35, 0.12);
padding: 1rem;
margin-top: 0.5rem;
}
.tf-fee-title {
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
color: var(--tf-amber);
letter-spacing: 2px;
margin-bottom: 0.5rem;
}
.tf-fee-row {
display: flex;
justify-content: space-between;
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
padding: 0.2rem 0;
}
.tf-fee-label { color: rgba(255, 255, 255, 0.4); }
.tf-fee-value { color: rgba(255, 255, 255, 0.7); }
.tf-fee-total {
border-top: 1px solid rgba(245, 166, 35, 0.15);
margin-top: 0.35rem;
padding-top: 0.5rem;
}
.tf-fee-total .tf-fee-value {
color: var(--tf-amber);
font-weight: 700;
}
/* ── Deploy Button ─────────────────────────────────────────── */
.tf-deploy-section {
grid-column: 1 / -1;
margin-top: 0.75rem;
}
.tf-deploy-btn {
width: 100%;
padding: 1rem;
background: rgba(245, 166, 35, 0.1);
border: 1px solid var(--tf-amber-dim);
color: var(--tf-amber);
font-family: 'Orbitron', monospace;
font-size: 0.8rem;
letter-spacing: 4px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.tf-deploy-btn:hover:not(:disabled) {
background: rgba(245, 166, 35, 0.2);
border-color: var(--tf-amber-bright);
box-shadow: 0 0 30px rgba(245, 166, 35, 0.2), inset 0 0 30px rgba(245, 166, 35, 0.05);
transform: translateY(-1px);
}
.tf-deploy-btn:active:not(:disabled) {
transform: translateY(0);
}
.tf-deploy-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.tf-deploy-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(245, 166, 35, 0.08), transparent);
transition: left 0.6s ease;
}
.tf-deploy-btn:hover:not(:disabled)::before {
left: 100%;
}
/* ── Status Panel ──────────────────────────────────────────── */
.tf-status {
background: rgba(16, 16, 16, 0.85);
border: 1px solid var(--border);
border-left: 3px solid var(--tf-amber-dim);
padding: 1.5rem;
margin-bottom: 1.5rem;
display: none;
}
.tf-status.active { display: block; }
.tf-status-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.tf-status-title {
font-family: 'Orbitron', monospace;
font-size: 0.75rem;
letter-spacing: 2px;
}
.tf-status.building .tf-status-title,
.tf-status.awaiting .tf-status-title,
.tf-status.confirming .tf-status-title { color: var(--tf-amber); }
.tf-status.success .tf-status-title { color: var(--tf-green); }
.tf-status.success { border-left-color: rgba(20, 241, 149, 0.6); }
.tf-status.error .tf-status-title { color: var(--tf-red); }
.tf-status.error { border-left-color: rgba(255, 68, 68, 0.6); }
.tf-status-body {
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.5);
line-height: 1.8;
}
.tf-status-body a {
color: var(--tf-amber);
text-decoration: none;
}
.tf-status-body a:hover { text-decoration: underline; }
.tf-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(245, 166, 35, 0.2);
border-top-color: var(--tf-amber);
border-radius: 50%;
animation: tf-spin 0.8s linear infinite;
}
@keyframes tf-spin { to { transform: rotate(360deg); } }
/* ── Result Details ────────────────────────────────────────── */
.tf-result-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.tf-result-row:last-child { border-bottom: none; }
.tf-result-label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.55rem;
color: rgba(255, 255, 255, 0.35);
letter-spacing: 2px;
flex-shrink: 0;
}
.tf-result-value {
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.7);
word-break: break-all;
text-align: right;
max-width: 60%;
}
/* ── Modals ────────────────────────────────────────────────── */
.tf-modal {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1000;
display: none;
}
.tf-modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.tf-modal-backdrop {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(4px);
}
.tf-modal-content {
position: relative;
background: rgba(16, 16, 16, 0.98);
border: 1px solid var(--tf-amber-border);
border-left: 3px solid var(--tf-amber-dim);
max-width: 550px;
width: 90%;
max-height: 85vh;
overflow-y: auto;
padding: 2rem;
}
.tf-modal-title {
font-family: 'Orbitron', monospace;
font-size: 0.75rem;
color: var(--tf-amber);
letter-spacing: 3px;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(245, 166, 35, 0.15);
}
.tf-modal-row {
display: flex;
justify-content: space-between;
padding: 0.4rem 0;
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
}
.tf-modal-label {
color: rgba(255, 255, 255, 0.35);
letter-spacing: 2px;
flex-shrink: 0;
}
.tf-modal-value {
color: rgba(255, 255, 255, 0.8);
text-align: right;
max-width: 55%;
word-break: break-all;
}
.tf-modal-warning {
background: rgba(255, 68, 68, 0.05);
border: 1px solid rgba(255, 68, 68, 0.15);
padding: 0.75rem;
margin: 1rem 0;
font-family: 'JetBrains Mono', monospace;
font-size: 0.55rem;
color: rgba(255, 68, 68, 0.75);
line-height: 1.7;
letter-spacing: 0.5px;
}
.tf-modal-actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
}
.tf-modal-cancel {
flex: 1;
padding: 0.7rem;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.4);
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
letter-spacing: 2px;
cursor: pointer;
transition: all 0.25s ease;
}
.tf-modal-cancel:hover {
border-color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.6);
}
.tf-modal-confirm {
flex: 1.5;
padding: 0.7rem;
background: rgba(245, 166, 35, 0.12);
border: 1px solid var(--tf-amber-dim);
color: var(--tf-amber);
font-family: 'Orbitron', monospace;
font-size: 0.65rem;
letter-spacing: 2px;
cursor: pointer;
transition: all 0.25s ease;
}
.tf-modal-confirm:hover {
background: rgba(245, 166, 35, 0.2);
border-color: var(--tf-amber);
box-shadow: 0 0 20px rgba(245, 166, 35, 0.15);
}
/* ── Wallet Picker ─────────────────────────────────────────── */
.tf-wallet-option {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(245, 166, 35, 0.12);
color: rgba(255, 255, 255, 0.7);
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.25s ease;
text-align: left;
}
.tf-wallet-option:hover {
background: rgba(245, 166, 35, 0.08);
border-color: var(--tf-amber-border);
color: var(--tf-amber);
}
.tf-wallet-install {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.6rem 1rem;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.3);
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
letter-spacing: 1px;
text-decoration: none;
transition: all 0.25s ease;
}
.tf-wallet-install:hover {
border-color: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.5);
}
/* ── Responsive ────────────────────────────────────────────── */
@media (max-width: 600px) {
.tf-container { padding: 0 1rem 2rem; }
.tf-form { padding: 1.25rem; }
.tf-modal-content { padding: 1.25rem; }
.tf-modal-actions { flex-direction: column; }
.tf-result-row { flex-direction: column; gap: 0.25rem; align-items: flex-start; }
.tf-result-value { text-align: left; max-width: 100%; }
.tf-checkboxes { flex-direction: column; gap: 1rem; }
.tf-wallet-bar { flex-direction: column; gap: 0.75rem; text-align: center; }
}
.hidden { display: none !important; }

663
js/tokenlauncher.js Normal file
View file

@ -0,0 +1,663 @@
/* TOKEN FORGE SPL Token Launcher
Deploys custom SPL tokens on Solana mainnet with Metaplex metadata.
Manual instruction encoding for SPL Token + Metaplex programs.
Requires @solana/web3.js CDN (solanaWeb3 global).
*/
(function () {
'use strict';
// ── Constants ──────────────────────────────────────────────
const RPC_URL = 'https://api.mainnet-beta.solana.com';
const FEE_WALLET = '9NuiHh5wgRPx69BFGP1ZR8kHiBENGoJrXs5GpZzKAyn8';
const SERVICE_FEE_LAMPORTS = 100_000_000; // 0.1 SOL
const MINT_ACCOUNT_SIZE = 82;
const TOKEN_PROGRAM = new solanaWeb3.PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const ATA_PROGRAM = new solanaWeb3.PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
const METADATA_PROGRAM = new solanaWeb3.PublicKey('metaqbxxUEFHGOqbxLZq71kbiKTfnLRjnNt8GYJoMKM');
const SYSTEM_PROGRAM = solanaWeb3.SystemProgram.programId;
const RENT_SYSVAR = new solanaWeb3.PublicKey('SysvarRent111111111111111111111111111111111');
const SOLSCAN_TX = 'https://solscan.io/tx/';
const SOLSCAN_TOKEN = 'https://solscan.io/token/';
const EXPLORER_TX = 'https://explorer.solana.com/tx/';
const MAX_U64 = 18446744073709551615n;
// ── State ──────────────────────────────────────────────────
let walletAddress = null;
// ── DOM ────────────────────────────────────────────────────
const $ = s => document.querySelector(s);
document.addEventListener('DOMContentLoaded', () => {
initWallet();
initForm();
});
// ═══════════════════════════════════════════════════════════
// WALLET
// ═══════════════════════════════════════════════════════════
function initWallet() {
window.addEventListener('wallet-connected', e => {
walletAddress = e.detail.address;
updateWalletUI();
});
window.addEventListener('wallet-disconnected', () => {
walletAddress = null;
updateWalletUI();
});
if (window.solWallet?.connected) walletAddress = window.solWallet.address;
updateWalletUI();
const btn = $('#wallet-btn');
if (btn) btn.addEventListener('click', () => {
walletAddress ? window.solWallet.disconnect() : showWalletPicker();
});
}
function updateWalletUI() {
const s = $('#wallet-status'), b = $('#wallet-btn');
if (!s || !b) return;
if (walletAddress) {
s.className = 'tf-wallet-status connected';
s.textContent = '\u25CF CONNECTED \u2014 ' + truncAddr(walletAddress);
b.className = 'tf-wallet-btn disconnect';
b.textContent = 'DISCONNECT';
} else {
s.className = 'tf-wallet-status';
s.textContent = '\u25CB NOT CONNECTED';
b.className = 'tf-wallet-btn';
b.textContent = 'CONNECT WALLET';
}
updateDeployBtn();
}
function showWalletPicker() {
const wallets = window.solWallet.getAvailableWallets();
const known = window.solWallet.KNOWN_WALLETS;
let m = $('#tf-wallet-modal');
if (!m) {
m = document.createElement('div');
m.id = 'tf-wallet-modal';
m.className = 'tf-modal';
document.body.appendChild(m);
}
const names = new Set(wallets.map(w => w.name));
const det = wallets.map(w =>
'<button class="tf-wallet-option" data-w="' + esc(w.name) + '">' + w.icon + ' ' + esc(w.name) + '</button>'
).join('');
const inst = known.filter(k => !names.has(k.name)).map(k =>
'<a href="' + esc(k.url) + '" target="_blank" rel="noopener" class="tf-wallet-install">' + k.icon + ' ' + esc(k.name) + ' \u2014 INSTALL \u2197</a>'
).join('');
m.innerHTML =
'<div class="tf-modal-backdrop"></div>' +
'<div class="tf-modal-content">' +
'<div class="tf-modal-title">SELECT WALLET</div>' +
(det
? '<div style="display:flex;flex-direction:column;gap:0.5rem;margin-bottom:1rem">' + det + '</div>'
: '<div style="font-family:JetBrains Mono,monospace;font-size:0.6rem;color:rgba(255,255,255,0.3);margin-bottom:1rem">NO WALLETS DETECTED</div>') +
(inst
? '<div style="font-family:JetBrains Mono,monospace;font-size:0.55rem;color:rgba(255,255,255,0.25);letter-spacing:1px;margin-bottom:0.5rem">INSTALL A WALLET</div><div style="display:flex;flex-direction:column;gap:0.35rem">' + inst + '</div>'
: '') +
'</div>';
m.classList.add('active');
m.querySelector('.tf-modal-backdrop').onclick = () => m.classList.remove('active');
m.querySelectorAll('.tf-wallet-option').forEach(btn => {
btn.onclick = async () => {
m.classList.remove('active');
try { await window.solWallet.connect(btn.dataset.w); } catch (e) {
console.warn('[TokenForge] Connect failed:', e.message);
}
};
});
}
// ═══════════════════════════════════════════════════════════
// FORM
// ═══════════════════════════════════════════════════════════
function initForm() {
$('#deploy-btn')?.addEventListener('click', handleDeploy);
document.querySelectorAll('.tf-input,.tf-textarea,.tf-select,#revoke-mint,#revoke-freeze').forEach(el => {
el.addEventListener('input', updateDeployBtn);
el.addEventListener('change', updateDeployBtn);
});
}
function formData() {
return {
name: ($('#token-name')?.value || '').trim(),
symbol: ($('#token-symbol')?.value || '').trim().toUpperCase(),
supply: ($('#token-supply')?.value || '').trim(),
decimals: parseInt($('#token-decimals')?.value || '6', 10),
imageUrl: ($('#token-image')?.value || '').trim(),
desc: ($('#token-desc')?.value || '').trim(),
revokeMint: !!$('#revoke-mint')?.checked,
revokeFreeze: !!$('#revoke-freeze')?.checked,
};
}
function validate() {
const d = formData(), e = [];
if (!d.name || d.name.length > 32) e.push('Token name required (max 32 chars)');
if (!d.symbol || d.symbol.length > 10) e.push('Symbol required (max 10 chars)');
// Supply validation
const supNum = Number(d.supply);
if (!d.supply || !Number.isFinite(supNum) || supNum <= 0 || supNum !== Math.floor(supNum)) {
e.push('Supply must be a positive whole number');
} else {
try {
const raw = BigInt(Math.floor(supNum)) * (10n ** BigInt(d.decimals));
if (raw > MAX_U64) e.push('Supply \u00D7 10^decimals exceeds u64 max');
} catch (_) { e.push('Invalid supply number'); }
}
if (d.decimals < 0 || d.decimals > 9) e.push('Decimals must be 0-9');
return e;
}
function updateDeployBtn() {
const b = $('#deploy-btn');
if (b) b.disabled = validate().length > 0 || !walletAddress;
}
// ═══════════════════════════════════════════════════════════
// DEPLOY FLOW
// ═══════════════════════════════════════════════════════════
function handleDeploy() {
const errs = validate();
if (errs.length) { showStatus('error', 'VALIDATION ERROR', errs.join('<br>')); return; }
if (!walletAddress || !window.solWallet?.provider) {
showStatus('error', 'WALLET NOT CONNECTED', 'Connect your wallet first.');
return;
}
showConfirmModal(formData());
}
function showConfirmModal(d) {
let m = $('#tf-confirm-modal');
if (!m) {
m = document.createElement('div');
m.id = 'tf-confirm-modal';
m.className = 'tf-modal';
document.body.appendChild(m);
}
const supFmt = Number(d.supply).toLocaleString();
const hasRevoke = d.revokeMint || d.revokeFreeze;
m.innerHTML =
'<div class="tf-modal-backdrop"></div>' +
'<div class="tf-modal-content">' +
'<div class="tf-modal-title">CONFIRM TOKEN DEPLOYMENT</div>' +
mRow('NAME', esc(d.name)) +
mRow('SYMBOL', esc(d.symbol)) +
mRow('SUPPLY', supFmt) +
mRow('DECIMALS', String(d.decimals)) +
(d.imageUrl ? mRow('IMAGE', '<span style="font-size:0.5rem">' + esc(d.imageUrl.length > 50 ? d.imageUrl.slice(0, 47) + '...' : d.imageUrl) + '</span>') : '') +
(d.revokeMint ? mRow('REVOKE MINT', '<span style="color:#F5A623">YES \u2014 PERMANENT</span>') : '') +
(d.revokeFreeze ? mRow('REVOKE FREEZE', '<span style="color:#F5A623">YES \u2014 PERMANENT</span>') : '') +
mRow('DEPLOYER', truncAddr(walletAddress)) +
'<div style="margin-top:1rem;padding-top:.75rem;border-top:1px solid rgba(245,166,35,.15)">' +
mRow('SERVICE FEE', '0.1 SOL') +
mRow('NETWORK FEES', '~0.015 SOL') +
mRow('ESTIMATED TOTAL', '<span style="color:#F5A623;font-weight:700">~0.115 SOL</span>') +
'</div>' +
(hasRevoke ? '<div class="tf-modal-warning">\u26A0 AUTHORITY REVOCATION IS PERMANENT AND CANNOT BE UNDONE.' +
(d.revokeMint ? ' You will not be able to mint additional tokens.' : '') +
(d.revokeFreeze ? ' You will not be able to freeze accounts.' : '') + '</div>' : '') +
'<div class="tf-modal-warning">\u26A0 THIS IS A REAL ON-CHAIN TRANSACTION. SOL WILL BE DEDUCTED FROM YOUR WALLET. VERIFY ALL DETAILS BEFORE CONFIRMING.</div>' +
'<div class="tf-modal-actions">' +
'<button class="tf-modal-cancel" id="confirm-cancel">CANCEL</button>' +
'<button class="tf-modal-confirm" id="confirm-deploy">DEPLOY TOKEN</button>' +
'</div>' +
'</div>';
m.classList.add('active');
m.querySelector('.tf-modal-backdrop').onclick = () => m.classList.remove('active');
m.querySelector('#confirm-cancel').onclick = () => m.classList.remove('active');
m.querySelector('#confirm-deploy').onclick = () => { m.classList.remove('active'); executeDeploy(d); };
}
async function executeDeploy(data) {
try {
showStatus('building', 'BUILDING TRANSACTION', 'Generating mint keypair and constructing instructions...');
disableForm(true);
const conn = new solanaWeb3.Connection(RPC_URL, 'confirmed');
const payer = new solanaWeb3.PublicKey(walletAddress);
const feeWallet = new solanaWeb3.PublicKey(FEE_WALLET);
const mintKp = solanaWeb3.Keypair.generate();
const mint = mintKp.publicKey;
// Rent exemption for mint account
const mintRent = await conn.getMinimumBalanceForRentExemption(MINT_ACCOUNT_SIZE);
// Derive Associated Token Account address
const [ata] = solanaWeb3.PublicKey.findProgramAddressSync(
[payer.toBytes(), TOKEN_PROGRAM.toBytes(), mint.toBytes()],
ATA_PROGRAM
);
// Derive Metaplex metadata PDA
const [metaPDA] = solanaWeb3.PublicKey.findProgramAddressSync(
[new TextEncoder().encode('metadata'), METADATA_PROGRAM.toBytes(), mint.toBytes()],
METADATA_PROGRAM
);
// Calculate raw supply with decimals
const rawSupply = BigInt(Math.floor(Number(data.supply))) * (10n ** BigInt(data.decimals));
// ── Build instruction list ──
const ixs = [];
// 1. Create mint account (SystemProgram)
ixs.push(solanaWeb3.SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: mint,
space: MINT_ACCOUNT_SIZE,
lamports: mintRent,
programId: TOKEN_PROGRAM,
}));
// 2. InitializeMint2 — SPL Token instruction 20
ixs.push(ixInitMint2(mint, data.decimals, payer, payer));
// 3. Create Associated Token Account (idempotent)
ixs.push(ixCreateATA(payer, ata, payer, mint));
// 4. MintTo — mint initial supply to user's ATA
ixs.push(ixMintTo(mint, ata, payer, rawSupply));
// 5. Create Metaplex metadata
ixs.push(ixCreateMetadata(metaPDA, mint, payer, data));
// 6. Revoke mint authority (optional)
if (data.revokeMint) ixs.push(ixSetAuthority(mint, payer, 0, null));
// 7. Revoke freeze authority (optional)
if (data.revokeFreeze) ixs.push(ixSetAuthority(mint, payer, 1, null));
// 8. Service fee transfer — 0.1 SOL to site wallet
ixs.push(solanaWeb3.SystemProgram.transfer({
fromPubkey: payer,
toPubkey: feeWallet,
lamports: SERVICE_FEE_LAMPORTS,
}));
// ── Build transaction ──
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash('confirmed');
const tx = new solanaWeb3.Transaction();
tx.recentBlockhash = blockhash;
tx.feePayer = payer;
ixs.forEach(ix => tx.add(ix));
// Partial sign with the mint keypair (payer signs via wallet)
tx.partialSign(mintKp);
// ── Sign & send ──
showStatus('awaiting', 'AWAITING WALLET SIGNATURE', 'Please approve the transaction in your wallet...');
let sig;
if (window.solWallet.isWalletStandard) {
sig = await wsSend(tx, conn);
} else {
sig = await legacySend(tx, conn);
}
// ── Confirm on-chain ──
showStatus('confirming', 'CONFIRMING ON-CHAIN',
'Transaction submitted \u2014 awaiting confirmation...<br>' +
rRow('TX', '<a href="' + SOLSCAN_TX + sig + '" target="_blank" rel="noopener">' + truncAddr(sig) + ' \u2197</a>')
);
const conf = await conn.confirmTransaction(
{ signature: sig, blockhash, lastValidBlockHeight },
'confirmed'
);
if (conf.value.err) throw new Error('On-chain failure: ' + JSON.stringify(conf.value.err));
// ── Success ──
const mintAddr = mint.toBase58();
showStatus('success', data.symbol + ' DEPLOYED SUCCESSFULLY',
rRow('TOKEN', esc(data.name) + ' (' + esc(data.symbol) + ')') +
rRow('SUPPLY', Number(data.supply).toLocaleString()) +
rRow('DECIMALS', String(data.decimals)) +
rRow('MINT ADDRESS', '<span style="font-size:0.55rem">' + mintAddr + '</span>') +
rRow('SOLSCAN', '<a href="' + SOLSCAN_TOKEN + mintAddr + '" target="_blank" rel="noopener">View Token \u2197</a>') +
rRow('EXPLORER', '<a href="' + EXPLORER_TX + sig + '" target="_blank" rel="noopener">View Transaction \u2197</a>') +
rRow('TX SIGNATURE', '<a href="' + SOLSCAN_TX + sig + '" target="_blank" rel="noopener" style="font-size:0.5rem">' + truncAddr(sig) + ' \u2197</a>')
);
} catch (err) {
console.error('[TokenForge] Deploy error:', err);
let msg = err.message || 'Unknown error';
if (/reject|denied|cancel|declined|disapproved/i.test(msg))
msg = 'Transaction was rejected by your wallet.';
else if (/insufficient|not enough|0x1/i.test(msg))
msg = 'Insufficient SOL balance. You need approximately 0.115 SOL.';
else if (/blockhash|expired|block height/i.test(msg))
msg = 'Transaction expired. Please try again.';
else if (/network|fetch|failed to fetch/i.test(msg))
msg = 'Network error. Check your connection and try again.';
showStatus('error', 'DEPLOYMENT FAILED', esc(msg));
} finally {
disableForm(false);
}
}
// ═══════════════════════════════════════════════════════════
// SIGN & SEND HELPERS
// ═══════════════════════════════════════════════════════════
async function legacySend(tx, conn) {
const p = window.solWallet.provider;
// Try signAndSendTransaction first (Phantom, Solflare, etc.)
if (typeof p.signAndSendTransaction === 'function') {
try {
const r = await p.signAndSendTransaction(tx, {
skipPreflight: false,
preflightCommitment: 'confirmed',
});
return r.signature || r;
} catch (e) {
if (/reject|denied|cancel|declined|disapproved/i.test(e.message || '')) throw e;
console.warn('[TokenForge] signAndSendTransaction failed, fallback:', e.message);
}
}
// Fallback: signTransaction + manual send
if (typeof p.signTransaction !== 'function')
throw new Error('Wallet does not support transaction signing.');
const signed = await p.signTransaction(tx);
return await conn.sendRawTransaction(signed.serialize(), {
skipPreflight: false,
preflightCommitment: 'confirmed',
});
}
async function wsSend(tx, conn) {
const w = window.solWallet.provider;
const acct = w.accounts?.[0];
if (!acct) throw new Error('No wallet account available.');
const bytes = tx.serialize({ requireAllSignatures: false, verifySignatures: false });
// Try signAndSendTransaction feature
const feat = w.features?.['solana:signAndSendTransaction'];
if (feat) {
try {
const res = await feat.signAndSendTransaction({
account: acct,
transaction: bytes,
chain: 'solana:mainnet',
});
const r = Array.isArray(res) ? res[0] : res;
if (r.signature) {
return typeof r.signature === 'string' ? r.signature : b58(new Uint8Array(r.signature));
}
return r;
} catch (e) {
if (/reject|denied|cancel|declined|disapproved/i.test(e.message || '')) throw e;
console.warn('[TokenForge] WS signAndSend failed, fallback:', e.message);
}
}
// Fallback: signTransaction + manual send
const sf = w.features?.['solana:signTransaction'];
if (!sf) throw new Error('Wallet does not support Solana signing.');
const res = await sf.signTransaction({
account: acct,
transaction: bytes,
chain: 'solana:mainnet',
});
const r = Array.isArray(res) ? res[0] : res;
return await conn.sendRawTransaction(r.signedTransaction, {
skipPreflight: false,
preflightCommitment: 'confirmed',
});
}
// ═══════════════════════════════════════════════════════════
// SPL TOKEN INSTRUCTIONS (manual binary encoding)
// ═══════════════════════════════════════════════════════════
/**
* InitializeMint2 SPL Token instruction index 20
* Data: [20, decimals(u8), mintAuthority(32), freezeOption(u8), freezeAuthority(32)]
* Accounts: [mint(writable)]
*/
function ixInitMint2(mint, decimals, mintAuth, freezeAuth) {
const d = new Uint8Array(67);
d[0] = 20;
d[1] = decimals;
d.set(mintAuth.toBytes(), 2);
d[34] = 1; // COption::Some
d.set(freezeAuth.toBytes(), 35);
return new solanaWeb3.TransactionInstruction({
programId: TOKEN_PROGRAM,
keys: [{ pubkey: mint, isSigner: false, isWritable: true }],
data: d,
});
}
/**
* MintTo SPL Token instruction index 7
* Data: [7, amount(u64 LE)]
* Accounts: [mint(writable), destination(writable), authority(signer)]
*/
function ixMintTo(mint, dest, auth, amount) {
const d = new Uint8Array(9);
d[0] = 7;
writeU64(d, amount, 1);
return new solanaWeb3.TransactionInstruction({
programId: TOKEN_PROGRAM,
keys: [
{ pubkey: mint, isSigner: false, isWritable: true },
{ pubkey: dest, isSigner: false, isWritable: true },
{ pubkey: auth, isSigner: true, isWritable: false },
],
data: d,
});
}
/**
* SetAuthority SPL Token instruction index 6
* Data: [6, authorityType(u8), newAuthorityOption(u8), newAuthority?(32)]
* authorityType: 0=MintTokens, 1=FreezeAccount
* Accounts: [account(writable), currentAuthority(signer)]
*/
function ixSetAuthority(acct, curAuth, authType, newAuth) {
const hasNew = newAuth !== null;
const d = new Uint8Array(hasNew ? 35 : 3);
d[0] = 6;
d[1] = authType;
d[2] = hasNew ? 1 : 0;
if (hasNew) d.set(newAuth.toBytes(), 3);
return new solanaWeb3.TransactionInstruction({
programId: TOKEN_PROGRAM,
keys: [
{ pubkey: acct, isSigner: false, isWritable: true },
{ pubkey: curAuth, isSigner: true, isWritable: false },
],
data: d,
});
}
/**
* CreateIdempotent Associated Token Account Program instruction 1
* Data: [1]
* Accounts: [payer(s,w), ata(w), owner, mint, systemProgram, tokenProgram]
*/
function ixCreateATA(payer, ata, owner, mint) {
return new solanaWeb3.TransactionInstruction({
programId: ATA_PROGRAM,
keys: [
{ pubkey: payer, isSigner: true, isWritable: true },
{ pubkey: ata, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: false, isWritable: false },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM, isSigner: false, isWritable: false },
],
data: new Uint8Array([1]),
});
}
// ═══════════════════════════════════════════════════════════
// METAPLEX TOKEN METADATA INSTRUCTION
// ═══════════════════════════════════════════════════════════
/**
* CreateMetadataAccountV3 Metaplex instruction index 33
* Borsh-serialized: [33, DataV2(...), isMutable(bool), collectionDetails(Option)]
*/
function ixCreateMetadata(metaPDA, mint, authority, tokenData) {
const enc = new TextEncoder();
const nameB = enc.encode(tokenData.name);
const symB = enc.encode(tokenData.symbol);
const uriB = enc.encode(tokenData.imageUrl || '');
// Layout: 1(ix) + string(name) + string(symbol) + string(uri) + u16 + opt + opt + opt + bool + opt
// Borsh string = 4-byte u32 LE length prefix + UTF-8 bytes
const sz = 1
+ (4 + nameB.length)
+ (4 + symB.length)
+ (4 + uriB.length)
+ 2 // seller_fee_basis_points
+ 1 // creators: None
+ 1 // collection: None
+ 1 // uses: None
+ 1 // is_mutable
+ 1; // collection_details: None
const d = new Uint8Array(sz);
let o = 0;
d[o++] = 33; // instruction index
// DataV2.name
writeU32(d, nameB.length, o); o += 4;
d.set(nameB, o); o += nameB.length;
// DataV2.symbol
writeU32(d, symB.length, o); o += 4;
d.set(symB, o); o += symB.length;
// DataV2.uri
writeU32(d, uriB.length, o); o += 4;
d.set(uriB, o); o += uriB.length;
// DataV2.seller_fee_basis_points (u16 LE) — 0 for fungible tokens
d[o++] = 0; d[o++] = 0;
// DataV2.creators: None
d[o++] = 0;
// DataV2.collection: None
d[o++] = 0;
// DataV2.uses: None
d[o++] = 0;
// is_mutable: true
d[o++] = 1;
// collection_details: None
d[o++] = 0;
return new solanaWeb3.TransactionInstruction({
programId: METADATA_PROGRAM,
keys: [
{ pubkey: metaPDA, isSigner: false, isWritable: true }, // metadata PDA
{ pubkey: mint, isSigner: false, isWritable: false }, // mint
{ pubkey: authority, isSigner: true, isWritable: false }, // mint authority
{ pubkey: authority, isSigner: true, isWritable: true }, // payer
{ pubkey: authority, isSigner: false, isWritable: false }, // update authority
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false }, // system program
{ pubkey: RENT_SYSVAR, isSigner: false, isWritable: false }, // rent (optional but safe)
],
data: d,
});
}
// ═══════════════════════════════════════════════════════════
// BYTE HELPERS
// ═══════════════════════════════════════════════════════════
function writeU32(buf, val, off) {
buf[off] = val & 0xFF;
buf[off + 1] = (val >> 8) & 0xFF;
buf[off + 2] = (val >> 16) & 0xFF;
buf[off + 3] = (val >> 24) & 0xFF;
}
function writeU64(buf, val, off) {
let v = BigInt(val);
for (let i = 0; i < 8; i++) {
buf[off + i] = Number(v & 0xFFn);
v >>= 8n;
}
}
/** Base58 encode a Uint8Array (for WalletStandard signature conversion) */
function b58(bytes) {
const A = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let r = '', n = 0n;
for (const b of bytes) n = n * 256n + BigInt(b);
while (n > 0n) { r = A[Number(n % 58n)] + r; n /= 58n; }
for (const b of bytes) { if (b === 0) r = '1' + r; else break; }
return r || '1';
}
// ═══════════════════════════════════════════════════════════
// UI HELPERS
// ═══════════════════════════════════════════════════════════
function showStatus(state, title, body) {
const panel = $('#status-panel');
if (!panel) return;
panel.className = 'tf-status active ' + state;
const sp = $('#status-spinner');
if (sp) sp.style.display = (state === 'success' || state === 'error') ? 'none' : 'inline-block';
const t = $('#status-title');
if (t) t.textContent = title;
const b = $('#status-body');
if (b) b.innerHTML = body;
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function disableForm(yes) {
document.querySelectorAll('.tf-input,.tf-textarea,.tf-select,#revoke-mint,#revoke-freeze').forEach(el => el.disabled = yes);
const btn = $('#deploy-btn');
if (btn) btn.disabled = yes;
}
/** Result row for status body */
function rRow(label, value) {
return '<div class="tf-result-row"><span class="tf-result-label">' + label + '</span><span class="tf-result-value">' + value + '</span></div>';
}
/** Modal confirm row */
function mRow(label, value) {
return '<div class="tf-modal-row"><span class="tf-modal-label">' + label + '</span><span class="tf-modal-value">' + value + '</span></div>';
}
function truncAddr(a) {
if (!a || a.length < 12) return a || '';
return a.slice(0, 4) + '...' + a.slice(-4);
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
})();

186
tokenlauncher/index.html Normal file
View file

@ -0,0 +1,186 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JAESWIFT // TOKEN FORGE</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/section.css">
<link rel="stylesheet" href="/css/tokenlauncher.css">
</head>
<body>
<div class="scanline-overlay"></div>
<div class="grid-bg"></div>
<nav class="nav-main" id="navbar">
<div class="nav-container">
<a href="/" class="nav-logo">
<span class="logo-bracket">[</span> JAE <span class="logo-bracket">]</span>
</a>
<button class="nav-toggle" id="navToggle" aria-label="Menu">
<span></span><span></span><span></span>
</button>
<ul class="nav-menu" id="navMenu"></ul>
<div class="nav-status">
<span class="nav-clock" id="navClock"></span>
</div>
</div>
</nav>
<div class="breadcrumb">
<a href="/">HOME</a>
<span class="separator">/</span>
<a href="/armoury">ARMOURY</a>
<span class="separator">/</span>
<a href="/armoury/lab.html">LAB</a>
<span class="separator">/</span>
<span class="current">TOKEN FORGE</span>
</div>
<section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);">
<div class="section-header-label">LAB // SPL TOKEN LAUNCHER</div>
<h1 class="section-header-title">TOKEN FORGE</h1>
<p class="section-header-sub">&gt; Deploy custom SPL tokens on Solana. Set name, symbol, supply, and metadata &mdash; launch in one transaction.</p>
</section>
<div class="tf-container">
<!-- Wallet Bar -->
<div class="tf-wallet-bar">
<div class="tf-wallet-status" id="wallet-status">&#9675; NOT CONNECTED</div>
<button class="tf-wallet-btn" id="wallet-btn">CONNECT WALLET</button>
</div>
<!-- Status Panel (hidden by default, shown during deploy) -->
<div class="tf-status" id="status-panel">
<div class="tf-status-header">
<div class="tf-spinner" id="status-spinner"></div>
<div class="tf-status-title" id="status-title">BUILDING TRANSACTION</div>
</div>
<div class="tf-status-body" id="status-body"></div>
</div>
<!-- Token Configuration Form -->
<div class="tf-form">
<div class="tf-form-title">TOKEN CONFIGURATION</div>
<div class="tf-form-grid">
<!-- Token Name -->
<div class="tf-field">
<label class="tf-label" for="token-name">TOKEN NAME</label>
<input type="text" class="tf-input" id="token-name" placeholder="e.g. JAESWIFT TOKEN" maxlength="32" autocomplete="off" spellcheck="false">
<div class="tf-label-hint">Max 32 characters. Displayed on explorers and wallets.</div>
</div>
<!-- Token Symbol -->
<div class="tf-field">
<label class="tf-label" for="token-symbol">SYMBOL / TICKER</label>
<input type="text" class="tf-input" id="token-symbol" placeholder="e.g. JAE" maxlength="10" autocomplete="off" spellcheck="false" style="text-transform:uppercase">
<div class="tf-label-hint">Max 10 characters. The trading ticker for your token.</div>
</div>
<!-- Total Supply -->
<div class="tf-field">
<label class="tf-label" for="token-supply">TOTAL SUPPLY</label>
<input type="number" class="tf-input" id="token-supply" placeholder="e.g. 1000000000" min="1" step="1" autocomplete="off">
<div class="tf-label-hint">Whole number. The total number of tokens to mint.</div>
</div>
<!-- Decimals -->
<div class="tf-field">
<label class="tf-label" for="token-decimals">DECIMALS</label>
<select class="tf-select" id="token-decimals">
<option value="0">0 &mdash; No fractions</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6" selected>6 &mdash; Standard (like USDC)</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9 &mdash; Maximum</option>
</select>
<div class="tf-label-hint">6 is standard for most tokens. Determines fractional precision.</div>
</div>
<!-- Token Image URL -->
<div class="tf-field full-width">
<label class="tf-label" for="token-image">TOKEN IMAGE URL</label>
<input type="url" class="tf-input" id="token-image" placeholder="https://example.com/token-logo.png" autocomplete="off" spellcheck="false">
<div class="tf-label-hint">Optional. URL to your token&rsquo;s logo image (PNG/SVG). Must be publicly accessible and hosted elsewhere.</div>
</div>
<!-- Description -->
<div class="tf-field full-width">
<label class="tf-label" for="token-desc">DESCRIPTION</label>
<textarea class="tf-textarea" id="token-desc" placeholder="A brief description of your token..." rows="3" spellcheck="false"></textarea>
<div class="tf-label-hint">Optional. Shown on token explorers and metadata records.</div>
</div>
<!-- Authority Revocation -->
<div class="tf-checkboxes">
<label class="tf-checkbox-wrap">
<input type="checkbox" id="revoke-mint" checked>
<div class="tf-checkbox-text">
<span class="tf-checkbox-label">REVOKE MINT AUTHORITY</span>
<span class="tf-checkbox-rec">RECOMMENDED &mdash; Prevents creating more tokens after launch</span>
</div>
</label>
<label class="tf-checkbox-wrap">
<input type="checkbox" id="revoke-freeze" checked>
<div class="tf-checkbox-text">
<span class="tf-checkbox-label">REVOKE FREEZE AUTHORITY</span>
<span class="tf-checkbox-rec">RECOMMENDED &mdash; Prevents freezing holder wallets</span>
</div>
</label>
</div>
<!-- Fee Breakdown -->
<div class="tf-fee-section">
<div class="tf-fee-title">COST BREAKDOWN</div>
<div class="tf-fee-row">
<span class="tf-fee-label">SERVICE FEE</span>
<span class="tf-fee-value">0.1 SOL</span>
</div>
<div class="tf-fee-row">
<span class="tf-fee-label">NETWORK FEES (rent + tx)</span>
<span class="tf-fee-value">~0.015 SOL</span>
</div>
<div class="tf-fee-row tf-fee-total">
<span class="tf-fee-label">ESTIMATED TOTAL</span>
<span class="tf-fee-value">~0.115 SOL</span>
</div>
</div>
<!-- Deploy Button -->
<div class="tf-deploy-section">
<button class="tf-deploy-btn" id="deploy-btn" disabled>DEPLOY TOKEN</button>
</div>
</div>
</div>
</div>
<footer class="footer">
<div class="footer-container">
<div class="footer-left">
<span class="footer-logo">[JAE]</span>
<span class="footer-copy">&copy; 2026 JAESWIFT.XYZ</span>
</div>
<div class="footer-right">
<span class="footer-signal">SIGNAL &#9608;&#9608;&#9608;&#9608;<span class="signal-flicker">&#9608;</span></span>
</div>
</div>
</footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/clock.js"></script>
<script src="https://unpkg.com/@solana/web3.js@1.98.0/lib/index.iife.min.js"></script>
<script src="/js/tokenlauncher.js"></script>
</body>
</html>