You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

838 lines
36 KiB

/* global solanaWeb3 */
(() => {
const { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction } = solanaWeb3;
// DOM helpers
const $ = (id) => document.getElementById(id);
const DEBUG = new URLSearchParams(location.search).has('debug');
const log = (m, cls='') => {
if (cls === 'err') console.error(m);
else if (cls === 'warn') console.warn(m);
if (DEBUG) console.debug(m);
};
// SNS (Solana Name Service) resolution
const snsCache = new Map();
const NAME_PROGRAM_ID = new PublicKey('namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX');
async function findOwnedNameAccountsForUser(connection, userAccount) {
try {
const filters = [
{
memcmp: {
offset: 32,
bytes: userAccount.toBase58(),
},
},
];
const accounts = await connection.getProgramAccounts(NAME_PROGRAM_ID, {
filters,
});
return accounts.map((a) => a.publicKey);
} catch (e) {
return [];
}
}
async function resolveSNSAddress(address) {
try {
if (snsCache.has(address)) {
return snsCache.get(address);
}
const { cluster } = getCfg();
const conn = new Connection(cluster, 'confirmed');
const publicKey = new PublicKey(address);
// Find owned name accounts
const nameAccounts = await findOwnedNameAccountsForUser(conn, publicKey);
if (nameAccounts.length > 0) {
// Get the first domain name account data
const accountInfo = await conn.getAccountInfo(nameAccounts[0]);
if (accountInfo && accountInfo.data) {
// Parse the domain name from account data
// SNS account structure: [96 bytes header] + [4 bytes name length] + [name data]
const data = accountInfo.data;
if (data.length > 100) {
const nameLength = data.readUInt32LE(96);
if (nameLength > 0 && nameLength < 64) {
const domainBytes = data.slice(100, 100 + nameLength);
const domain = new TextDecoder().decode(domainBytes);
if (domain && domain.includes('.')) {
snsCache.set(address, domain);
return domain;
}
}
}
}
}
// If no SNS name found, cache the negative result
snsCache.set(address, null);
return null;
} catch (e) {
// Silently fail and return null
snsCache.set(address, null);
return null;
}
}
async function formatAddressWithSNS(address) {
const snsName = await resolveSNSAddress(address);
if (snsName) {
return snsName;
}
return `${address.slice(0,4)}${address.slice(-4)}`;
}
// Providers
function standardToLegacyProvider(stdWallet) {
const features = stdWallet?.features || {};
const connectFeature = features['standard:connect'];
const eventsFeature = features['standard:events'];
const disconnectFeature = features['standard:disconnect'];
const signTxFeature = features['solana:signTransactions'];
const signAndSendFeature = features['solana:signAndSendTransaction'];
const signMsgFeature = features['solana:signMessage'];
let currentAccount = null;
const listeners = { connect: [], disconnect: [] };
const emit = (evt, ...args) => (listeners[evt] || []).forEach((fn) => {
try { fn(...args); } catch {}
});
// Forward standard events into legacy-style events
if (eventsFeature?.on) {
try {
eventsFeature.on('change', ({ accounts }) => {
if (accounts && accounts[0]) {
currentAccount = accounts[0];
provider.publicKey = {
toString: () => accounts[0].address,
toBase58: () => accounts[0].address
};
emit('connect');
} else {
currentAccount = null;
provider.publicKey = null;
emit('disconnect');
}
});
} catch {}
}
const provider = {
isStandard: true,
publicKey: null,
on: (evt, fn) => {
if (!listeners[evt]) listeners[evt] = [];
listeners[evt].push(fn);
},
off: (evt, fn) => {
listeners[evt] = (listeners[evt] || []).filter((f) => f !== fn);
},
connect: async () => {
if (!connectFeature?.connect) throw new Error('Wallet Standard connect not available');
const { accounts } = await connectFeature.connect();
if (!accounts?.length) throw new Error('No account returned');
currentAccount = accounts[0];
provider.publicKey = {
toString: () => accounts[0].address,
toBase58: () => accounts[0].address
};
emit('connect');
return { publicKey: provider.publicKey };
},
disconnect: async () => {
try {
if (disconnectFeature?.disconnect) await disconnectFeature.disconnect();
} finally {
currentAccount = null;
provider.publicKey = null;
emit('disconnect');
}
},
signTransaction: async (tx) => {
if (signTxFeature?.signTransactions) {
const signed = await signTxFeature.signTransactions({ transactions: [tx] });
// Some implementations return { signedTransactions }, others return array directly
const arr = Array.isArray(signed) ? signed : (signed?.signedTransactions || []);
if (!arr[0]) throw new Error('signTransactions returned no result');
return arr[0];
}
throw new Error('Wallet Standard wallet does not support signTransactions');
},
_standard: {
signTransactions: signTxFeature?.signTransactions,
signAndSendTransaction: signAndSendFeature?.signAndSendTransaction,
signMessage: signMsgFeature?.signMessage
}
};
return provider;
}
function detectProviders() {
const map = new Map();
const add = (id, name, provider) => { if (provider && !map.has(id)) map.set(id, { id, name, provider }); };
// Injected
add('phantom','Phantom', window.solana?.isPhantom ? window.solana : null);
add('solflare','Solflare', window.solflare?.isSolflare ? window.solflare : null);
add('backpack','Backpack', window.backpack?.isBackpack ? window.backpack : null);
add('glow','Glow', window.glow?.solana || null);
add('exodus','Exodus', window.exodus?.solana || null);
// Wallet Standard (if present)
try {
const std = window.navigator?.wallets?.get?.();
if (Array.isArray(std)) {
for (const w of std) {
const id = w.name?.toLowerCase?.().replace(/\s+/g,'-') || 'standard';
add(id, w.name || id, standardToLegacyProvider(w));
}
}
} catch {}
return Array.from(map.values());
}
function populateWalletSelect(){
const select = $('walletSelect'); if(!select) return;
const detected = detectProviders();
// Keep first option as Auto-detect, remove the rest, then append detected
select.innerHTML = '<option value="auto">Auto-detect</option>' + detected.map(d=>`<option value="${d.id}">${d.name}</option>`).join('');
// If none detected, keep auto only
}
// Centralized ticker functions
async function loadTickerFromServer() {
try {
const response = await fetch('ticker-api.php', {
method: 'GET',
cache: 'no-cache'
});
if (!response.ok) return;
const tickerData = await response.json();
const track = document.getElementById('ticker');
if (!track) return;
// Clear existing ticker items
track.innerHTML = '';
// Add items from server (reverse order since server stores newest first)
for (const item of tickerData.reverse()) {
const span = document.createElement('span');
span.className = 'ticker-item ' + (item.type || '');
// Try to resolve SNS names in existing ticker text
let displayText = item.text;
if (item.snsName) {
// If SNS name is already stored, use it
displayText = displayText.replace(/[A-Za-z0-9]{4}…[A-Za-z0-9]{4}/, item.snsName);
} else {
// Try to extract address and resolve SNS
const addressMatch = displayText.match(/([A-Za-z0-9]{4})…([A-Za-z0-9]{4})/);
if (addressMatch && item.fullAddress) {
const snsName = await resolveSNSAddress(item.fullAddress);
if (snsName) {
displayText = displayText.replace(addressMatch[0], snsName);
}
}
}
span.textContent = displayText;
track.appendChild(span);
}
} catch (e) {
log('Failed to load ticker data from server', 'warn');
}
}
async function saveTickerToServer(text, type, fullAddress = null, snsName = null) {
try {
const payload = { text, type };
if (fullAddress) payload.fullAddress = fullAddress;
if (snsName) payload.snsName = snsName;
await fetch('ticker-api.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
} catch (e) {
log('Failed to save ticker item to server', 'warn');
}
}
async function pushTickerItem(text, cls, address = null) {
const track = document.getElementById('ticker');
if (!track) return;
let displayText = text;
let snsName = null;
// Try to resolve SNS name if address is provided
if (address) {
snsName = await resolveSNSAddress(address);
if (snsName) {
displayText = text.replace(/[A-Za-z0-9]{4}…[A-Za-z0-9]{4}/, snsName);
}
}
// Add to local DOM immediately for responsiveness
const span = document.createElement('span');
span.className = 'ticker-item ' + (cls || '');
span.textContent = displayText;
track.appendChild(span);
// Limit items to prevent DOM bloat (keep more than server since DOM updates faster)
while (track.childNodes.length > 50) {
track.removeChild(track.firstChild);
}
// Save to server for other users
saveTickerToServer(displayText, cls || '', address, snsName);
}
// Updated discriminator functions for our new program
async function getDiscriminators(){
const buyTokens = await anchorDisc('buy_tokens');
const sellTokens = await anchorDisc('sell_tokens');
return { buy: buyTokens, sell: sellTokens };
}
function b64ToBytes(b64){ const bin = atob(b64); const arr = new Uint8Array(bin.length); for(let i=0;i<bin.length;i++) arr[i]=bin.charCodeAt(i); return arr; }
function startsWith(buf, prefix){ if(prefix.length>buf.length) return false; for(let i=0;i<prefix.length;i++){ if(buf[i]!==prefix[i]) return false; } return true; }
function readLeU64(bytes, offset){
let v=0n; for(let i=0;i<8;i++){ v |= BigInt(bytes[offset+i]) << (8n*BigInt(i)); }
return v;
}
let __seenSigs = new Set();
async function pollRecentActivity(){
try{
const { programId, cluster } = getCfg();
const conn = new Connection(cluster,'confirmed');
const discs = await getDiscriminators();
const sigs = await conn.getSignaturesForAddress(programId, { limit: 15 });
for(const s of sigs){
if(__seenSigs.has(s.signature)) continue;
__seenSigs.add(s.signature);
const tx = await conn.getTransaction(s.signature, { maxSupportedTransactionVersion: 0 });
if(!tx?.transaction) continue;
const msg = tx.transaction.message;
const keys = msg.accountKeys.map(k=>k.toString());
const feePayer = keys[0] || '';
const ixList = msg.instructions || msg.compiledInstructions || [];
for(const ix of ixList){
const progIdx = ix.programIdIndex ?? undefined;
const prog = progIdx !== undefined ? keys[progIdx] : ix.programId?.toString();
if(prog !== programId.toString()) continue;
const dataB64 = ix.data || '';
const data = typeof dataB64 === 'string' ? b64ToBytes(dataB64) : new Uint8Array();
if(startsWith(data, discs.buy)){
const lamports = Number(readLeU64(data, 8));
const sol = lamports / 1_000_000_000;
await pushTickerItem(`BUY ${sol.toFixed(3)} SOL • ${feePayer.slice(0,4)}${feePayer.slice(-4)}`, 'buy', feePayer);
} else if(startsWith(data, discs.sell)){
const tokensRaw = Number(readLeU64(data, 8));
const t = tokensRaw / 1_000_000_000;
await pushTickerItem(`SELL ${t.toFixed(3)} tk • ${feePayer.slice(0,4)}${feePayer.slice(-4)}`, 'sell', feePayer);
}
}
}
}catch(e){ /*silent*/ }
}
function pickProvider() {
const sel = $('walletSelect').value;
const detected = detectProviders();
if (sel !== 'auto') return detected.find(p => p.id === sel)?.provider;
return detected[0]?.provider;
}
// Buffer helpers
function le64(n) { const b=new ArrayBuffer(8); new DataView(b).setBigUint64(0, BigInt(n), true); return new Uint8Array(b); }
async function anchorDisc(name){ const data=new TextEncoder().encode(`global:${name}`); const h=await crypto.subtle.digest('SHA-256', data); return new Uint8Array(h).slice(0,8); }
async function buildIx(name, args){ const disc=await anchorDisc(name); const out=new Uint8Array(disc.length+args.length); out.set(disc,0); out.set(args,disc.length); return out; }
// ATAs
const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const ATA_PROGRAM_ID = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
async function findAta(mint, owner){
const [addr] = await PublicKey.findProgramAddress([
owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()
], ATA_PROGRAM_ID);
return addr;
}
function createAtaIx(payer, ata, owner, mint){
const 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:SystemProgram.programId, isSigner:false, isWritable:false },
{ pubkey:TOKEN_PROGRAM_ID, isSigner:false, isWritable:false },
{ pubkey:solanaWeb3.SYSVAR_RENT_PUBKEY, isSigner:false, isWritable:false },
];
return new TransactionInstruction({ keys, programId: ATA_PROGRAM_ID, data: new Uint8Array([]) });
}
// Config via hidden meta tag
function getCfg(){
const meta = document.getElementById('app-config');
const cluster = meta?.dataset.cluster || 'https://api.devnet.solana.com';
const programIdStr = meta?.dataset.programId || '';
const mintStr = meta?.dataset.mint || '';
const devWalletStr = meta?.dataset.devWallet || '';
const programId = programIdStr ? new PublicKey(programIdStr) : null;
const mint = mintStr ? new PublicKey(mintStr) : null;
const devWallet = devWalletStr ? new PublicKey(devWalletStr) : null;
return { programId, mint, cluster, devWallet };
}
async function applyConfig(){
try{
const res = await fetch('config.json', { cache:'no-store' });
if(!res.ok) return;
const cfg = await res.json();
const meta = document.getElementById('app-config'); if(!meta) return;
if(cfg.cluster) meta.dataset.cluster = cfg.cluster;
if(cfg.programId) meta.dataset.programId = cfg.programId;
if(cfg.mint) meta.dataset.mint = cfg.mint;
if(cfg.devWallet) meta.dataset.devWallet = cfg.devWallet;
}catch(e){ /*silent*/ }
}
let pendingReferrer = '';
async function readRefFromUrl(){
const params = new URLSearchParams(location.search);
const ref = params.get('ref');
if(ref){
const el = $('referrer');
if (el) el.value = ref; else pendingReferrer = ref;
}
}
let walletProvider = null; let walletPubkey = null;
function shortAddr(pk){ const s=pk.toBase58(); return s.slice(0,4)+'…'+s.slice(-4); }
function updateUIDisconnected(){
walletPubkey = null;
const chip = $('walletChip'); if(chip){ chip.textContent = 'Not connected'; chip.style.display='none'; }
const wa = $('walletAddr'); if(wa) wa.value = '';
const ref = $('reflinkRow'); if(ref) ref.style.display = 'none';
const ap = $('actionsPanel'); if(ap) ap.style.display = 'none';
const d = $('disconnectBtn'); if(d) d.style.display = 'none';
const c = $('connectBtn'); if(c) c.style.display = '';
const st = $('statusValue'); if(st) st.value = 'Connect wallet to participate';
}
function updateUIConnected(){
if(!walletPubkey) return;
const chip = $('walletChip'); if(chip){ chip.textContent = shortAddr(walletPubkey); chip.style.display=''; }
const ref = $('reflinkRow'); if(ref) ref.style.display = '';
const ap = $('actionsPanel'); if(ap) ap.style.display = '';
const d = $('disconnectBtn'); if(d) d.style.display = '';
const c = $('connectBtn'); if(c) c.style.display = 'none';
const st = $('statusValue'); if(st) st.value = 'Ready';
}
async function disconnectWallet(){
try{
if(walletProvider && typeof walletProvider.disconnect === 'function'){
try { await walletProvider.disconnect(); } catch { /* some wallets throw if not connected */ }
}
} finally {
walletProvider = null;
updateUIDisconnected();
}
}
async function connectWallet(){
try{
walletProvider = pickProvider();
if(!walletProvider){ log('No compatible wallet detected. Install Phantom/Solflare/Backpack/Glow/Exodus.', 'err'); return; }
// Attach events if provided by wallet
try{
if (walletProvider.on) {
walletProvider.on('connect', () => { if(walletProvider?.publicKey){ walletPubkey = new PublicKey(walletProvider.publicKey.toString()); $('walletAddr').value = walletPubkey.toBase58(); updateUIConnected(); updateUserBalance(); updateReflink(); } });
walletProvider.on('disconnect', () => { updateUIDisconnected(); });
}
}catch{}
const res = await walletProvider.connect();
const pubkey = new PublicKey((res?.publicKey ?? walletProvider.publicKey).toString());
walletPubkey = pubkey; $('walletAddr').value = pubkey.toBase58();
await readRefFromUrl();
log(`Connected ${pubkey.toBase58()}`, 'ok');
updateUIConnected();
updateUserBalance();
updateReflink();
// Check if user account exists and toggle Prepare button visibility
const { programId } = getCfg();
await withConn(async (conn)=>{
const exists = await userExists(conn, programId, walletPubkey);
const prep = document.getElementById('registerBtn');
if (prep) prep.style.display = exists ? 'none' : '';
const st = $('statusValue'); if(st) st.value = exists ? 'Ready' : 'Ready — account will be prepared on your first purchase';
});
}catch(e){ log(`Connect failed: ${e.message}`,'err'); console.error(e); }
}
async function withConn(fn){ const {cluster}=getCfg(); const conn=new Connection(cluster,'confirmed'); return fn(conn); }
async function userExists(conn, programId, owner){
try {
const [userPda] = await PublicKey.findProgramAddress([new TextEncoder().encode('user'), owner.toBuffer()], programId);
const info = await conn.getAccountInfo(userPda);
return !!info;
} catch { return false; }
}
function logSendError(e){
try{
if (e && typeof e.getLogs === 'function') {
e.getLogs().then((logs)=>console.error('Tx logs:', logs)).catch(()=>{});
} else if (e?.logs) {
console.error('Tx logs:', e.logs);
}
}catch{}
}
async function ensureAta(conn, owner, mint){
const ata = await findAta(mint, owner);
const acc = await conn.getAccountInfo(ata);
if(!acc) return { ata, createIx: createAtaIx(owner, ata, owner, mint) };
return { ata };
}
// Actions - Updated for our new program
let __lastPot = null;
async function updatePot(){
try{
const { programId, cluster } = getCfg();
if(!programId){ const s=$('statusValue'); if(s) s.value='Not configured'; return; }
const conn = new Connection(cluster,'confirmed');
// Use our new vault PDA
const [vaultPda] = await PublicKey.findProgramAddress([new TextEncoder().encode('vault')], programId);
const lamports = await conn.getBalance(vaultPda, 'confirmed');
const solNum = lamports/1_000_000_000;
const sol = solNum.toFixed(3);
const potEl = $('potValue'); if(potEl) potEl.value = `${sol} SOL`;
const potEl2 = $('potValue2'); if(potEl2) potEl2.value = `${sol} SOL`;
const potBig = document.getElementById('potBig'); if(potBig) potBig.textContent = sol;
// delta animation
const deltaEl = document.getElementById('potDelta');
if(deltaEl !== null){
if(__lastPot !== null){
const d = solNum - __lastPot;
if(Math.abs(d) >= 0.001){
deltaEl.textContent = (d>0?'+':'')+d.toFixed(3);
deltaEl.classList.remove('pos','neg','show');
deltaEl.classList.add(d>0?'pos':'neg');
// force reflow for animation restart
void deltaEl.offsetWidth;
deltaEl.classList.add('show');
setTimeout(()=>deltaEl.classList.remove('show'), 1200);
}
}
__lastPot = solNum;
}
}catch(e){ /*silent*/ }
}
async function updateUserBalance(){
try{
if(!walletPubkey) return;
const { mint, cluster } = getCfg();
const conn = new Connection(cluster,'confirmed');
const ata = await findAta(mint, walletPubkey);
const bal = await conn.getTokenAccountBalance(ata).catch(()=>null);
const v = bal?.value?.uiAmountString ?? '0';
const el = $('userBalance'); if(el) el.value = v;
// Auto-populate sell field with user's raw token balance (9 decimals)
const rawAmount = bal?.value?.amount ?? '0';
const sellEl = $('sellAmount'); if(sellEl) sellEl.value = rawAmount;
// Update vault fields
const keysEl = $('userKeys'); if(keysEl) keysEl.value = v;
const keys2El = $('userKeys2'); if(keys2El) keys2El.value = v; // if there's a second field
const earningsEl = $('userEarnings'); if(earningsEl) earningsEl.value = '0.000 SOL'; // placeholder
}catch(e){ /*silent*/ }
}
function updateReflink(){
if(!walletPubkey) return;
const url = new URL(location.href); url.searchParams.set('ref', walletPubkey.toBase58());
const el = $('reflink'); if(el) el.value = url.toString();
const el2 = $('reflink2'); if(el2) el2.value = url.toString();
}
async function register(){
try{
if(!walletPubkey) return log('Connect wallet first','warn');
const { programId, mint } = getCfg(); if(!programId||!mint){ const s=$('statusValue'); if(s) s.value='Not configured'; return; } const owner=walletPubkey;
await withConn(async (conn)=>{
// Use our new PDA seeds
const [statePda] = await PublicKey.findProgramAddress([new TextEncoder().encode('state')], programId);
const [userPda] = await PublicKey.findProgramAddress([new TextEncoder().encode('user'), owner.toBuffer()], programId);
const ata = await findAta(mint, owner);
// Our new program uses register_user without referrer args
const data = await buildIx('register_user', new Uint8Array([]));
const keys=[
{ pubkey:statePda, isSigner:false, isWritable:false },
{ pubkey:userPda, isSigner:false, isWritable:true },
{ pubkey:ata, isSigner:false, isWritable:true },
{ pubkey:mint, isSigner:false, isWritable:false },
{ pubkey:owner, isSigner:true, isWritable:true },
{ pubkey:SystemProgram.programId, isSigner:false, isWritable:false },
{ pubkey:TOKEN_PROGRAM_ID, isSigner:false, isWritable:false },
{ pubkey:ATA_PROGRAM_ID, isSigner:false, isWritable:false },
];
const ix = new TransactionInstruction({ keys, programId, data });
const tx = new Transaction().add(ix); // No manual ATA creation needed
const {blockhash,lastValidBlockHeight}=await conn.getLatestBlockhash('confirmed');
tx.recentBlockhash=blockhash; tx.feePayer=owner;
const signed = await walletProvider.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize());
await conn.confirmTransaction({signature:sig,blockhash,lastValidBlockHeight},'confirmed');
log(`✅ Registered: ${sig}`,'ok');
});
}catch(e){ log(`Register failed: ${e.message}`,'err'); logSendError(e); }
}
async function buy(){
try{
if(!walletPubkey) return log('Connect wallet first','warn');
const { programId, mint, devWallet } = getCfg(); if(!programId||!mint){ const s=$('statusValue'); if(s) s.value='Not configured'; return; } const owner=walletPubkey;
const solAmount = parseFloat($('buyAmount').value||'0'); const lamports = Math.floor(solAmount*1_000_000_000);
if(lamports<=0) return log('Invalid amount','warn');
await withConn(async (conn)=>{
// Ensure user exists; auto-register if needed
const exists = await userExists(conn, programId, owner);
if(!exists){
log('User not found; registering...','warn');
await register();
}
// Use our new PDA seeds and account structure
const [statePda] = await PublicKey.findProgramAddress([new TextEncoder().encode('state')], programId);
const [vaultPda] = await PublicKey.findProgramAddress([new TextEncoder().encode('vault')], programId);
const ata = await findAta(mint, owner);
// Check if ATA exists, create if needed
const ataAccount = await conn.getAccountInfo(ata);
if (!ataAccount) {
log('Creating token account first...', 'warn');
const createAtaTx = new Transaction().add(createAtaIx(owner, ata, owner, mint));
const {blockhash: ataBlockhash, lastValidBlockHeight: ataLastValid} = await conn.getLatestBlockhash('confirmed');
createAtaTx.recentBlockhash = ataBlockhash;
createAtaTx.feePayer = owner;
const signedAtaTx = await walletProvider.signTransaction(createAtaTx);
const ataSig = await conn.sendRawTransaction(signedAtaTx.serialize());
await conn.confirmTransaction({signature: ataSig, blockhash: ataBlockhash, lastValidBlockHeight: ataLastValid}, 'confirmed');
log(`ATA created: ${ataSig}`, 'ok');
}
const data = await buildIx('buy_tokens', le64(lamports));
const devWalletPk = devWallet ?? owner;
// Build keys for our new buy_tokens instruction
const keys = [
{ pubkey: statePda, isSigner:false, isWritable:true }, // state
{ pubkey: vaultPda, isSigner:false, isWritable:true }, // vault
{ pubkey: mint, isSigner:false, isWritable:true }, // mint
{ pubkey: ata, isSigner:false, isWritable:true }, // buyer_token_account
{ pubkey: devWalletPk, isSigner:false, isWritable:true }, // dev_wallet
{ pubkey: owner, isSigner:true, isWritable:true }, // buyer
{ pubkey: SystemProgram.programId, isSigner:false, isWritable:false }, // system_program
{ pubkey: TOKEN_PROGRAM_ID, isSigner:false, isWritable:false } // token_program
];
const ix = new TransactionInstruction({ keys, programId, data });
const tx = new Transaction().add(ix);
const {blockhash,lastValidBlockHeight}=await conn.getLatestBlockhash('confirmed');
tx.recentBlockhash=blockhash; tx.feePayer=owner;
const signed = await walletProvider.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize());
await conn.confirmTransaction({signature:sig,blockhash,lastValidBlockHeight},'confirmed');
await pushTickerItem(`BUY ${solAmount.toFixed(3)} SOL • ${shortAddr(owner)}`,'buy', owner.toBase58());
log(`✅ Buy: ${sig}`,'ok');
// Refresh balances after successful buy
setTimeout(() => { updateUserBalance(); updatePot(); }, 2000);
});
}catch(e){ log(`Buy failed: ${e.message}`,'err'); logSendError(e); }
}
async function sell(){
try{
if(!walletPubkey) return log('Connect wallet first','warn');
const { programId, mint } = getCfg(); const owner=walletPubkey;
const tokensRaw = BigInt($('sellAmount').value||'0'); if(tokensRaw<=0n) return log('Invalid amount','warn');
await withConn(async (conn)=>{
// Use our new PDA seeds
const [statePda] = await PublicKey.findProgramAddress([new TextEncoder().encode('state')], programId);
const [vaultPda] = await PublicKey.findProgramAddress([new TextEncoder().encode('vault')], programId);
const ata = await findAta(mint, owner);
const data = await buildIx('sell_tokens', le64(tokensRaw));
const keys=[
{ pubkey: statePda, isSigner:false, isWritable:true },
{ pubkey: vaultPda, isSigner:false, isWritable:true },
{ pubkey: mint, isSigner:false, isWritable:true },
{ pubkey: ata, isSigner:false, isWritable:true },
{ pubkey: owner, isSigner:true, isWritable:true },
{ pubkey: SystemProgram.programId, isSigner:false, isWritable:false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner:false, isWritable:false },
];
const ix = new TransactionInstruction({ keys, programId, data });
const tx = new Transaction().add(ix);
const {blockhash,lastValidBlockHeight}=await conn.getLatestBlockhash('confirmed');
tx.recentBlockhash=blockhash; tx.feePayer=owner;
const signed = await walletProvider.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize());
await conn.confirmTransaction({signature:sig,blockhash,lastValidBlockHeight},'confirmed');
const tk = Number(tokensRaw)/1_000_000_000; await pushTickerItem(`SELL ${tk.toFixed(3)} tk • ${shortAddr(owner)}`,'sell', owner.toBase58());
log(`✅ Sell: ${sig}`,'ok');
// Refresh balances after successful sell
setTimeout(() => { updateUserBalance(); updatePot(); }, 2000);
});
}catch(e){ log(`Sell failed: ${e.message}`,'err'); logSendError(e); }
}
async function withdraw(){
try{
if(!walletPubkey) return log('Connect wallet first','warn');
const { programId, mint } = getCfg(); const owner=walletPubkey;
await withConn(async (conn)=>{
// Use our new PDA seeds and withdraw_dividends instruction
const [statePda] = await PublicKey.findProgramAddress([new TextEncoder().encode('state')], programId);
const [vaultPda] = await PublicKey.findProgramAddress([new TextEncoder().encode('vault')], programId);
const [userPda] = await PublicKey.findProgramAddress([new TextEncoder().encode('user'), owner.toBuffer()], programId);
const ata = await findAta(mint, owner);
const data = await buildIx('withdraw_dividends', new Uint8Array([]));
const keys=[
{ pubkey: statePda, isSigner:false, isWritable:false },
{ pubkey: vaultPda, isSigner:false, isWritable:true },
{ pubkey: userPda, isSigner:false, isWritable:true },
{ pubkey: ata, isSigner:false, isWritable:false },
{ pubkey: mint, isSigner:false, isWritable:false },
{ pubkey: owner, isSigner:true, isWritable:true },
{ pubkey: SystemProgram.programId, isSigner:false, isWritable:false },
];
const ix = new TransactionInstruction({ keys, programId, data });
const tx = new Transaction().add(ix);
const {blockhash,lastValidBlockHeight}=await conn.getLatestBlockhash('confirmed');
tx.recentBlockhash=blockhash; tx.feePayer=owner;
const signed = await walletProvider.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize());
await conn.confirmTransaction({signature:sig,blockhash,lastValidBlockHeight},'confirmed');
log(`✅ Withdraw: ${sig}`,'ok');
// Refresh balances after successful withdraw
setTimeout(() => { updateUserBalance(); updatePot(); }, 2000);
});
}catch(e){ log(`Withdraw failed: ${e.message}`,'err'); logSendError(e); }
}
// Reflink copy
function copyRef(){
const el = $('reflink'); if(!el || !el.value) return log('Nothing to copy','warn');
navigator.clipboard.writeText(el.value);
log('Reflink copied to clipboard','ok');
}
// Tabs & countdown
function initTabs(){
const tabs = document.querySelectorAll('.tab');
const contents = {
purchase: document.getElementById('tab-purchase'),
vault: document.getElementById('tab-vault'),
referrals: document.getElementById('tab-referrals')
};
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
const target = tab.dataset.tab;
tabs.forEach((t) => t.classList.remove('active'));
Object.values(contents).forEach((c) => c && c.classList.remove('active'));
tab.classList.add('active');
if (contents[target]) {
contents[target].classList.add('active');
contents[target].style.display = '';
}
});
});
}
function addEventListeners(){
const connectBtn = $('connectBtn'); if(connectBtn) connectBtn.addEventListener('click', connectWallet);
const disconnectBtn = $('disconnectBtn'); if(disconnectBtn) disconnectBtn.addEventListener('click', disconnectWallet);
const registerBtn = $('registerBtn'); if(registerBtn) registerBtn.addEventListener('click', register);
const buyBtn = $('buyBtn'); if(buyBtn) buyBtn.addEventListener('click', buy);
const sellBtn = $('sellBtn'); if(sellBtn) sellBtn.addEventListener('click', sell);
const withdrawBtn = $('withdrawBtn'); if(withdrawBtn) withdrawBtn.addEventListener('click', withdraw);
const withdrawBtn2 = $('withdrawBtn2'); if(withdrawBtn2) withdrawBtn2.addEventListener('click', withdraw);
const copyRefBtn = $('copyRef'); if(copyRefBtn) copyRefBtn.addEventListener('click', copyRef);
const copyRefBtn2 = $('copyRef2'); if(copyRefBtn2) copyRefBtn2.addEventListener('click', copyRef);
}
// Money rain animation (working version from website-solana)
function initMoneyRain(count=72){
const wrap = document.getElementById('moneyRain'); if(!wrap) return;
wrap.innerHTML = '';
const vw = Math.max(window.innerWidth, document.documentElement.clientWidth);
for(let i=0;i<count;i++){
const el = document.createElement('div');
el.className='money';
// Evenly distribute across width with small jitter so the right side gets coverage
const base = (i + 0.5) / count;
const jitter = (Math.random() - 0.5) * (1 / count);
const left = Math.max(0, Math.min(vw - 40, Math.floor((base + jitter) * vw)));
const dur = 8 + Math.random()*10; // 8-18s
const delay = -Math.random()*dur; // stagger
const scale = 0.7 + Math.random()*0.8;
el.style.left = `${left}px`;
el.style.animationDuration = `${dur}s`;
el.style.animationDelay = `${delay}s`;
el.style.transform = `scale(${scale})`;
wrap.appendChild(el);
}
// Reflow on resize (debounced)
let to=null; window.addEventListener('resize', ()=>{ clearTimeout(to); to=setTimeout(()=>initMoneyRain(count), 200); }, { passive:true });
}
// Initialize
async function init(){
log('🚀 PoWH3d Lottery initializing...');
populateWalletSelect();
await applyConfig();
initTabs();
addEventListeners();
updateUIDisconnected();
// Load existing ticker data from server
await loadTickerFromServer();
// Set up periodic updates
setInterval(updatePot, 5000);
setInterval(pollRecentActivity, 10000);
// Periodically refresh ticker from server to get updates from other users
setInterval(loadTickerFromServer, 15000); // Every 15 seconds
initMoneyRain(); // Start the money rain!
updatePot(); // initial load
log('✅ Ready!');
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();