1/** 2 * Raffles page: admin panel (create raffle + NFT picker), active raffle cards, entries modal. 3 * Depends: app.js (getWalletPublicKey, getSolanaProvider), config (tokenMint), optional solanaWeb3 for NFT transfer. 4 */ 5(function () { 6 var TOKEN_PROGRAM_ID = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'; 7 var TOKEN_2022_PROGRAM_ID = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'; 8 var ATA_PROGRAM_ID = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'; 9 var SOLANA_RPC = window.location.origin + '/api/solana-rpc'; 10 11 function getWalletPublicKey() { 12 return typeof window.getWalletPublicKey === 'function' ? window.getWalletPublicKey() : null; 13 } 14 function getSolanaProvider() { 15 return typeof window.getSolanaProvider === 'function' ? window.getSolanaProvider() : null; 16 } 17 18 function fetchWithCreds(url, opts) { 19 var options = opts && typeof opts === 'object' ? opts : {}; 20 options.credentials = options.credentials || 'include'; 21 return fetch(url, options); 22 } 23 24 /** Raffle API URL: on localhost use direct path; on Vercel use proxy (no rewrite for /api/*). */ 25 function rafflesApiUrl(pathSegment) { 26 var origin = window.location.origin; 27 var isLocal = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin); 28 if (isLocal) { 29 return pathSegment ? origin + '/api/raffles/' + pathSegment : origin + '/api/raffles/'; 30 } 31 var base = origin + '/api/raffles-proxy'; 32 if (pathSegment) return base + '?path=' + encodeURIComponent(pathSegment); 33 return base; 34 } 35 36 var selectedNft = null; 37 var prizeWalletForCreate = null; 38 var tokenInfoCache = {}; 39 var customTokenDecimals = 6; 40 41 function setMsg(text, isErr) { 42 var msgEl = document.getElementById('raffles-admin-msg'); 43 if (!msgEl) return; 44 msgEl.textContent = text || ''; 45 msgEl.hidden = !text; 46 msgEl.classList.toggle('raffles-admin__msg--err', !!isErr); 47 msgEl.classList.toggle('raffles-admin__msg--ok', !isErr); 48 } 49 50 function initRafflesPage() { 51 var adminEl = document.getElementById('raffles-admin'); 52 var listGrid = document.getElementById('raffles-list-grid'); 53 var listEmpty = document.getElementById('raffles-list-empty'); 54 if (!adminEl || !listGrid) return; 55 56 fetchWithCreds(rafflesApiUrl('admin-check')) 57 .then(function (r) { return r.json(); }) 58 .then(function (data) { 59 adminEl.hidden = !data.admin; 60 prizeWalletForCreate = (data.prizeWallet && String(data.prizeWallet).trim()) || null; 61 if (data.admin) { 62 setMinEndTime(); 63 bindAdminForm(); 64 } 65 }) 66 .catch(function () { adminEl.hidden = true; }); 67 68 fetchWithCreds(rafflesApiUrl(), { cache: 'no-store' }) 69 .then(function (r) { return r.json(); }) 70 .then(function (data) { 71 var raffles = data.raffles || []; 72 listEmpty.hidden = raffles.length > 0; 73 listGrid.innerHTML = ''; 74 raffles.forEach(function (r) { listGrid.appendChild(renderRaffleCard(r)); }); 75 }) 76 .catch(function () { 77 listEmpty.hidden = false; 78 listGrid.innerHTML = ''; 79 }); 80 } 81 82 function setMinEndTime() { 83 var input = document.getElementById('raffles-ends-at'); 84 if (!input) return; 85 var now = new Date(); 86 now.setMinutes(now.getMinutes() + 5); 87 var pad = function (n) { return (n < 10 ? '0' : '') + n; }; 88 input.min = now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate()) + 'T' + pad(now.getHours()) + ':' + pad(now.getMinutes()); 89 } 90 91 function bindAdminForm() { 92 var selectNftBtn = document.getElementById('raffles-select-nft'); 93 var tokenType = document.getElementById('raffles-token-type'); 94 var customWrap = document.getElementById('raffles-custom-token-wrap'); 95 var customMint = document.getElementById('raffles-custom-mint'); 96 var customInfo = document.getElementById('raffles-custom-token-info'); 97 var startBtn = document.getElementById('raffles-start-btn'); 98 var msgEl = document.getElementById('raffles-admin-msg'); 99 100 if (selectNftBtn) selectNftBtn.addEventListener('click', openNftPicker); 101 var removeNftBtn = document.getElementById('raffles-nft-remove'); 102 if (removeNftBtn) removeNftBtn.addEventListener('click', function (e) { 103 e.preventDefault(); 104 selectedNft = null; 105 var preview = document.getElementById('raffles-nft-preview'); 106 var previewImg = document.getElementById('raffles-nft-preview-img'); 107 var previewName = document.getElementById('raffles-nft-preview-name'); 108 if (preview) preview.hidden = true; 109 if (previewImg) previewImg.src = ''; 110 if (previewName) previewName.textContent = ''; 111 }); 112 if (tokenType) tokenType.addEventListener('change', function () { 113 customWrap.hidden = tokenType.value !== 'custom'; 114 customInfo.textContent = ''; 115 customTokenDecimals = 6; 116 }); 117 if (customMint) { 118 var debounce; 119 customMint.addEventListener('input', function () { 120 clearTimeout(debounce); 121 var mint = customMint.value.trim(); 122 customInfo.textContent = mint ? 'Loadingâ¦' : ''; 123 customTokenDecimals = 6; 124 if (!mint) return; 125 debounce = setTimeout(function () { 126 fetchWithCreds(window.location.origin + '/api/token-info?mint=' + encodeURIComponent(mint)) 127 .then(function (r) { return r.json(); }) 128 .then(function (d) { 129 customTokenDecimals = typeof d.decimals === 'number' ? d.decimals : 6; 130 customInfo.textContent = (d.symbol || d.name) ? (d.symbol + ' â ' + d.name) : 'Token found'; 131 }) 132 .catch(function () { customInfo.textContent = 'Could not load token'; }); 133 }, 400); 134 }); 135 } 136 var nftPreview = document.getElementById('raffles-nft-preview'); 137 var nftPreviewImg = document.getElementById('raffles-nft-preview-img'); 138 var nftPreviewName = document.getElementById('raffles-nft-preview-name'); 139 var lastPreviewImageUrl = ''; 140 function updateNftPreview() { 141 if (!nftPreview || !nftPreviewImg || !nftPreviewName) return; 142 if (selectedNft && selectedNft.id) { 143 nftPreview.hidden = false;
144 var rawImg = selectedNft.image || ''; 145 if (rawImg && rawImg !== lastPreviewImageUrl) { 146 lastPreviewImageUrl = rawImg; 147 nftPreviewImg.src = proxyImageUrl(rawImg); 148 } else if (!rawImg) { 149 lastPreviewImageUrl = ''; 150 nftPreviewImg.src = ''; 151 } 152 nftPreviewImg.alt = selectedNft.name || ''; 153 nftPreviewName.textContent = selectedNft.name || selectedNft.id || 'Selected'; 154 } else { 155 lastPreviewImageUrl = ''; 156 nftPreview.hidden = true; 157 nftPreviewImg.src = ''; 158 nftPreviewName.textContent = ''; 159 } 160 } 161 updateNftPreview(); 162 setInterval(updateNftPreview, 500); 163 } 164 165 function openNftPicker() { 166 var modal = document.getElementById('raffles-nft-modal'); 167 var hint = document.getElementById('raffles-nft-modal-hint'); 168 var grid = document.getElementById('raffles-nft-modal-grid'); 169 var wallet = getWalletPublicKey(); 170 if (!wallet) { 171 hint.textContent = 'Connect your wallet first, then try again.'; 172 grid.innerHTML = ''; 173 if (modal) { modal.setAttribute('aria-hidden', 'false'); } 174 return; 175 } 176 hint.textContent = 'Loading NFTsâ¦'; 177 grid.innerHTML = ''; 178 if (modal) modal.setAttribute('aria-hidden', 'false'); 179 180 fetchWithCreds(window.location.origin + '/api/nfts?wallet=' + encodeURIComponent(wallet)) 181 .then(function (r) { return r.json(); }) 182 .then(function (data) { 183 var nfts = data.nfts || []; 184 hint.textContent = nfts.length === 0 ? 'No NFTs found in this wallet.' : 'Click an NFT to select as prize.'; 185 nfts.forEach(function (nft) { 186 var item = document.createElement('button'); 187 item.type = 'button'; 188 item.className = 'raffles-modal__nft-item'; 189 item.innerHTML = '<img src="' + (nft.image ? escapeHtml(proxyImageUrl(nft.image)) : '') + '" alt="" loading="lazy" onerror="this.style.display=\'none\'" /><span>' + escapeHtml(nft.name || nft.id) + '</span>'; 190 item.addEventListener('click', function () {
191 selectedNft = { id: nft.id, name: nft.name, image: nft.image }; 192 var preview = document.getElementById('raffles-nft-preview'); 193 var previewImg = document.getElementById('raffles-nft-preview-img'); 194 var previewName = document.getElementById('raffles-nft-preview-name'); 195 if (preview) preview.hidden = false; 196 if (previewImg) previewImg.src = nft.image ? proxyImageUrl(nft.image) : ''; 197 if (previewName) previewName.textContent = nft.name || nft.id || 'Selected'; 198 closeNftModal(); 199 }); 200 grid.appendChild(item); 201 }); 202 }) 203 .catch(function () { 204 hint.textContent = 'Failed to load NFTs. Try again.'; 205 }); 206 } 207 208 function closeNftModal() { 209 var modal = document.getElementById('raffles-nft-modal'); 210 if (modal) modal.setAttribute('aria-hidden', 'true'); 211 } 212 function closeEntriesModal() { 213 var modal = document.getElementById('raffles-entries-modal'); 214 if (modal) modal.setAttribute('aria-hidden', 'true'); 215 } 216 217 var TX_EXPLORER_BASE = 'https://explorer.solana.com/tx/'; 218 function showClaimSuccessModal(signature) { 219 var modal = document.getElementById('raffles-claim-success-modal'); 220 var link = document.getElementById('raffles-claim-success-tx-link'); 221 if (link && signature) link.href = TX_EXPLORER_BASE + signature; 222 if (modal) modal.setAttribute('aria-hidden', 'false'); 223 } 224 function closeClaimSuccessModal() { 225 var modal = document.getElementById('raffles-claim-success-modal'); 226 if (modal) modal.setAttribute('aria-hidden', 'true'); 227 initRafflesPage(); 228 } 229 230 function showTicketPurchaseSuccessModal(signature) { 231 var modal = document.getElementById('raffles-purchase-success-modal'); 232 var link = document.getElementById('raffles-purchase-success-tx-link'); 233 if (link && signature) link.href = TX_EXPLORER_BASE + signature; 234 if (modal) modal.setAttribute('aria-hidden', 'false'); 235 } 236 function closeTicketPurchaseSuccessModal() { 237 var modal = document.getElementById('raffles-purchase-success-modal'); 238 if (modal) modal.setAttribute('aria-hidden', 'true'); 239 initRafflesPage(); 240 } 241 242 document.getElementById('raffles-nft-modal-close')?.addEventListener('click', closeNftModal); 243 document.getElementById('raffles-nft-modal-backdrop')?.addEventListener('click', closeNftModal); 244 document.getElementById('raffles-entries-modal-close')?.addEventListener('click', closeEntriesModal); 245 document.getElementById('raffles-entries-modal-backdrop')?.addEventListener('click', closeEntriesModal); 246 document.getElementById('raffles-claim-success-close')?.addEventListener('click', closeClaimSuccessModal); 247 document.getElementById('raffles-claim-success-backdrop')?.addEventListener('click', closeClaimSuccessModal); 248 document.getElementById('raffles-claim-success-done')?.addEventListener('click', closeClaimSuccessModal); 249 document.getElementById('raffles-purchase-success-close')?.addEventListener('click', closeTicketPurchaseSuccessModal); 250 document.getElementById('raffles-purchase-success-backdrop')?.addEventListener('click', closeTicketPurchaseSuccessModal); 251 document.getElementById('raffles-purchase-success-done')?.addEventListener('click', closeTicketPurchaseSuccessModal); 252 253 function escapeHtml(s) { 254 if (!s) return ''; 255 var div = document.createElement('div'); 256 div.textContent = s; 257 return div.innerHTML; 258 } 259 260 /** Use same-origin proxy for external image URLs to avoid cross-site cookies and CORS. */ 261 function proxyImageUrl(url) { 262 if (!url || !url.trim()) return ''; 263 var u = url.trim(); 264 if (u.startsWith('/') && !u.startsWith('//')) return u; 265 try { 266 var origin = window.location.origin; 267 var parsed = new URL(u, origin); 268 if (parsed.origin === origin) return u; 269 } catch (e) { /* ignore */ } 270 return window.location.origin + '/api/proxy-image?url=' + encodeURIComponent(u); 271 } 272 273 function getTicketPriceTokenType() { 274 var tokenType = document.getElementById('raffles-token-type'); 275 return tokenType ? tokenType.value : 'sol'; 276 } 277 /** Get ticket price as whole units (e.g. 1.5 SOL) and convert to raw string for the API. */ 278 function getTicketPriceRawFromWholeUnits() { 279 var input = document.getElementById('raffles-ticket-price'); 280 var val = input ? input.value : ''; 281 var num = parseFloat(String(val).replace(/,/g, '').trim()); 282 if (isNaN(num) || num < 0) return null; 283 var type = getTicketPriceTokenType(); 284 var decimals = 9; 285 if (type === 'sol') decimals = 9; 286 else if (type === 'aaa') decimals = 6; 287 else decimals = customTokenDecimals; 288 var raw = Math.round(num * Math.pow(10, decimals)); 289 return String(raw); 290 } 291 function getTicketPriceTokenMint() { 292 var type = getTicketPriceTokenType(); 293 if (type === 'aaa' && window.ABSURD_APES_CONFIG && window.ABSURD_APES_CONFIG.tokenMint) return window.ABSURD_APES_CONFIG.tokenMint; 294 if (type === 'custom') return document.getElementById('raffles-custom-mint')?.value?.trim() || null; 295 return null; 296 } 297 function getTicketPriceDecimals() { 298 var type = getTicketPriceTokenType(); 299 if (type === 'sol') return 9; 300 if (type === 'aaa') return 6; 301 return customTokenDecimals; 302 } 303 304 function startRaffle() { 305 console.log('[Raffles] Start raffle clicked'); 306 var msgEl = document.getElementById('raffles-admin-msg'); 307 var startBtn = document.getElementById('raffles-start-btn'); 308 if (msgEl) { msgEl.hidden = true; msgEl.className = 'raffles-admin__msg'; msgEl.textContent = ''; } 309 310 if (!selectedNft || !selectedNft.id) { 311 setMsg('Select a prize NFT first.', true); 312 return; 313 } 314 var ticketCountEl = document.getElementById('raffles-ticket-count'); 315 var ticketCount = ticketCountEl ? parseInt(ticketCountEl.value, 10) : 0; 316 if (isNaN(ticketCount) || ticketCount < 1) { 317 setMsg('Set a valid number of tickets.', true); 318 return; 319 } 320 var priceRaw = getTicketPriceRawFromWholeUnits(); 321 if (priceRaw === null || priceRaw === '0') { 322 setMsg('Set the ticket price (e.g. 1 for 1 SOL, 100 for 100 AAA).', true); 323 return; 324 } 325 var endsAtEl = document.getElementById('raffles-ends-at'); 326 var endsAt = endsAtEl ? endsAtEl.value : ''; 327 if (!endsAt) { 328 setMsg('Set the raffle end date and time.', true); 329 return; 330 } 331 var endsAtDate = new Date(endsAt);
332 if (isNaN(endsAtDate.getTime()) || endsAtDate <= new Date()) { 333 setMsg('End time must be in the future.', true); 334 return; 335 } 336 337 var tokenType = getTicketPriceTokenType(); 338 var tokenMint = getTicketPriceTokenMint(); 339 if (tokenType === 'custom' && !tokenMint) { 340 setMsg('Enter the custom token mint address.', true); 341 return; 342 } 343 344 if (!prizeWalletForCreate) { 345 setMsg('Prize wallet not configured. Refresh the page; if it persists, check server PRIZE_WALLET.', true); 346 return; 347 } 348 349 if (startBtn) { startBtn.disabled = true; startBtn.textContent = 'Send NFTâ¦'; } 350 setMsg('Confirm in your wallet: send the prize NFT to the prize wallet.', false); 351 352 transferNftToPrizeWallet(selectedNft.id, prizeWalletForCreate) 353 .then(function (sig) { 354 if (!sig) throw new Error('No transaction signature'); 355 if (startBtn) startBtn.textContent = 'Creatingâ¦'; 356 setMsg('NFT sent. Creating raffleâ¦', false); 357 var body = { 358 prizeNftMint: selectedNft.id, 359 prizeNftName: selectedNft.name || null, 360 prizeNftImage: selectedNft.image || null, 361 ticketCount: ticketCount, 362 ticketPriceTokenType: tokenType, 363 ticketPriceTokenMint: tokenMint || undefined, 364 ticketPriceRaw: String(priceRaw), 365 ticketPriceDecimals: getTicketPriceDecimals(), 366 endsAt: endsAtDate.toISOString(), 367 nftTransferSignature: sig, 368 }; 369 var createUrl = rafflesApiUrl() + '?t=' + Date.now(); 370 return fetchWithCreds(createUrl, { 371 method: 'POST', 372 headers: { 'Content-Type': 'application/json' }, 373 body: JSON.stringify(body), 374 redirect: 'manual', 375 }).then(function (r) { 376 return r.text().then(function (text) { 377 if (r.type === 'opaqueredirect' || r.status === 302 || r.status === 301) { 378 throw new Error('Server redirected (session may have expired). Please refresh and connect Discord again, then try Start raffle.'); 379 } 380 if (!r.ok) { 381 var j; 382 try { j = text ? JSON.parse(text) : {}; } catch (e) { j = {}; } 383 throw new Error(j.error || r.statusText || 'Request failed'); 384 } 385 var raffle = null; 386 try { raffle = text ? JSON.parse(text) : null; } catch (e) { raffle = null; } 387 return { status: r.status, text: text, raffle: raffle }; 388 }); 389 }).then(function (result) { 390 var raffle = result.raffle; 391 var hasId = raffle != null && typeof raffle === 'object' && ('id' in raffle) && (raffle.id === 0 || raffle.id === '0' || (raffle.id !== undefined && raffle.id !== null && raffle.id !== '')); 392 if (!hasId) { 393 setMsg('Server error: invalid response after creating raffle.', true); 394 return; 395 } 396 setMsg('Raffle started.', false); 397 selectedNft = null; 398 var nftPreview = document.getElementById('raffles-nft-preview'); 399 if (nftPreview) nftPreview.hidden = true; 400 initRafflesPage(); 401 }).catch(function (err) { 402 setMsg(err && err.message ? err.message : 'Failed to create raffle.', true); 403 }).finally(function () { 404 if (startBtn) { startBtn.disabled = false; startBtn.textContent = 'Start raffle'; } 405 }); 406 }); 407 } 408 409 function transferNftToPrizeWallet(nftMint, prizeWallet) { 410 var provider = getSolanaProvider(); 411 var wallet = getWalletPublicKey(); 412 if (!provider || !wallet) return Promise.reject(new Error('Wallet not connected')); 413 414 var solanaWeb3 = window.solanaWeb3; 415 if (!solanaWeb3 || !solanaWeb3.Connection || !solanaWeb3.PublicKey || !solanaWeb3.Transaction || !solanaWeb3.TransactionInstruction) { 416 return Promise.reject(new Error('Solana web3 not loaded. Refresh the page.')); 417 } 418 419 var Connection = solanaWeb3.Connection; 420 var PublicKey = solanaWeb3.PublicKey; 421 var Transaction = solanaWeb3.Transaction; 422 var TransactionInstruction = solanaWeb3.TransactionInstruction; 423 424 var connection = new Connection(SOLANA_RPC, 'confirmed'); 425 var mintPk = new PublicKey(nftMint); 426 var ownerPk = new PublicKey(wallet); 427 var destPk = new PublicKey(prizeWallet); 428 if (ownerPk.equals(destPk)) { 429 return Promise.reject(new Error('Prize wallet must be different from your connected wallet. Set PRIZE_WALLET to a different address in .env.')); 430 } 431 var tokenProgramId = new PublicKey(TOKEN_PROGRAM_ID); 432 var ataProgramId = new PublicKey(ATA_PROGRAM_ID); 433 434 function findAta(owner, mint, tokenProgram) { 435 var tp = tokenProgram || tokenProgramId; 436 var seeds = [owner.toBuffer(), tp.toBuffer(), mint.toBuffer()]; 437 var out = PublicKey.findProgramAddressSync(seeds, ataProgramId); 438 return out[0]; 439 } 440 441 function buildAndSendTx() { 442 var instructions = []; 443 var sysProgramId = new PublicKey('11111111111111111111111111111111'); 444 var rentId = new PublicKey('SysvarRent111111111111111111111111111111111'); 445 446 return connection.getAccountInfo(mintPk).then(function (mintInfo) { 447 if (!mintInfo || !mintInfo.owner || !mintInfo.data) return Promise.reject(new Error('Could not load NFT mint.')); 448 var actualTokenProgram = new PublicKey(mintInfo.owner); 449 var mintData = mintInfo.data; 450 var decimals = mintData.length > 44 ? mintData[44] : 0; 451 var sourceAta = findAta(ownerPk, mintPk, actualTokenProgram); 452 var destAta = findAta(destPk, mintPk, actualTokenProgram); 453 return connection.getAccountInfo(sourceAta).then(function (sourceInfo) { 454 if (!sourceInfo || !sourceInfo.data) { 455 return Promise.reject(new Error( 456 'You donât own this NFT in the connected wallet. Use the same wallet you used when selecting the NFT, or re-open the picker and choose an NFT from the current wallet. Compressed NFTs are not supported as prizes.' 457 )); 458 } 459 var data = sourceInfo.data; 460 if (data.length < 72) return Promise.reject(new Error('Invalid token account data.')); 461 var amountView = new DataView(data.buffer || data, data.byteOffset + 64, 8); 462 var amount = amountView.getBigUint64(0, true); 463 if (amount < 1) { 464 return Promise.reject(new Error( 465 'You donât own this NFT in the connected wallet. Use the same wallet you used when selecting the NFT, or re-open the picker and choose an NFT from the current wallet.' 466 )); 467 } 468 return { actualTokenProgram: actualTokenProgram, sourceAta: sourceAta, destAta: destAta, decimals: decimals, mintData: mintData }; 469 }); 470 }).then(function (opts) { 471 var actualTokenProgram = opts.actualTokenProgram; 472 var sourceAta = opts.sourceAta; 473 var destAta = opts.destAta; 474 var decimals = opts.decimals != null ? opts.decimals : 0; 475 var mintData = opts.mintData; 476 return connection.getAccountInfo(destAta).then(function (info) { 477 var needCreate = !info; 478 if (needCreate) { 479 var createIx = new TransactionInstruction({ 480 keys: [ 481 { pubkey: ownerPk, isSigner: true, isWritable: true }, 482 { pubkey: destAta, isSigner: false, isWritable: true }, 483 { pubkey: destPk, isSigner: false, isWritable: false }, 484 { pubkey: mintPk, isSigner: false, isWritable: false }, 485 { pubkey: sysProgramId, isSigner: false, isWritable: false }, 486 { pubkey: actualTokenProgram, isSigner: false, isWritable: false }, 487 { pubkey: ataProgramId, isSigner: false, isWritable: false }, 488 { pubkey: rentId, isSigner: false, isWritable: false }, 489 ], 490 programId: ataProgramId, 491 data: new Uint8Array([1]), 492 }); 493 instructions.push(createIx); 494 } 495 496 var transferKeys = [ 497 { pubkey: sourceAta, isSigner: false, isWritable: true }, 498 { pubkey: mintPk, isSigner: false, isWritable: false }, 499 { pubkey: destAta, isSigner: false, isWritable: true }, 500 { pubkey: ownerPk, isSigner: true, isWritable: false }, 501 ]; 502 503 function addTransferAndContinue() { 504 var transferData = new Uint8Array(10); 505 transferData[0] = 12; /* SPL Token TransferChecked */ 506 new DataView(transferData.buffer, transferData.byteOffset, transferData.byteLength).setBigUint64(1, BigInt(1), true); 507 transferData[9] = decimals; 508 instructions.push(new TransactionInstruction({ keys: transferKeys, programId: actualTokenProgram, data: transferData })); 509 var tx = new Transaction(); 510 instructions.forEach(function (ix) { tx.add(ix); }); 511 return connection.getLatestBlockhash('confirmed').then(function (bh) { 512 var blockhash = (bh && bh.value && bh.value.blockhash) ? bh.value.blockhash : (bh && bh.blockhash) ? bh.blockhash : null; 513 if (!blockhash) return Promise.reject(new Error('Could not get blockhash')); 514 tx.recentBlockhash = blockhash; 515 tx.feePayer = ownerPk; 516 517 function signThenSendViaServer(txToSign) { 518 var t = txToSign || tx; 519 var signPromise = typeof provider.signTransaction === 'function' 520 ? Promise.resolve(provider.signTransaction(t)) 521 : Promise.resolve(t).then(function (unsigned) { 522 var ser = unsigned.serialize({ requireAllSignatures: false }); 523 var b64 = btoa(String.fromCharCode.apply(null, (ser instanceof Uint8Array ? ser : new Uint8Array(ser)))
523); 524 return provider.request({ method: 'signTransaction', params: { message: b64 } }).then(function (signedB64) { 525 if (!signedB64) return null; 526 var decoded = typeof atob !== 'undefined' ? atob(signedB64) : Buffer.from(signedB64, 'base64').toString('binary'); 527 var arr = new Uint8Array(decoded.length); 528 for (var i = 0; i < decoded.length; i++) arr[i] = decoded.charCodeAt(i); 529 return { serialize: function () { return arr; } }; 530 }); 531 }); 532 return signPromise.then(function (signedTx) { 533 if (!signedTx) return Promise.reject(new Error('Wallet did not return signed transaction')); 534 var serialized = signedTx.serialize ? signedTx.serialize() : (signedTx instanceof Uint8Array ? signedTx : new Uint8Array(signedTx)); 535 var raw = serialized instanceof Uint8Array ? serialized : new Uint8Array(serialized); 536 var base64 = typeof raw.toString === 'function' && raw.toString('base64') ? raw.toString('base64') : btoa(String.fromCharCode.apply(null, raw)); 537 return fetch(rafflesApiUrl('send-raw'), { 538 method: 'POST', 539 headers: { 'Content-Type': 'application/json' }, 540 credentials: 'include', 541 body: JSON.stringify({ signedTransaction: base64 }), 542 }).then(function (r) { return r.json().then(function (data) { return { ok: r.ok, data: data }; }, function () { return { ok: false, data: null }; }); }).then(function (result) { 543 if (result.ok && result.data && result.data.signature) return result.data.signature; 544 var errMsg = (result.data && (result.data.error || result.data.logs)) ? (result.data.error || (Array.isArray(result.data.logs) ? result.data.logs.join('\n') : '')) : 'Send failed'; 545 return Promise.reject(new Error(errMsg)); 546 }); 547 }); 548 } 549 550 return signThenSendViaServer(tx); 551 }); 552 } 553 return addTransferAndContinue(); 554 }); 555 }); 556 } 557 558 return buildAndSendTx(); 559 } 560 561 /** Build, sign and send payment for raffle tickets. Returns { signature, paymentDestination }. */ 562 function buildAndSendRafflePayment(raffle, count) { 563 var provider = getSolanaProvider(); 564 var wallet = getWalletPublicKey(); 565 if (!provider || !wallet) return Promise.reject(new Error('Wallet not connected')); 566 var treasury = (raffle.treasury || '').trim(); 567 if (!treasury) return Promise.reject(new Error('Raffle treasury not configured. Cannot pay for tickets.')); 568 var raw = String(raffle.ticketPriceRaw || '0').trim(); 569 var type = (raffle.ticketPriceTokenType || 'sol').toLowerCase(); 570 var solanaWeb3 = window.solanaWeb3; 571 if (!solanaWeb3 || !solanaWeb3.Connection || !solanaWeb3.PublicKey || !solanaWeb3.Transaction || !solanaWeb3.TransactionInstruction) { 572 return Promise.reject(new Error('Solana web3 not loaded. Refresh the page.')); 573 } 574 var Connection = solanaWeb3.Connection; 575 var PublicKey = solanaWeb3.PublicKey; 576 var Transaction = solanaWeb3.Transaction; 577 var TransactionInstruction = solanaWeb3.TransactionInstruction; 578 var SystemProgram = solanaWeb3.SystemProgram; 579 var connection = new Connection(SOLANA_RPC, 'confirmed'); 580 var ownerPk = new PublicKey(wallet); 581 var treasuryPk = new PublicKey(treasury); 582 583 if (type === 'sol') { 584 var lamports = parseInt(raw, 10) * count; 585 if (isNaN(lamports) || lamports < 1) return Promise.reject(new Error('Invalid ticket price')); 586 var tx = new Transaction().add( 587 SystemProgram.transfer({ 588 fromPubkey: ownerPk, 589 toPubkey: treasuryPk, 590 lamports: lamports, 591 }) 592 ); 593 return connection.getLatestBlockhash('confirmed').then(function (bh) { 594 var blockhash = (bh && bh.value && bh.value.blockhash) ? bh.value.blockhash : (bh && bh.blockhash) || null; 595 if (!blockhash) return Promise.reject(new Error('Could not get blockhash')); 596 tx.recentBlockhash = blockhash; 597 tx.feePayer = ownerPk; 598 if (typeof provider.signAndSendTransaction === 'function') { 599 return Promise.resolve(provider.signAndSendTransaction(tx)).then(function (result) { 600 var sig = (result && (typeof result === 'string' ? result : result.signature || result.hash)) || null; 601 return sig ? { signature: sig, paymentDestination: treasury } : Promise.reject(new Error('No signature returned')); 602 }); 603 } 604 var serialized = tx.serialize({ requireAllSignatures: false }); 605 var rawBuf = serialized && serialized instanceof Uint8Array ? serialized : new Uint8Array(serialized); 606 var base64 = typeof rawBuf.toString === 'function' && rawBuf.toString('base64') ? rawBuf.toString('base64') : btoa(String.fromCharCode.apply(null, rawBuf)); 607 return provider.request({ method: 'signAndSendTransaction', params: { transaction: base64 } }).then(function (result) { 608 var sig = (result && (typeof result === 'string' ? result : result.signature || result.hash)) || null; 609 return sig ? { signature: sig, paymentDestination: treasury } : Promise.reject(new Error('No signature returned')); 610 }); 611 }); 612 } 613 614 var mintStr = (type === 'aaa' && window.ABSURD_APES_CONFIG && window.ABSURD_APES_CONFIG.tokenMint) ? window.ABSURD_APES_CONFIG.tokenMint : (raffle.ticketPriceTokenMint || '').trim(); 615 if (!mintStr) return Promise.reject(new Error('Token mint not configured for this raffle')); 616 var mintPk = new PublicKey(mintStr); 617 var tokenProgramId = new PublicKey(TOKEN_PROGRAM_ID); 618 var ataProgramId = new PublicKey(ATA_PROGRAM_ID); 619 function findAta(owner, mint, tokenProgram) { 620 var tp = tokenProgram || tokenProgramId; 621 var seeds = [owner.toBuffer(), tp.toBuffer(), mint.toBuffer()]; 622 var out = PublicKey.findProgramAddressSync(seeds, ataProgramId); 623 return out[0]; 624 } 625 var storedDecimals = raffle.ticketPriceDecimals != null ? Number(raffle.ticketPriceDecimals) : 6; 626 var decimals = type === 'aaa' ? 6 : customTokenDecimals; 627 return connection.getAccountInfo(mintPk).then(function (mintInfo) { 628 if (mintInfo && mintInfo.owner) { 629 var actualTokenProgram = new PublicKey(mintInfo.owner); 630 if (mintInfo.data && mintInfo.data.length > 44) decimals = mintInfo.data[44]; 631 return { actualTokenProgram: actualTokenProgram, decimals: decimals }; 632 } 633 return { actualTokenProgram: tokenProgramId, decimals: decimals }; 634 }).then(function (opts) { 635 var actualTokenProgram = opts.actualTokenProgram; 636 var actualDecimals = opts.decimals != null ? opts.decimals : 6; 637 var humanPrice = Number(raw) / Math.pow(10, storedDecimals); 638 var amountRaw = BigInt(Math.round(humanPrice * Math.pow(10, actualDecimals) * count)); 639 if (amountRaw < 1) return Promise.reject(new Error('Invalid ticket price or count')); 640 var decimals = actualDecimals; 641 var sourceAta = findAta(ownerPk, mintPk, actualTokenProgram); 642 var destAta = findAta(treasuryPk, mintPk, actualTokenProgram); 643 var sysProgramId = new PublicKey('11111111111111111111111111111111'); 644 var rentId = new PublicKey('SysvarRent111111111111111111111111111111111'); 645 var instructions = []; 646 return connection.getAccountInfo(destAta).then(function (info) { 647 if (!info) { 648 instructions.push(new TransactionInstruction({ 649 keys: [ 650 { pubkey: ownerPk, isSigner: true, isWritable: true }, 651 { pubkey: destAta, isSigner: false, isWritable: true }, 652 { pubkey: treasuryPk, isSigner: false, isWritable: false }, 653 { pubkey: mintPk, isSigner: false, isWritable: false }, 654 { pubkey: sysProgramId, isSigner: false, isWritable: false }, 655 { pubkey: actualTokenProgram, isSigner: false, isWritable: false }, 656 { pubkey: ataProgramId, isSigner: false, isWritable: false }, 657 { pubkey: rentId, isSigner: false, isWritable: false }, 658 ], 659 programId: ataProgramId, 660 data: new Uint8Array([1]), 661 })); 662 } 663 return null; 664 }).then(function () { 665 var transferData = new Uint8Array(10); 666 transferData[0] = 12; /* SPL Token TransferChecked */ 667 var dv = new DataView(transferData.buffer, transferData.byteOffset, transferData.byteLength); 668 var amt = amountRaw > BigInt('0xffffffffffffffff') ? BigInt('0xffffffffffffffff') : amountRaw; 669 dv.setBigUint64(1, amt, true); 670 transferData[9] = decimals; 671 instructions.push(new TransactionInstruction({ 672 keys: [ 673 { pubkey: sourceAta, isSigner: false, isWritable: true }, 674 { pubkey: mintPk, isSigner: false, isWritable: false }, 675 { pubkey: destAta, isSigner: false, isWritable: true }, 676 { pubkey: ownerPk, isSigner: true, isWritable: false }, 677 ], 678 programId: actualTokenProgram, 679 data: transferData, 680 })); 681 var tx = new Transaction(); 682 instructions.forEach(function (ix) { tx.add(ix); }); 683 return connection.getLatestBlockhash('confirmed').then(function (bh) { 684 var blockhash = (bh && bh.value && bh.value.blockhash) ? bh.value.blockhash : (bh && bh.blockhash) || null; 685 if (!blockhash) return Promise.reject(new Error('Could not get blockhash')); 686 tx.recentBlockhash = blockhash; 687 tx.feePayer = ownerPk; 688 if (typeof provider.signAndSendTransaction === 'function') { 689 return Promise.resolve(provider.signAndSendTransaction(tx)).then(function (result) { 690 var sig = (result && (typeof result === 'string' ? result : result.signature || result.hash)) || null; 691 return sig ? { signature: sig, paymentDestination: destAta.toString() } : Promise.reject(new Error('No signature returned')); 692 }); 693 } 694 var serialized = tx.serialize({ requireAllSignatures: false }); 695 var rawBuf = serialized && serialized instanceof Uint8Array ? serialized : new Uint8Array(serialized); 696 var base64 = typeof rawBuf.toString === 'function' && rawBuf.toString('base64') ? rawBuf.toString('base64') : btoa(String.fromCharCode.apply(null, rawBuf)); 697 return provider.request({ method: 'signAndSendTransaction', params: { transaction: base64 } }).then(function (result) { 698 var sig = (result && (typeof result === 'string' ? result : result.signature || result.hash)) || null; 699 return sig ? { signature: sig, paymentDestination: destAta.toString() } : Promise.reject(new Error('No signature returned')); 700 }); 701 }); 702 }); 703 }); 704 } 705 706 function renderRaffleCard(r) { 707 var card = document.createElement('article'); 708 card.className = 'raffle-card'; 709 var sold = r.ticketsSold != null ? r.ticketsSold : 0; 710 var total = r.ticketCount || 0; 711 var price = formatPrice(r); 712 var priceHtml = (price.imageUrl ? '<img class="raffle-card__token-icon" src="' + escapeHtml(price.imageUrl) + '" alt="" aria-hidden="true" />' : '') + 713 '<span class="raffle-card__price-amount">' + escapeHtml(price.amountStr) + '</span> <span class="raffle-card__price-symbol">' + escapeHtml(price.symbol) + '</span>'; 714 var endsAt = r.endsAt ? new Date(r.endsAt) : null; 715 var isEnded = endsAt && endsAt <= new Date(); 716 var maxPerWallet = Math.max(0, Math.floor(total * 0.2));
717 var remaining = Math.max(0, total - sold); 718 var winnerWallet = (r.winnerWallet || '').toLowerCase(); 719 var winnerDisplay = r.winnerDisplay || ''; 720 var winnerLabel = winnerDisplay ? escapeHtml(winnerDisplay) : (winnerWallet ? (winnerWallet.slice(0, 4) + 'â¦' + winnerWallet.slice(-4)) : ''); 721 var myWallet = (getWalletPublicKey() || '').toLowerCase(); 722 var prizeClaimed = !!(r.claimTxSignature && r.claimTxSignature.trim()); 723 var showClaim = isEnded && winnerWallet && myWallet && winnerWallet === myWallet && !prizeClaimed; 724 var showBuy = !isEnded && remaining >= 1; 725 var txExplorerUrl = 'https://explorer.solana.com/tx/'; 726 var claimStatusHtml = prizeClaimed 727 ? '<span class="raffle-card__claimed">Prize claimed</span>' + 728 (r.claimTxSignature 729 ? ' <a class="raffle-card__tx-link" href="' + escapeHtml(txExplorerUrl + r.claimTxSignature) + '" target="_blank" rel="noopener noreferrer">View transaction</a>' 730 : '') 731 : (showClaim ? '<button type="button" class="btn btn--primary raffle-card__btn raffle-claim-btn" data-id="' + r.id + '">Claim</button>' : ''); 732 card.innerHTML = 733 '<div class="raffle-card__image-wrap">' + 734 '<img class="raffle-card__image" src="' + escapeHtml(r.prizeNftImage ? proxyImageUrl(r.prizeNftImage) : '') + '" alt="" loading="lazy" onerror="this.style.display=\'none\'" />' + 735 '</div>' + 736 '<div class="raffle-card__body">' + 737 '<h3 class="raffle-card__name">' + escapeHtml(r.prizeNftName || 'Prize NFT') + '</h3>' + 738 '<p class="raffle-card__meta">Ticket: <span class="raffle-card__price" data-token-mint="' + (price.mint ? escapeHtml(price.mint) : '') + '">' + priceHtml + '</span></p>' + 739 '<div class="raffle-card__row">' + 740 '<span class="raffle-card__meta">Tickets</span>' + 741 '<span class="raffle-card__meta">' + sold + ' / ' + total + '</span>' + 742 '</div>' + 743 '<div class="raffle-card__row">' + 744 '<span class="raffle-card__meta">Ends</span>' + 745 '<span class="raffle-card__meta raffle-card__time" data-ends="' + (r.endsAt || '') + '">' + (endsAt ? formatTimeLeft(endsAt) : 'â') + '</span>' + 746 '</div>' + 747 (isEnded && winnerWallet 748 ? '<div class="raffle-card__row raffle-card__winner">' + 749 '<span class="raffle-card__meta">Winner</span>' + 750 '<span class="raffle-card__meta raffle-card__winner-address" title="' + escapeHtml(r.winnerWallet || '') + '">' + winnerLabel + '</span>' + 751 '</div>' 752 : '') + 753 (showBuy 754 ? '<div class="raffle-card__buy">' + 755 '<label class="raffle-card__buy-label"><span class="raffle-card__meta">Number of tickets</span></label>' + 756 '<input type="number" class="raffle-card__buy-input" min="1" max="' + Math.min(maxPerWallet, remaining) + '" value="1" data-raffle-id="' + r.id + '" data-max="' + maxPerWallet + '" data-remaining="' + remaining + '" />' + 757 '<button type="button" class="btn btn--primary raffle-card__btn raffle-buy-btn" data-id="' + r.id + '">Buy tickets</button>' + 758 '</div>' 759 : '') + 760 '<div class="raffle-card__actions">' + 761 '<button type="button" class="btn btn--outline raffle-card__btn raffle-entries-btn" data-id="' + r.id + '">Entries</button>' + 762 claimStatusHtml + 763 '</div>' + 764 '</div>'; 765 var entriesBtn = card.querySelector('.raffle-entries-btn'); 766 if (entriesBtn) entriesBtn.addEventListener('click', function () { openEntriesModal(r.id); }); 767 var claimBtn = card.querySelector('.raffle-claim-btn'); 768 if (claimBtn) claimBtn.addEventListener('click', function () { 769 var wallet = getWalletPublicKey(); 770 if (!wallet) { 771 setMsg('Connect your wallet to claim.', true); 772 return; 773 } 774 claimBtn.disabled = true; 775 claimBtn.textContent = 'Claimingâ¦'; 776 fetchWithCreds(rafflesApiUrl(r.id + '/claim'), { 777 method: 'POST', 778 headers: { 'Content-Type': 'application/json' }, 779 body: JSON.stringify({ wallet: wallet }), 780 }) 781 .then(function (res) { return res.json().then(function (body) { return { status: res.status, body: body }; }); }) 782 .then(function (x) { 783 if (x.status === 200 && x.body.signature) { 784 showClaimSuccessModal(x.body.signature); 785 initRafflesPage(); 786 } else { 787 setMsg(x.body.error || 'Claim failed.', true); 788 claimBtn.disabled = false;
789 claimBtn.textContent = 'Claim'; 790 } 791 }) 792 .catch(function (err) { 793 setMsg(err && err.message ? err.message : 'Claim failed.', true); 794 claimBtn.disabled = false; 795 claimBtn.textContent = 'Claim'; 796 }); 797 }); 798 var buyBtn = card.querySelector('.raffle-buy-btn'); 799 var buyInput = card.querySelector('.raffle-card__buy-input'); 800 if (buyBtn && buyInput) { 801 buyBtn.addEventListener('click', function () { 802 var count = parseInt(buyInput.value, 10); 803 var wallet = getWalletPublicKey(); 804 if (!wallet) { 805 setMsg('Connect your wallet to buy tickets.', true); 806 return; 807 } 808 if (!Number.isInteger(count) || count < 1) { 809 setMsg('Enter a valid number of tickets.', true); 810 return; 811 } 812 if (count > maxPerWallet) { 813 setMsg('Maximum ' + maxPerWallet + ' tickets per wallet for this raffle.', true); 814 return; 815 } 816 if (remaining < 1 || count > remaining) { 817 setMsg('Not enough tickets left.', true); 818 return; 819 } 820 buyBtn.disabled = true; 821 buyBtn.textContent = 'Buyingâ¦'; 822 fetchWithCreds(rafflesApiUrl(r.id + '/my-tickets') + '?wallet=' + encodeURIComponent(wallet)) 823 .then(function (res) { return res.json(); }) 824 .then(function (data) { 825 var current = (data && typeof data.ticketCount === 'number') ? data.ticketCount : 0; 826 if (current >= maxPerWallet) { 827 buyBtn.disabled = false; 828 buyBtn.textContent = 'Buy tickets'; 829 setMsg('Maximum ' + maxPerWallet + ' tickets per wallet for this raffle. You already have ' + current + '.', true); 830 return null; 831 } 832 if (current + count > maxPerWallet) { 833 buyBtn.disabled = false; 834 buyBtn.textContent = 'Buy tickets'; 835 setMsg('You have ' + current + ' tickets. Maximum ' + maxPerWallet + ' per wallet â you can buy up to ' + (maxPerWallet - current) + ' more.', true); 836 return null; 837 } 838 return buildAndSendRafflePayment(r, count); 839 }) 840 .then(function (pay) { 841 if (!pay) return; 842 return fetchWithCreds(rafflesApiUrl(r.id + '/buy'), { 843 method: 'POST', 844 headers: { 'Content-Type': 'application/json' }, 845 body: JSON.stringify({ 846 wallet: wallet, 847 count: count, 848 signature: pay.signature, 849 paymentDestination: pay.paymentDestination, 850 }), 851 }).then(function (res) { 852 return res.json().then(function (j) { 853 return { status: res.status, body: j, signature: pay.signature }; 854 }); 855 }); 856 }) 857 .then(function (x) { 858 if (x.status !== 200) throw new Error(x.body && x.body.error ? x.body.error : 'Purchase failed'); 859 showTicketPurchaseSuccessModal(x.signature); 860 initRafflesPage(); 861 }) 862 .catch(function (err) { 863 setMsg(err && err.message ? err.message : 'Purchase failed.', true); 864 }) 865 .finally(function () { 866 buyBtn.disabled = false; 867 buyBtn.textContent = 'Buy tickets'; 868 }); 869 }); 870 } 871 if (price.mint) { 872 fetchWithCreds(window.location.origin + '/api/token-info?mint=' + encodeURIComponent(price.mint)) 873 .then(function (res) { return res.ok ? res.json() : null; }) 874 .then(function (info) { 875 if (!info || !card.parentNode) return; 876 var raw = r.ticketPriceRaw || '0'; 877 var dec = typeof info.decimals === 'number' ? info.decimals : 6; 878 var num = parseInt(raw, 10); 879 var amountStr = isNaN(num) ? raw : trimTrailingZeros((num / Math.pow(10, dec)
879).toFixed(dec)); 880 var symbol = info.symbol || info.name || 'tokens'; 881 var wrap = card.querySelector('.raffle-card__price'); 882 if (wrap) { 883 wrap.innerHTML = (info.imageUrl ? '<img class="raffle-card__token-icon" src="' + escapeHtml(info.imageUrl) + '" alt="" aria-hidden="true" />' : '') + 884 '<span class="raffle-card__price-amount">' + escapeHtml(amountStr) + '</span> <span class="raffle-card__price-symbol">' + escapeHtml(symbol) + '</span>'; 885 } 886 }) 887 .catch(function () {}); 888 } 889 return card; 890 } 891 892 function trimTrailingZeros(s) { 893 if (typeof s !== 'string') s = String(s); 894 return s.replace(/\.?0+$/, '') || '0'; 895 } 896 897 function formatPrice(r) { 898 var type = (r.ticketPriceTokenType || 'sol').toLowerCase(); 899 var raw = r.ticketPriceRaw || '0'; 900 var solLogoUrl = (window.ABSURD_APES_CONFIG && window.ABSURD_APES_CONFIG.hero && window.ABSURD_APES_CONFIG.hero.solanaLogoUrl) || '/assets/solana-logo.svg'; 901 if (type === 'sol') { 902 var lamports = parseInt(raw, 10); 903 if (isNaN(lamports)) return { amountStr: raw, symbol: 'SOL', imageUrl: solLogoUrl }; 904 var solAmount = lamports / 1e9; 905 var amountStr = trimTrailingZeros(solAmount.toFixed(Math.min(9, 4))); 906 return { amountStr: amountStr, symbol: 'SOL', imageUrl: solLogoUrl }; 907 } 908 if (type === 'aaa') { 909 var aaaRaw = parseInt(raw, 10); 910 var aaaDecimals = 6; 911 var aaaAmount = isNaN(aaaRaw) ? raw : (aaaRaw / Math.pow(10, aaaDecimals)); 912 return { amountStr: trimTrailingZeros(Number(aaaAmount).toFixed(aaaDecimals)), symbol: 'AAA', imageUrl: '' }; 913 } 914 if (type === 'custom' && r.ticketPriceTokenMint) { 915 return { amountStr: raw, symbol: 'tokens', imageUrl: '', mint: r.ticketPriceTokenMint }; 916 } 917 return { amountStr: raw, symbol: 'tokens', imageUrl: '' }; 918 } 919 920 function formatTimeLeft(endsAt) { 921 var now = new Date(); 922 if (endsAt <= now) return 'Ended'; 923 var s = Math.floor((endsAt - now) / 1000); 924 var m = Math.floor(s / 60); 925 var h = Math.floor(m / 60); 926 var d = Math.floor(h / 24); 927 if (d > 0) return d + 'd ' + (h % 24) + 'h left'; 928 if (h > 0) return h + 'h ' + (m % 60) + 'm left'; 929 if (m > 0) return m + 'm left'; 930 return s + 's left'; 931 } 932 933 function openEntriesModal(raffleId) { 934 var modal = document.getElementById('raffles-entries-modal'); 935 var list = document.getElementById('raffles-entries-list'); 936 if (!modal || !list) return; 937 list.innerHTML = '<p class="raffles-modal__hint">Loadingâ¦</p>'; 938 modal.setAttribute('aria-hidden', 'false'); 939 940 fetchWithCreds(rafflesApiUrl(raffleId + '/entries')) 941 .then(function (r) { return r.json(); }) 942 .then(function (data) { 943 var entries = data.entries || []; 944 list.innerHTML = ''; 945 if (entries.length === 0) { 946 list.innerHTML = '<p class="raffles-modal__hint">No entries yet.</p>'; 947 return; 948 } 949 entries.forEach(function (e) { 950 var row = document.createElement('div'); 951 row.className = 'raffles-entries-list__row'; 952 row.innerHTML = '<span class="raffles-entries-list__wallet">' + escapeHtml(e.walletAddress) + '</span><span class="raffles-entries-list__count">' + e.ticketCount + '</span>'; 953 list.appendChild(row); 954 }); 955 }) 956 .catch(function () { 957 list.innerHTML = '<p class="raffles-modal__hint">Failed to load entries.</p>'; 958 }); 959 } 960 961 var rafflesRefetchAfterEndThrottle = 0; 962 setInterval(function () { 963 var main = document.getElementById('main-raffles'); 964 if (!main || main.hidden) return; 965 var now = Date.now(); 966 var shouldRefetch = false; 967 document.querySelectorAll('.raffle-card__time[data-ends]').forEach(function (el) { 968 var ends = el.getAttribute('data-ends'); 969 if (!ends) return; 970 var endsAt = new Date(ends); 971 el.textContent = formatTimeLeft(endsAt); 972 if (endsAt <= new Date()) shouldRefetch = true; 973 }); 974 if (shouldRefetch && now > rafflesRefetchAfterEndThrottle) { 975 rafflesRefetchAfterEndThrottle = now + 15000; 976 initRafflesPage(); 977 } 978 }, 5000); 979 980 document.addEventListener('click', function (e) { 981 var btn = e.target && (e.target.id === 'raffles-start-btn' ? e.target : e.target.closest && e.target.closest('#raffles-start-btn')); 982 if (btn && !btn.disabled) startRaffle(); 983 }); 984 985 window.initRafflesPage = initRafflesPage; 986 987 // Load raffles list on initial page load when URL is /raffles (app.js runs before this, so showView('raffles') may have run before initRafflesPage existed) 988 var path = (window.location.pathname || '/').replace(/\/$/, '') || '/'; 989 if (path === '/raffles') initRafflesPage(); 990})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.