git / moonbite-network / templates/markets.html · raw
{% extends "base.html" %} {% block body_class %}mb-v2{% endblock %} {% block title %}MBITE Internal Exchange — non-custodial order book{% endblock %} {% block meta_description %}Post and browse MoonBite (MBITE) buy/sell order intents against LTC, BTC and USDT. Non-custodial: the server never holds coins or keys — settlement is a wallet-to-wallet atomic swap.{% endblock %} {% block content %} <section class="mb-hero"> <div class="mb-hero-glow"></div> <div class="mb-container"> <div class="mb-hero-inner"> <span class="mb-badge"><span class="mb-dot"></span> Non-custodial order book</span> <h1 class="mb-hero-title">An exchange that <span class="mb-gradient-text">can't hold your coins.</span></h1> <p class="mb-hero-sub">Post what you'd trade MBITE for, or take someone else's offer — in LTC, BTC or USDT. This board only introduces buyers and sellers. Your coins and keys never touch our server; you settle wallet-to-wallet with an atomic swap.</p> <div class="mb-cta-row"> <a href="#book" class="btn btn-primary mb-btn-xl">Open the order book</a> <a href="#place" class="btn btn-secondary mb-btn-xl">Post an order</a> </div> </div> </div> </section> <section class="mb-section" id="book"> <div class="mb-container"> <div class="mb-banner mb-reveal" style="margin-bottom: var(--spacing-2xl);"> <span><strong>This is not a live market.</strong> Every number below is an offer someone typed in — not a traded price, not a valuation. MBITE has no exchange listing yet and MoonBite is <strong>not an investment or a security</strong>. The server holds no funds; every trade settles directly between two wallets.</span> </div> <div class="mb-section-head mb-reveal"> <span class="mb-eyebrow">Order book</span> <h2 class="mb-section-title">Who's offering what.</h2> <p class="mb-section-sub">Pick a pair. Green is people wanting to buy MBITE (bids); red is people selling it (asks). Every row is a real person's standing offer.</p> </div> <div class="mb-ex-tabs mb-reveal" role="tablist" id="mbExTabs"> <button class="mb-ex-tab is-active" data-pair="MBITE/LTC" role="tab">MBITE / LTC</button> <button class="mb-ex-tab" data-pair="MBITE/BTC" role="tab">MBITE / BTC</button> <button class="mb-ex-tab" data-pair="MBITE/USDT" role="tab">MBITE / USDT</button> </div> <div class="mb-ex-lastbar mb-reveal"> <span class="mb-ex-last-label">Last settled price</span> <span class="mb-ex-last-value" id="mbExLast">—</span> <span class="mb-ex-last-quote" id="mbExLastQuote"></span> <span class="mb-ex-ease" id="mbExEase"></span> </div> <div class="mb-ex-book mb-reveal"> <div class="mb-ex-side"> <div class="mb-ex-side-head mb-ex-bids">Bids · buying MBITE</div> <table class="mb-ex-table"> <thead><tr><th>Price</th><th>Amount (MBITE)</th><th>Total</th><th>Settle</th></tr></thead> <tbody id="mbExBids"><tr><td colspan="4" class="mb-ex-empty">Loading…</td></tr></tbody> </table> </div> <div class="mb-ex-side"> <div class="mb-ex-side-head mb-ex-asks">Asks · selling MBITE</div> <table class="mb-ex-table"> <thead><tr><th>Price</th><th>Amount (MBITE)</th><th>Total</th><th>Settle</th></tr></thead> <tbody id="mbExAsks"><tr><td colspan="4" class="mb-ex-empty">Loading…</td></tr></tbody> </table> </div> </div> </div> </section> <section class="mb-section" id="place" style="background: var(--bg-secondary); border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);"> <div class="mb-container"> <div class="mb-section-head mb-reveal"> <span class="mb-eyebrow">Post an order</span> <h2 class="mb-section-title">Name your price.</h2> <p class="mb-section-sub">You're posting a public offer, not sending coins. Drop in the address where you'd get paid so a taker knows where to send. Nothing moves until you both run the swap.</p> </div> <form class="mb-ex-form mb-reveal" id="mbExForm" autocomplete="off"> <div class="mb-ex-field mb-ex-sidepick"> <label>I want to</label> <div class="mb-ex-toggle"> <button type="button" class="mb-ex-sidebtn is-active" data-side="buy">Buy MBITE</button> <button type="button" class="mb-ex-sidebtn" data-side="sell">Sell MBITE</button> </div> </div> <div class="mb-ex-field"> <label for="mbExPair">Pair</label> <select id="mbExPair" name="pair"> <option value="MBITE/LTC">MBITE / LTC</option> <option value="MBITE/BTC">MBITE / BTC</option> <option value="MBITE/USDT">MBITE / USDT</option> </select> </div> <div class="mb-ex-field"> <label for="mbExPrice">Price <span class="mb-ex-qlabel" id="mbExPriceQuote">(LTC per 1 MBITE)</span></label> <input id="mbExPrice" name="price" type="number" step="any" min="0" placeholder="0.00" required> </div> <div class="mb-ex-field"> <label for="mbExAmount">Amount (MBITE)</label> <input id="mbExAmount" name="amount" type="number" step="any" min="0" placeholder="0" required> </div> <div class="mb-ex-field"> <label for="mbExMbite">Your MBITE address</label> <input id="mbExMbite" name="mbite_address" type="text" placeholder="moon1…" required> </div> <div class="mb-ex-field"> <label for="mbExQuote">Your <span id="mbExQuoteName">LTC</span> address</label> <input id="mbExQuote" name="quote_address" type="text" placeholder="ltc1…" required> </div> <div class="mb-ex-field mb-ex-field-wide"> <div class="mb-ex-total" id="mbExFormTotal"></div> <button type="submit" class="btn btn-primary mb-btn-xl" id="mbExSubmit">Post order</button> </div> <div class="mb-ex-formmsg" id="mbExMsg" role="status" aria-live="polite"></div> </form> <p class="mb-ex-cancelnote mb-reveal">Made a mistake? Paste your order ID and the cancel token shown when you posted it.</p> <form class="mb-ex-cancel mb-reveal" id="mbExCancelForm" autocomplete="off"> <input type="text" id="mbExCancelId" placeholder="order id" required> <input type="text" id="mbExCancelToken" placeholder="cancel token" required> <button type="submit" class="btn btn-secondary">Cancel order</button> <span class="mb-ex-formmsg" id="mbExCancelMsg" role="status" aria-live="polite"></span> </form> </div> </section> <section class="mb-section"> <div class="mb-container"> <div class="mb-section-head mb-reveal"> <span class="mb-eyebrow">How settlement works</span> <h2 class="mb-section-title">No middleman ever holds the money.</h2> <p class="mb-section-sub">The order book is just where two people shake hands. When you agree, the coins move directly between your wallets — the math guarantees it, not us.</p> </div> <div class="mb-grid mb-grid-3 mb-reveal"> <div class="mb-card"> <h3 class="mb-card-title">1 · Post or take an offer</h3> <p class="mb-card-text">Add a buy or sell order, or grab one you like from the book. The only thing you share is the address where you get paid.</p> </div> <div class="mb-card"> <h3 class="mb-card-title">2 · Atomic swap</h3> <p class="mb-card-text">Both sides lock funds in a hash-time-locked contract. Either the full trade clears, or everyone gets their coins back. There is no version where one side walks away with both.</p> </div> <div class="mb-card"> <h3 class="mb-card-title">3 · Done</h3> <p class="mb-card-text">MBITE ⇄ LTC or BTC swaps natively — they share the Bitcoin script family. USDT needs a smart-contract leg. All the server ever did was note that you two matched.</p> </div> </div> <div class="mb-banner mb-reveal" style="margin-top: var(--spacing-2xl);"> <span> <strong>Automated swap coordination isn't built yet (Phase 2).</strong> Today the exchange matches orders and hands you both the addresses; you run the atomic swap with an external tool. Funds will never route through this server — not now, not after Phase 2.</span> </div> </div> </section> <section class="mb-section" id="notify" style="background: var(--bg-secondary); border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);"> <div class="mb-container"> {% set notify_title = "Know the moment the swap engine ships" %} {% set notify_text = "Leave an email and we'll ping you when the automated atomic-swap coordinator goes live or a new pair opens. Nothing else — no price alerts, no spam." %} {% set notify_source = "markets" %} {% include "partials/notify_form.html" %} </div> </section> <section class="mb-section"> <div class="mb-container"> <div class="mb-banner mb-reveal"> <span>MoonBite is a live, open-source project. The internal exchange is non-custodial software — it stores public order intents only and never holds coins, keys or balances. Nothing here is financial advice or an offer to sell a security.</span> </div> </div> </section> <!-- Atomic-swap hand-off modal (populated from /api/exchange/order/<id>/settle) --> <div class="mb-ex-modal" id="mbExModal" hidden> <div class="mb-ex-modal-overlay" data-close></div> <div class="mb-ex-modal-card" role="dialog" aria-modal="true" aria-labelledby="mbExModalTitle"> <button class="mb-ex-modal-x" data-close aria-label="Close">×</button> <div class="mb-ex-modal-eyebrow">Atomic-swap hand-off</div> <h3 id="mbExModalTitle">Settle this order</h3> <div class="mb-ex-modal-summary" id="mbExModalSummary"></div> <div class="mb-ex-legs" id="mbExModalLegs"></div> <div class="mb-ex-field" style="margin-top: var(--spacing-lg);"> <label for="mbExTakerAddr">Your <span id="mbExTakerAsset">receiving</span> address <span class="mb-ex-qlabel">(optional — completes the summary you share with the maker)</span></label> <input id="mbExTakerAddr" type="text" placeholder="where you receive the coins"> </div> <div class="mb-ex-modal-note" id="mbExModalNote"></div> </div> </div> {% endblock %} {% block scripts_extra %} <script nonce="{{ csp_nonce }}"> (function () { "use strict"; var PAIRS = null; // filled from /api/exchange/pairs var currentPair = "MBITE/LTC"; var currentSide = "buy"; var elBids = document.getElementById("mbExBids"); var elAsks = document.getElementById("mbExAsks"); var elLast = document.getElementById("mbExLast"); var elLastQuote = document.getElementById("mbExLastQuote"); var elEase = document.getElementById("mbExEase"); function quoteOf(pair) { return (PAIRS && PAIRS[pair]) ? PAIRS[pair].quote : pair.split("/")[1]; } function easeOf(pair) { return (PAIRS && PAIRS[pair]) ? PAIRS[pair].ease : ""; } function esc(s) { return String(s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } function fmt(n) { var v = Number(n); return isFinite(v) ? v.toLocaleString(undefined, { maximumFractionDigits: 8 }) : String(n); } function rowsHtml(orders, quote, empty) { if (!orders || !orders.length) { return '<tr><td colspan="4" class="mb-ex-empty">' + empty + "</td></tr>"; } return orders.map(function (o) { var total = Number(o.price) * Number(o.amount); return "<tr>" + "<td>" + fmt(o.price) + "</td>" + "<td>" + fmt(o.amount) + "</td>" + "<td>" + fmt(total) + " " + esc(quote) + "</td>" + '<td class="mb-ex-actions">' + '<button class="mb-ex-takebtn" data-id="' + esc(o.id) + '">Take</button>' + '<button class="mb-ex-idbtn" data-id="' + esc(o.id) + '" title="Copy order id">id</button>' + "</td></tr>"; }).join(""); } function loadBook() { var quote = quoteOf(currentPair); elEase.textContent = easeOf(currentPair) === "native" ? "native atomic swap" : (easeOf(currentPair) === "contract" ? "needs contract leg" : ""); fetch("/api/exchange/orders?pair=" + encodeURIComponent(currentPair)) .then(function (r) { return r.json(); }) .then(function (d) { if (!d || d.status !== "success") throw new Error(d && d.message || "load failed"); elBids.innerHTML = rowsHtml(d.bids, quote, "No buy orders yet — be the first."); elAsks.innerHTML = rowsHtml(d.asks, quote, "No sell orders yet — be the first."); elLast.textContent = d.last_price != null ? fmt(d.last_price) : "—"; elLastQuote.textContent = d.last_price != null ? quote + " per MBITE" : "no settled trades yet"; wireActionButtons(); }) .catch(function (e) { elBids.innerHTML = elAsks.innerHTML = '<tr><td colspan="4" class="mb-ex-empty">Could not load: ' + esc(e.message) + "</td></tr>"; }); } function wireActionButtons() { document.querySelectorAll(".mb-ex-idbtn").forEach(function (b) { b.addEventListener("click", function () { var id = b.getAttribute("data-id"); document.getElementById("mbExCancelId").value = id; if (navigator.clipboard) navigator.clipboard.writeText(id).catch(function () {}); b.textContent = ""; setTimeout(function () { b.textContent = "id"; }, 1200); }); }); document.querySelectorAll(".mb-ex-takebtn").forEach(function (b) { b.addEventListener("click", function () { openSettle(b.getAttribute("data-id")); }); }); } /* ---- settle hand-off modal ---- */ var modal = document.getElementById("mbExModal"); var settleData = null; // last-fetched settle_hint payload function closeSettle() { modal.hidden = true; document.body.style.overflow = ""; } modal.querySelectorAll("[data-close]").forEach(function (el) { el.addEventListener("click", closeSettle); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && !modal.hidden) closeSettle(); }); function openSettle(id) { fetch("/api/exchange/order/" + encodeURIComponent(id) + "/settle") .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }) .then(function (res) { if (!res.ok || res.d.status !== "success") throw new Error(res.d.message || "Could not load order"); settleData = res.d; document.getElementById("mbExTakerAddr").value = ""; renderSettle(); modal.hidden = false; document.body.style.overflow = "hidden"; }) .catch(function (err) { alert(err.message); }); } function renderSettle() { var o = settleData.order; var sw = settleData.swap; var quote = sw.quote_asset; var total = Number(o.price) * Number(o.amount); // From the *taker's* perspective. Maker "sell" => taker buys MBITE. var takerBuysMbite = (o.side === "sell"); var summary = document.getElementById("mbExModalSummary"); summary.innerHTML = '<div class="mb-ex-modal-pair">' + esc(o.pair) + " · you " + (takerBuysMbite ? "buy" : "sell") + " MBITE</div>" + "<div class=\"mb-ex-modal-nums\">" + "<span>" + fmt(o.amount) + " MBITE</span>" + "<span>@ " + fmt(o.price) + " " + esc(quote) + "</span>" + "<span>= " + fmt(total) + " " + esc(quote) + "</span>" + "</div>"; // The address the taker paid the maker to for each leg. The other leg // goes to the taker's own address (optional field below). var takerAddr = (document.getElementById("mbExTakerAddr") || {}).value || ""; takerAddr = takerAddr.trim(); var legs = document.getElementById("mbExModalLegs"); var sendLeg, recvLeg; if (takerBuysMbite) { sendLeg = { label: "You send", asset: quote, amt: fmt(total) + " " + esc(quote), to: sw.maker_quote_address, toLabel: "maker's " + esc(quote) + " address" }; recvLeg = { label: "You receive", asset: "MBITE", amt: fmt(o.amount) + " MBITE", to: takerAddr || "— your MBITE address —", toLabel: "your MBITE address" }; } else { sendLeg = { label: "You send", asset: "MBITE", amt: fmt(o.amount) + " MBITE", to: sw.maker_mbite_address, toLabel: "maker's MBITE address" }; recvLeg = { label: "You receive", asset: quote, amt: fmt(total) + " " + esc(quote), to: takerAddr || "— your " + esc(quote) + " address —", toLabel: "your " + esc(quote) + " address" }; } document.getElementById("mbExTakerAsset").textContent = recvLeg.asset; function legHtml(leg, kind) { return '<div class="mb-ex-leg mb-ex-leg-' + kind + '">' + '<div class="mb-ex-leg-label">' + leg.label + "</div>" + '<div class="mb-ex-leg-amt">' + leg.amt + "</div>" + '<div class="mb-ex-leg-to">→ ' + esc(leg.toLabel) + "</div>" + '<code class="mb-ex-leg-addr">' + esc(leg.to) + "</code>" + "</div>"; } legs.innerHTML = legHtml(sendLeg, "send") + '<div class="mb-ex-leg-swap">⇄</div>' + legHtml(recvLeg, "recv"); document.getElementById("mbExModalNote").innerHTML = "<strong>" + esc(sw.mechanism) + " · status: " + esc(sw.status) + ".</strong> " + esc(sw.note); } var takerInput = document.getElementById("mbExTakerAddr"); if (takerInput) takerInput.addEventListener("input", function () { if (settleData) renderSettle(); }); /* ---- pair tabs ---- */ document.querySelectorAll(".mb-ex-tab").forEach(function (tab) { tab.addEventListener("click", function () { document.querySelectorAll(".mb-ex-tab").forEach(function (t) { t.classList.remove("is-active"); }); tab.classList.add("is-active"); currentPair = tab.getAttribute("data-pair"); document.getElementById("mbExPair").value = currentPair; syncQuoteLabels(); loadBook(); }); }); /* ---- form quote labels + side ---- */ function syncQuoteLabels() { var pair = document.getElementById("mbExPair").value; var q = quoteOf(pair); document.getElementById("mbExPriceQuote").textContent = "(" + q + " per 1 MBITE)"; document.getElementById("mbExQuoteName").textContent = q; updateFormTotal(); } function updateFormTotal() { var p = Number(document.getElementById("mbExPrice").value); var a = Number(document.getElementById("mbExAmount").value); var q = quoteOf(document.getElementById("mbExPair").value); var box = document.getElementById("mbExFormTotal"); if (p > 0 && a > 0) { box.textContent = (currentSide === "buy" ? "You'd pay ≈ " : "You'd receive ≈ ") + fmt(p * a) + " " + q; } else { box.textContent = ""; } } document.getElementById("mbExPair").addEventListener("change", function () { currentPair = this.value; document.querySelectorAll(".mb-ex-tab").forEach(function (t) { t.classList.toggle("is-active", t.getAttribute("data-pair") === currentPair); }); syncQuoteLabels(); loadBook(); }); document.getElementById("mbExPrice").addEventListener("input", updateFormTotal); document.getElementById("mbExAmount").addEventListener("input", updateFormTotal); document.querySelectorAll(".mb-ex-sidebtn").forEach(function (b) { b.addEventListener("click", function () { document.querySelectorAll(".mb-ex-sidebtn").forEach(function (x) { x.classList.remove("is-active"); }); b.classList.add("is-active"); currentSide = b.getAttribute("data-side"); document.getElementById("mbExSubmit").textContent = currentSide === "buy" ? "Post buy order" : "Post sell order"; updateFormTotal(); }); }); /* ---- submit new order ---- */ document.getElementById("mbExForm").addEventListener("submit", function (e) { e.preventDefault(); var msg = document.getElementById("mbExMsg"); var btn = document.getElementById("mbExSubmit"); var body = { side: currentSide, pair: document.getElementById("mbExPair").value, price: document.getElementById("mbExPrice").value, amount: document.getElementById("mbExAmount").value, mbite_address: document.getElementById("mbExMbite").value, quote_address: document.getElementById("mbExQuote").value }; btn.disabled = true; msg.className = "mb-ex-formmsg"; msg.textContent = "Posting…"; fetch("/api/exchange/order", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }) .then(function (res) { if (!res.ok || res.d.status !== "success") throw new Error(res.d.message || "Could not post order"); msg.className = "mb-ex-formmsg is-ok"; msg.textContent = "Order posted. ID: " + res.d.order.id + " — cancel token: " + res.d.order.cancel_token + " (save both; the token is shown only once and is required to cancel)."; document.getElementById("mbExPrice").value = ""; document.getElementById("mbExAmount").value = ""; updateFormTotal(); if (body.pair === currentPair) loadBook(); }) .catch(function (err) { msg.className = "mb-ex-formmsg is-err"; msg.textContent = err.message; }) .finally(function () { btn.disabled = false; }); }); /* ---- cancel order ---- */ document.getElementById("mbExCancelForm").addEventListener("submit", function (e) { e.preventDefault(); var msg = document.getElementById("mbExCancelMsg"); var id = document.getElementById("mbExCancelId").value.trim(); var token = document.getElementById("mbExCancelToken").value.trim(); msg.className = "mb-ex-formmsg"; msg.textContent = "Cancelling…"; fetch("/api/exchange/order/" + encodeURIComponent(id) + "/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cancel_token: token }) }).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }) .then(function (res) { if (!res.ok || res.d.status !== "success") throw new Error(res.d.message || "Could not cancel"); msg.className = "mb-ex-formmsg is-ok"; msg.textContent = "Order cancelled."; loadBook(); }) .catch(function (err) { msg.className = "mb-ex-formmsg is-err"; msg.textContent = err.message; }); }); /* ---- boot ---- */ fetch("/api/exchange/pairs") .then(function (r) { return r.json(); }) .then(function (d) { if (d && d.status === "success") PAIRS = d.pairs; }) .catch(function () {}) .finally(function () { syncQuoteLabels(); loadBook(); }); setInterval(loadBook, 15000); })(); </script> {% endblock %}