diff --git a/armoury/lab.html b/armoury/lab.html index 16d2af1..dc76f8d 100644 --- a/armoury/lab.html +++ b/armoury/lab.html @@ -35,6 +35,13 @@ box-shadow: 0 4px 20px rgba(20, 241, 149, 0.15); 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 { font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; @@ -125,6 +132,18 @@
jaeswift.xyz/soldomains
+ + +
◆ EXPERIMENTAL
+
TOKEN FORGE
+
Deploy custom SPL tokens on Solana. Set name, symbol, supply, and metadata — launch in one transaction.
+
+ SOLANA + SPL + TOKENS +
+
jaeswift.xyz/tokenlauncher
+
diff --git a/css/tokenlauncher.css b/css/tokenlauncher.css new file mode 100644 index 0000000..e95428a --- /dev/null +++ b/css/tokenlauncher.css @@ -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; } diff --git a/js/tokenlauncher.js b/js/tokenlauncher.js new file mode 100644 index 0000000..5f29420 --- /dev/null +++ b/js/tokenlauncher.js @@ -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 => + '' + ).join(''); + const inst = known.filter(k => !names.has(k.name)).map(k => + '' + k.icon + ' ' + esc(k.name) + ' \u2014 INSTALL \u2197' + ).join(''); + + m.innerHTML = + '
' + + '
' + + '
SELECT WALLET
' + + (det + ? '
' + det + '
' + : '
NO WALLETS DETECTED
') + + (inst + ? '
INSTALL A WALLET
' + inst + '
' + : '') + + '
'; + + 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('
')); 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 = + '
' + + '
' + + '
CONFIRM TOKEN DEPLOYMENT
' + + mRow('NAME', esc(d.name)) + + mRow('SYMBOL', esc(d.symbol)) + + mRow('SUPPLY', supFmt) + + mRow('DECIMALS', String(d.decimals)) + + (d.imageUrl ? mRow('IMAGE', '' + esc(d.imageUrl.length > 50 ? d.imageUrl.slice(0, 47) + '...' : d.imageUrl) + '') : '') + + (d.revokeMint ? mRow('REVOKE MINT', 'YES \u2014 PERMANENT') : '') + + (d.revokeFreeze ? mRow('REVOKE FREEZE', 'YES \u2014 PERMANENT') : '') + + mRow('DEPLOYER', truncAddr(walletAddress)) + + '
' + + mRow('SERVICE FEE', '0.1 SOL') + + mRow('NETWORK FEES', '~0.015 SOL') + + mRow('ESTIMATED TOTAL', '~0.115 SOL') + + '
' + + (hasRevoke ? '
\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.' : '') + '
' : '') + + '
\u26A0 THIS IS A REAL ON-CHAIN TRANSACTION. SOL WILL BE DEDUCTED FROM YOUR WALLET. VERIFY ALL DETAILS BEFORE CONFIRMING.
' + + '
' + + '' + + '' + + '
' + + '
'; + + 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...
' + + rRow('TX', '' + truncAddr(sig) + ' \u2197') + ); + + 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', '' + mintAddr + '') + + rRow('SOLSCAN', 'View Token \u2197') + + rRow('EXPLORER', 'View Transaction \u2197') + + rRow('TX SIGNATURE', '' + truncAddr(sig) + ' \u2197') + ); + + } 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 '
' + label + '' + value + '
'; + } + + /** Modal confirm row */ + function mRow(label, value) { + return '
' + label + '' + value + '
'; + } + + 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; + } +})(); diff --git a/tokenlauncher/index.html b/tokenlauncher/index.html new file mode 100644 index 0000000..698fd05 --- /dev/null +++ b/tokenlauncher/index.html @@ -0,0 +1,186 @@ + + + + + + JAESWIFT // TOKEN FORGE + + + + + + + +
+
+ + + + + +
+
LAB // SPL TOKEN LAUNCHER
+

TOKEN FORGE

+

> Deploy custom SPL tokens on Solana. Set name, symbol, supply, and metadata — launch in one transaction.

+
+ +
+ + +
+
○ NOT CONNECTED
+ +
+ + +
+
+
+
BUILDING TRANSACTION
+
+
+
+ + +
+
TOKEN CONFIGURATION
+
+ + +
+ + +
Max 32 characters. Displayed on explorers and wallets.
+
+ + +
+ + +
Max 10 characters. The trading ticker for your token.
+
+ + +
+ + +
Whole number. The total number of tokens to mint.
+
+ + +
+ + +
6 is standard for most tokens. Determines fractional precision.
+
+ + +
+ + +
Optional. URL to your token’s logo image (PNG/SVG). Must be publicly accessible and hosted elsewhere.
+
+ + +
+ + +
Optional. Shown on token explorers and metadata records.
+
+ + +
+ + +
+ + +
+
COST BREAKDOWN
+
+ SERVICE FEE + 0.1 SOL +
+
+ NETWORK FEES (rent + tx) + ~0.015 SOL +
+
+ ESTIMATED TOTAL + ~0.115 SOL +
+
+ + +
+ +
+ +
+
+ +
+ + + + + + + + + +