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