jaeswift-website/js/tokenlauncher.js

663 lines
30 KiB
JavaScript

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