PageSourceSearch

https://absurd-apes.vercel.app/js/raffles.js?v=16

js absurd-apes.vercel.app collected 2026-10-03 06:03:23 UTC 48,965 bytes, 990 lines download raw bytes

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.