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