PageSourceSearch

https://webcade.fun/coins.js?v=1025436e

js webcade.fun collected 2026-10-08 08:27:17 UTC 5,434 bytes, 119 lines download raw bytes

1// Buying coins, from anywhere.
2//
3// This lived inside app.js, which only cabinet pages load. The moment the duel
4// lobby needed a stake, a player could arrive there with an empty balance, hit
5// CHALLENGE, be told "need 1 coin" and find nothing on the page that sells one.
6// A dead end on the first screen of a new feature.
7//
8// So it moves here, shared. app.js keeps what only a cabinet does — spending a
9// coin on a run — and this owns the shop.
10(() => {
11  const $ = id => document.getElementById(id);
12  const W = () => window.ARCADE_WALLET;
13  const fmt = n => Number(n).toLocaleString(undefined, { maximumFractionDigits: 2 });
14
15  let config = null;
16  let onBought = null;
17
18  function build() {
19    if ($('coin-modal')) return true;
20    if (!config) return false;
21    const div = document.createElement('div');
22    div.id = 'coin-modal';
23    div.className = 'hidden';
24    div.innerHTML = `
25      <div id="coin-box">
26        <p id="coin-label">BUY COINS</p>
27        <p class="muted coin-sub">${config.freeToPlay
28          ? 'A coin enters a <b>duel</b>. Cabinets are free to play &mdash; a coin buys nothing there.'
29          : '1 coin = 1 play on any cabinet'}<br>
30          Coins are granted free, as a daily allowance. They are not sold.</p>
31        <div id="pack-list"></div>
32        <p id="coin-status"></p>
33        <button id="coin-close" class="pixel-btn">CLOSE</button>
34      </div>`;
35    document.body.appendChild(div);
36    $('coin-close').addEventListener('click', () => div.classList.add('hidden'));
37    // clicking the backdrop closes it too; a modal with one way out is a trap
38    div.addEventListener('click', e => { if (e.target === div) div.classList.add('hidden'); });
39
40    const list = $('pack-list');
41    // No packs means the server has no price it trusts and is not selling.
42    // Saying so beats an empty shelf that looks like a broken page.
43    if (!config.packs.length) {
44      list.innerHTML = '<p class="muted">Price feed unavailable — coins are back in a moment.</p>';
45      return true;
46    }
47    for (const pack of config.packs) {
48      const btn = document.createElement('button');
49      btn.className = 'coin-btn pack-btn';
50      // Worked out from the pack rather than hard-coded, so the badge cannot
51      // drift from what the server is actually charging.
52      const paidFor = Math.round(pack.sol / config.coinPriceSol);
53      const bonus = pack.coins > paidFor
54        ? ` <span class="pack-bonus">+${Math.round((pack.coins / paidFor - 1) * 100)}%</span>` : '';
55      // A coin is priced in dollars, so that is the number players read. The
56      // SOL underneath is what they actually sign for, so it is shown too —
57      // quoting only a dollar figure for a SOL payment hides the real amount.
58      const price = pack.usd != null
59        ? `$${pack.usd} <span class="pack-sol">${pack.sol.toFixed(4)} SOL</span>`
60        : `${pack.sol} SOL`;
61      btn.innerHTML = `<span class="coin-slot"></span>${pack.coins} COIN${pack.coins > 1 ? 'S' : ''}${bonus} — ${price}`;
62      btn.addEventListener('click', () => buy(pack, btn));
63      list.appendChild(btn);
64    }
65    return true;
66  }
67
68  async function buy(pack, btn) {
69    btn.disabled = true;
70    const player = W() && W().publicKey;
71    const provider = W() && W().provider();
72    try {
73      if (!player || !provider) throw new Error('connect your wallet first');
74      $('coin-status').textContent = 'Building transaction…';
75      const buildRes = await fetch('/api/coins/build', {
76        method: 'POST', headers: { 'Content-Type': 'application/json' },
77        body: JSON.stringify({ player, pack: pack.id }),
78      });
79      if (!buildRes.ok) throw new Error((await buildRes.json()).error);
80      const { tx: txB64 } = await buildRes.json();
81      const tx = solanaWeb3.Transaction.from(Uint8Array.from(atob(txB64), c => c.charCodeAt(0)));
82      $('coin-status').textContent = 'Approve in your wallet…';
83      const { signature } = await provider.signAndSendTransaction(tx);
84      $('coin-status').textContent = 'Confirming payment…';
85      let done = false;
86      for (let i = 0; i < 20; i++) {
87        const r = await fetch('/api/coins/confirm', {
88          method: 'POST', headers: { 'Content-Type': 'application/json' },
89          body: JSON.stringify({ player, signature, pack: pack.id }),
90        });
91        if (r.ok) { done = true; break; }
92        const err = (await r.json()).error || '';
93        if (err.includes('already used')) throw new Error(err);
94        await new Promise(res => setTimeout(res, 1500));
95      }
96      if (!done) throw new Error('payment not confirmed — if SOL left your wallet, contact support');
97      if (W()) await W().refreshCoins();
98      $('coin-status').textContent = `${pack.coins}
98 coins added! You have ${fmt(W() ? W().coins : pack.coins)}.`;
99      if (onBought) { try { onBought(); } catch (_) {} }
100    } catch (e) {
101      $('coin-status').textContent = 'Error: ' + (e.message || e);
102    }
103    btn.disabled = false;
104  }
105
106  window.ARCADE_COINS = {
107    // pages that already fetched /api/config can hand it over rather than refetch
108    use(cfg) { config = cfg; },
109    onBought(fn) { onBought = fn; },
110    async open() {
111      if (!config) {
112        try { config = await (await fetch('/api/config')).json(); } catch (_) { return; }
113      }
114      if (!build()) return;
115      $('coin-status').textContent = '';
116      $('coin-modal').classList.remove('hidden');
117    },
118  };
119})();

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.