Reviewed both pages at desktop and phone widths. Desktop was fine; on phones three things were wrong. The top bar wrapped into four rows because the font-comparison pill and full-size links all stayed; on screens under 640px the pill is hidden and links and buttons tighten. The dashboard menu rendered as five full-width blocks because the wide-screen width:100% rule outranked the phone override; it is now a row of pills, the wallet block collapses to one line (short address, live balance, refresh), and the sub-tabs scroll sideways instead of wrapping. The long address had also been pushing the whole page wider than the viewport, fixed with minmax(0,1fr) on the grid column. The market showed an empty filter bar and a one-line status when there were no listings; it now shows a proper empty state with the two things a visitor can do (sell from the dashboard, register a new name) and hides the filters until there is something to filter. Buy buttons go full width on phones.
133 lines
8 KiB
JavaScript
133 lines
8 KiB
JavaScript
// Marketplace page: list the gateway's active offers and complete a purchase.
|
|
//
|
|
// A listing is the seller's partially signed transaction (see
|
|
// Argus/src/lib/market-tx.js). Buying = verify it locally, append our own
|
|
// funding inputs and the outputs that give us the certificate and update
|
|
// the registry, sign our inputs, broadcast. The seller's signature only
|
|
// stays valid while their certificate UTXO is unspent, so the gateway prunes
|
|
// stale offers and the buyer's broadcast is the final arbiter.
|
|
|
|
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market";
|
|
|
|
const API = "https://silentmode.st";
|
|
const $ = (id) => document.getElementById(id);
|
|
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
const fmtInt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
const usd = (sats) => { const r = window.siriusPricing?.CHIPNET_SATS_PER_USD || 250000; const v = Number(sats) / r; return (window.siriusPricing?.formatUsd || ((n) => "$" + n.toFixed(2)))(Math.round(v * 100) / 100); };
|
|
const shortAddr = (a) => (a ? a.replace(/^[^:]+:/, "").slice(0, 8) + "…" + a.slice(-5) : "—");
|
|
|
|
let listings = [];
|
|
let wallet = null;
|
|
let selected = null;
|
|
|
|
async function restoreWallet() {
|
|
if (window.siriusWallet) return window.siriusWallet;
|
|
const S = window.siriusSession;
|
|
if (!S?.restore) return null;
|
|
try {
|
|
const s = await S.restore();
|
|
if (!s?.mnemonic) return null;
|
|
const w = await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.CHIPNET_PREFIX, s.accountPath || undefined);
|
|
window.siriusWallet = w;
|
|
return w;
|
|
} catch { return null; }
|
|
}
|
|
|
|
async function load() {
|
|
const status = $("status");
|
|
try {
|
|
const r = await fetch(`${API}/api/market`, { cache: "no-store" });
|
|
if (!r.ok) throw new Error("API " + r.status);
|
|
const j = await r.json();
|
|
listings = (j.listings || []).map((l) => ({ ...l, label: l.name.split(".")[0], tld: l.tld || l.name.split(".").pop() }));
|
|
render();
|
|
} catch (e) { status.className = "status err"; status.textContent = "Could not load listings: " + (e.message || e); }
|
|
}
|
|
function render() {
|
|
const q = ($("q").value || "").toLowerCase().trim();
|
|
const sort = $("sort").value;
|
|
const tldSel = $("tld");
|
|
const tlds = [...new Set(listings.map((l) => l.tld))].sort();
|
|
const keep = tldSel.value;
|
|
tldSel.innerHTML = `<option value="">All TLDs</option>` + tlds.map((t) => `<option value="${esc(t)}"${t === keep ? " selected" : ""}>.${esc(t)}</option>`).join("");
|
|
let rows = listings.filter((l) => (!q || l.name.includes(q)) && (!tldSel.value || l.tld === tldSel.value));
|
|
rows.sort((a, b) => sort === "name" ? a.name.localeCompare(b.name) : sort === "new" ? Date.parse(b.created_at) - Date.parse(a.created_at) : Number(a.price_sats) - Number(b.price_sats));
|
|
$("count").textContent = `${rows.length} of ${listings.length}`;
|
|
const status = $("status");
|
|
document.querySelector(".toolbar").hidden = !listings.length;
|
|
if (!listings.length) {
|
|
status.hidden = true;
|
|
$("list").innerHTML = `<div class="empty big">
|
|
<div class="ico">🛒</div>
|
|
<h3>Nothing is for sale right now</h3>
|
|
<p>Names go on sale when their owners list them from the dashboard. Have one to sell, or want a fresh one instead?</p>
|
|
<div class="cta"><a class="btn acid" href="./portal.html#domains">Sell a name from your dashboard →</a><a class="btn ghost" href="./#search-input">Register a new name</a></div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
status.hidden = false;
|
|
status.className = "status"; status.textContent = `${listings.length} name${listings.length === 1 ? "" : "s"} for sale. Prices are set by the sellers; you pay the price plus about 1,300 sat of chain dust and fee.`;
|
|
$("list").innerHTML = rows.length ? rows.map((l) => `
|
|
<div class="row">
|
|
<div class="n">${esc(l.label)}.<span class="tld">${esc(l.tld)}</span> ${wallet && l.seller === wallet.address ? '<span class="badge">yours</span>' : ""}</div>
|
|
<div class="price"><b>${fmtInt(l.price_sats)} sat</b><span>≈ ${esc(usd(l.price_sats))}</span></div>
|
|
<div><button class="btn acid small" data-buy="${esc(l.name)}" ${wallet && l.seller === wallet.address ? "disabled" : ""}>Buy →</button></div>
|
|
<div class="meta"><span>seller ${esc(shortAddr(l.seller))}</span><span>·</span><span>listed ${esc(new Date(l.created_at).toLocaleDateString())}</span><span>·</span><a href="${API}/bns/${encodeURIComponent(l.name)}/" target="_blank" rel="noopener">view site →</a></div>
|
|
</div>`).join("") : `<div class="empty">Nothing matches that filter.</div>`;
|
|
}
|
|
["q", "sort", "tld"].forEach((id) => $(id).addEventListener("input", render));
|
|
document.addEventListener("click", (e) => { const b = e.target.closest("[data-buy]"); if (b) openBuy(b.dataset.buy); });
|
|
|
|
// ---------- buy ----------
|
|
function setMsg(t, cls = "") { $("buy-msg").textContent = t; $("buy-msg").className = "msg " + cls; }
|
|
function push(t) { const log = $("buy-steps"); log.className = "steps on"; const d = document.createElement("div"); d.textContent = t; log.appendChild(d); log.scrollTop = 1e6; }
|
|
function openBuy(name) {
|
|
selected = listings.find((l) => l.name === name); if (!selected) return;
|
|
$("buy-name").textContent = name;
|
|
const price = BigInt(selected.price_sats);
|
|
const check = BNS.verifyListing(selected, { category: selected.category });
|
|
$("buy-kv").innerHTML = [
|
|
["Price", `${fmtInt(price)} sat (≈ ${esc(usd(price))})`],
|
|
["Seller", esc(selected.seller)],
|
|
["You receive", "the name's certificate, to your token address"],
|
|
["Offer check", check.ok ? "seller signature valid" : "INVALID: " + esc(check.error)],
|
|
["Extra", "≈ 1,600 sat dust + fee"],
|
|
].map(([k, v]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join("");
|
|
$("buy-steps").className = "steps"; $("buy-steps").innerHTML = ""; setMsg("");
|
|
const canBuy = !!wallet && check.ok && wallet.address !== selected.seller;
|
|
$("buy-go").disabled = !canBuy; $("buy-go").textContent = "Buy — sign & broadcast";
|
|
$("buy-signin").hidden = !!wallet;
|
|
if (!wallet) setMsg("Sign in on the dashboard first — the purchase is signed with your wallet in this browser.");
|
|
$("buy").classList.add("open");
|
|
}
|
|
const closeBuy = () => $("buy").classList.remove("open");
|
|
$("buy-close").addEventListener("click", closeBuy); $("buy-cancel").addEventListener("click", closeBuy);
|
|
$("buy").addEventListener("click", (e) => { if (e.target === $("buy")) closeBuy(); });
|
|
$("buy-go").addEventListener("click", async () => {
|
|
if (!selected || !wallet) return;
|
|
const btn = $("buy-go"); btn.disabled = true; btn.textContent = "Buying…"; setMsg("");
|
|
let el = null;
|
|
try {
|
|
el = await BNS.connect(); push("connected to chipnet");
|
|
const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses);
|
|
push(`wallet has ${utxos.filter((u) => !u.token).length} spendable coin(s)`);
|
|
const built = BNS.completeSale({ listing: selected, buyerAddress: wallet.address, buyerTokenAddress: wallet.tokenAddress, utxos, records: selected.records || {}, addressToLockingBytecode: BNS.addressToLockingBytecode });
|
|
push(`built sale · price ${fmtInt(built.costs.priceSats)} sat · fee ${built.costs.chainFeeSats} sat`);
|
|
const signed = BNS.signInputs(built.transaction, built.sourceOutputs, (h) => wallet.keyFor(h), { skip: [built.sellerInputIndex] });
|
|
push("signed your inputs (the seller's signature is already in place)");
|
|
const txid = await BNS.broadcast(el, signed.hex);
|
|
push("broadcast: " + txid);
|
|
setMsg(`Done. ${selected.name} is yours — it appears in your dashboard within a minute.`, "ok");
|
|
btn.textContent = "Bought ✓";
|
|
setTimeout(load, 3000);
|
|
} catch (e) {
|
|
const raw = String(e.message || e);
|
|
setMsg(/not enough funds/i.test(raw) ? `Not enough coins: ${raw}. Fund ${wallet.address} first.` : "Failed: " + raw, "err");
|
|
push("error: " + raw); btn.disabled = false; btn.textContent = "Buy — sign & broadcast";
|
|
} finally { try { el?.close?.(); } catch {} }
|
|
});
|
|
|
|
(async function boot() {
|
|
wallet = await restoreWallet();
|
|
await load();
|
|
})();
|