// 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 bch = (sats) => (window.siriusPricing?.formatBch || ((s) => (Number(s) / 1e8).toFixed(8).replace(/0+$/, "").replace(/\.$/, "") + " BCH"))(sats); const bits = (sats) => (window.siriusPricing?.formatBits || ((s) => (Number(s) / 100).toLocaleString("en-US") + " bits"))(sats); 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 = `` + tlds.map((t) => ``).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 = `
πŸ›’

Nothing is for sale right now

Names go on sale when their owners list them from the dashboard. Have one to sell, or want a fresh one instead?

Sell a name from your dashboard β†’Register a new name
`; 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 13 bits of chain dust and fee.`; $("list").innerHTML = rows.length ? rows.map((l) => `
${esc(l.label)}.${esc(l.tld)} ${wallet && l.seller === wallet.address ? 'yours' : ""}
${esc(bch(l.price_sats))}β‰ˆ ${esc(usd(l.price_sats))}
seller ${esc(shortAddr(l.seller))}Β·listed ${esc(new Date(l.created_at).toLocaleDateString())}Β·view site β†’
`).join("") : `
Nothing matches that filter.
`; } ["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", `${esc(bch(price))} (β‰ˆ ${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", "β‰ˆ 16 bits dust + fee"], ].map(([k, v]) => `
${k}
${v}
`).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 ${bch(built.costs.priceSats)} Β· fee ${bits(built.costs.chainFeeSats)}`); 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(); })(); window.addEventListener("sirius:price", () => { if (listings.length) render(); });