git / moonbite-network / website/js/spark.js · raw

/* ============================================================   Spark — browser mining REHEARSAL (honest).   - Real CPU throughput probe via Web Workers (a SHA-256 stand-in     integer-mix loop). It is NOT the real scrypt PoW and earns     NO coins. Everything the user sees is literally true.   - Consent gates every cycle. Stop terminates all workers.   - On-battery devices auto-pause below 20% (when the API exists).   No trackers, no network calls.   ============================================================ */(function () {  "use strict";  var THREAD_STEPS = [1, 2, 4, 8];  var LABELS = ["LOW", "MEDIUM", "HIGH", "MAX"];  var maxCores = (navigator.hardwareConcurrency && navigator.hardwareConcurrency > 0)    ? navigator.hardwareConcurrency : 4;  // Cap the tick choices to the machine's real core count (never claim more).  var steps = THREAD_STEPS.filter(function (n) { return n <= maxCores; });  if (steps.length === 0) steps = [1];  if (steps[steps.length - 1] !== maxCores && maxCores < 8) steps.push(maxCores);  var el = function (id) { return document.getElementById(id); };  el("maxThreads").textContent = String(maxCores);  var slider = el("intensity");  slider.max = String(steps.length - 1);  slider.value = "0";  function selectedThreads() { return steps[parseInt(slider.value, 10)] || 1; }  function selectedLabel() {    var i = parseInt(slider.value, 10);    return LABELS[Math.min(i, LABELS.length - 1)];  }  function syncSlider() {    el("useThreads").textContent = String(selectedThreads());    el("intensityLabel").textContent = selectedLabel();  }  slider.addEventListener("input", syncSlider);  syncSlider();  // ---- Worker source (self-contained via Blob) ----  var workerSrc = [    "var running=true;",    "self.onmessage=function(e){ if(e.data && e.data.cmd==='stop'){ running=false; } };",    "function mix(x){ x^=x<<13; x^=x>>>7; x^=x<<17; return x>>>0; }",    "var x=(Math.random()*4294967295)>>>0, count=0, last=performance.now();",    "function chunk(){",    "  if(!running) return;",    "  for(var i=0;i<150000;i++){ x=mix(x); count++; }",    "  var now=performance.now();",    "  if(now-last>200){ self.postMessage(count); count=0; last=now; }",    "  setTimeout(chunk,0);",    "}",    "chunk();"  ].join("\n");  var workerUrl = URL.createObjectURL(new Blob([workerSrc], { type: "text/javascript" }));  var workers = [];  var running = false;  var sessionCycles = 0;  var windowCycles = 0;  var windowStart = 0;  var rafId = null;  function fmt(n) { return Math.round(n).toLocaleString(); }  function startMining() {    var n = selectedThreads();    running = true;    sessionCycles = 0; windowCycles = 0; windowStart = performance.now();    for (var i = 0; i < n; i++) {      var w = new Worker(workerUrl);      w.onmessage = function (e) {        if (!running) return;        sessionCycles += e.data;        windowCycles += e.data;      };      workers.push(w);    }    el("consent").hidden = true;    el("mining").hidden = false;    el("stopBtn").hidden = false;    var t = el("thermal"); t.hidden = false;    setThermal(n);    tick();    watchBattery();  }  function setThermal(n) {    var dots = document.querySelectorAll(".thermal__dot");    var lit = n <= steps[0] ? 1 : (n >= maxCores ? 3 : 2);    dots.forEach(function (d, i) {      d.classList.toggle("on", i < lit && lit < 3);      d.classList.toggle("warn", lit === 3 && i < lit);    });  }  function tick() {    el("probeNum").textContent = fmt(sessionCycles);    var now = performance.now();    var dt = (now - windowStart) / 1000;    if (dt >= 0.5) {      el("rate").textContent = fmt(windowCycles / dt);      windowCycles = 0; windowStart = now;    }    if (running) rafId = requestAnimationFrame(tick);  }  function stopMining(reason) {    running = false;    workers.forEach(function (w) { try { w.postMessage({ cmd: "stop" }); w.terminate(); } catch (e) {} });    workers = [];    if (rafId) cancelAnimationFrame(rafId);    el("stopBtn").hidden = true;    el("thermal").hidden = true;    if (reason) {      showToast(reason);    } else {      // Return to consent, honestly reset.      el("mining").hidden = true;      el("consent").hidden = false;    }  }  function showToast(msg) {    var t = el("toast");    t.textContent = msg;    t.hidden = false;  }  // ---- Battery guard (honest, only if the API exists) ----  function watchBattery() {    if (!navigator.getBattery) return;    navigator.getBattery().then(function (batt) {      function check() {        if (running && !batt.charging && batt.level < 0.2) {          stopMining("Battery under 20% — Spark paused to protect your device. Resume when charging.");        }      }      batt.addEventListener("levelchange", check);      batt.addEventListener("chargingchange", check);      check();      // Also surface a real battery reading on the consent line for next time.      var pct = Math.round(batt.level * 100);      el("battLine").textContent =        "This device is at " + pct + "% " + (batt.charging ? "(charging)" : "(on battery)") +        "; on battery, Spark pauses below 20%.";    }).catch(function () {});  }  // Populate battery line on load too.  watchBattery();  el("startBtn").addEventListener("click", startMining);  el("stopBtn").addEventListener("click", function () { stopMining(null); });  el("declineBtn").addEventListener("click", function () {    el("consent").querySelectorAll("input,button").forEach(function (n) {      if (n.id !== "declineBtn") n.setAttribute("disabled", "disabled");    });    el("declinedMsg").hidden = false;  });  // ============================================================  // Mine to the LIVE chain — asks the explorer to mine one block  // to your address (the node does the real proof-of-work).  // ============================================================  (function liveMine() {    var urlInput = el("explorerUrl");    var addrInput = el("mineAddress");    var btn = el("mineLiveBtn");    var status = el("liveStatus");    if (!urlInput || !addrInput || !btn) return;    // Prefill explorer URL from ?api=..., then localStorage, then the live default.    var DEFAULT_EXPLORER = "https://moonbite.org";    var params = new URLSearchParams(location.search);    var apiParam = params.get("api");    if (apiParam) {      urlInput.value = apiParam;      try { localStorage.setItem("moonbite_explorer", apiParam); } catch (e) {}    } else {      try {        var saved = localStorage.getItem("moonbite_explorer");        if (saved) urlInput.value = saved;      } catch (e) {}    }    if (!urlInput.value) urlInput.value = DEFAULT_EXPLORER;    try {      var savedAddr = localStorage.getItem("moonbite_address");      if (savedAddr) addrInput.value = savedAddr;    } catch (e) {}    function setStatus(msg, kind) {      status.hidden = false;      status.textContent = msg;      status.className = "livemine__status mono" + (kind ? " is-" + kind : "");    }    function normBase(u) {      u = (u || "").trim().replace(/\/+$/, "");      if (u && !/^https?:\/\//i.test(u)) u = "https://" + u;      return u;    }    var busy = false;    btn.addEventListener("click", function () {      if (busy) return;      var base = normBase(urlInput.value);      var addr = addrInput.value.trim();      if (!base) { setStatus("Enter your explorer URL first.", "err"); return; }      if (!addr) { setStatus("Enter your MoonBite reward address.", "err"); return; }      try {        localStorage.setItem("moonbite_explorer", base);        localStorage.setItem("moonbite_address", addr);      } catch (e) {}      busy = true;      btn.setAttribute("disabled", "disabled");      setStatus("Mining… the node is doing the proof-of-work.", "busy");      fetch(base + "/api/mine", {        method: "POST",        headers: { "Content-Type": "application/json" },        body: JSON.stringify({ address: addr })      }).then(function (r) {        return r.json().then(function (body) { return { ok: r.ok, body: body }; });      }).then(function (res) {        if (!res.ok) {          setStatus("Node says: " + (res.body.error || "mining failed") , "err");        } else if (res.body.found) {          setStatus("Block #" + res.body.height + " mined to you ✓  " + res.body.hashes[0], "ok");        } else {          setStatus("No block this round — click again to keep trying.", "busy");        }      }).catch(function (e) {        setStatus("Could not reach explorer at " + base + " (" + e.message + ").", "err");      }).then(function () {        busy = false;        btn.removeAttribute("disabled");      });    });  })();})();