663 lines
30 KiB
JavaScript
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;
|
|
}
|
|
})();
|