/* 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; } })();