Every client hard-coded the chipnet beacons, address prefix and electrum servers on its own: resolver, registrar config, wallets, gateway, indexer, mirror scripts, the browser bundle and the mobile Java. A mainnet launch would have meant finding all of them and hoping none was missed. The table now lives in resolver-web.js, the one file every client already shares, so it stays a single-file drop-in. BNS_NETWORK selects the record; unset means chipnet, so nothing changes today: the live index resolves the same 60 names and 20 TLDs, the 67 offline tests pass, and the dashboard, market and studio load the same values through BNS.NETWORK. The mainnet record carries the verified public servers, the prefix and its own Sia bucket, but its beacons, start height and operator address are deliberately null: requireBeacons() refuses to scan until they are pinned in the order ROADMAP-MAINNET.md §6 requires. Bns.java reads a generated BnsNetwork.java so the phone cannot drift from the desktop clients. NETWORK-CONFIG.md records what reads the table and what a launch still pins.
155 lines
10 KiB
JavaScript
155 lines
10 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=20260927net";
|
|
|
|
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 fmtUsdC = (cents) => (window.siriusPricing?.formatUsd || ((n) => "$" + n.toFixed(2)))(Math.round(Number(cents)) / 100);
|
|
const satsForCents = (cents) => Math.round(Number(cents) / 100 * (window.siriusPricing?.CHIPNET_SATS_PER_USD || 400000));
|
|
const clampSats = (sats, l) => Math.min(Number(l.ceil_sats), Math.max(Number(l.floor_sats), sats));
|
|
const shortAddr = (a) => (a ? a.replace(/^[^:]+:/, "").slice(0, 8) + "…" + a.slice(-5) : "—");
|
|
|
|
window.siriusNetwork = BNS.NETWORK;
|
|
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.NETWORK.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));
|
|
const priceOf = (l) => (l.kind === "usd" ? clampSats(satsForCents(l.target_cents), l) : Number(l.price_sats));
|
|
rows.sort((a, b) => sort === "name" ? a.name.localeCompare(b.name) : sort === "new" ? Date.parse(b.created_at) - Date.parse(a.created_at) : priceOf(a) - priceOf(b));
|
|
$("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 13 bits 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">${l.kind === "usd"
|
|
? `<b>${esc(fmtUsdC(l.target_cents))}</b><span>fixed in USD · today ≈ ${esc(bch(satsForCents(l.target_cents)))}</span>`
|
|
: `<b>${esc(bch(l.price_sats))}</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 isUsd = selected.kind === "usd";
|
|
const price = BigInt(isUsd ? clampSats(satsForCents(selected.target_cents), selected) : selected.price_sats);
|
|
const check = isUsd ? BNS.verifyUsdListing(selected) : BNS.verifyListing(selected, { category: selected.category });
|
|
$("buy-kv").innerHTML = [
|
|
isUsd ? ["Price", `${esc(fmtUsdC(selected.target_cents))} fixed · about ${esc(bch(price))} at today's rate (the exact amount is set by the oracle quote when you buy, between ${esc(bch(selected.floor_sats))} and ${esc(bch(selected.ceil_sats))})`]
|
|
: ["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 ? (isUsd ? "contract terms verified" : "seller signature valid") : "INVALID: " + esc(check.error)],
|
|
["Extra", "≈ 16 bits 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 ${BNS.NETWORK.label}`);
|
|
const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses);
|
|
push(`wallet has ${utxos.filter((u) => !u.token).length} spendable coin(s)`);
|
|
let built, signed;
|
|
if (selected.kind === "usd") {
|
|
const q = await (await fetch(`${API}/api/price/oracle?txid=${encodeURIComponent(selected.outpoint.txid)}`, { cache: "no-store" })).json();
|
|
if (!q.message) throw new Error(q.error || "no oracle quote");
|
|
push(`oracle quote: 1 BCH = $${(q.price_cents / 100).toFixed(2)} (median of ${q.median_of} exchanges)`);
|
|
built = BNS.buildUsdBuyTx({ listing: selected, quote: q, buyerAddress: wallet.address, buyerTokenAddress: wallet.tokenAddress, utxos, records: selected.records || {}, addressToLockingBytecode: BNS.addressToLockingBytecode });
|
|
push(`you pay ${bch(built.priceSats)} · fee ${bits(built.fee)}`);
|
|
signed = BNS.signInputs(built.transaction, built.sourceOutputs, (h) => wallet.keyFor(h), { skip: [built.covenantInputIndex] });
|
|
} else {
|
|
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)}`);
|
|
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(); });
|