diff --git a/docs/index.html b/docs/index.html index c9ce624..c9b6ec2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -604,9 +604,9 @@ process.exit(0);" - + - + diff --git a/i18n/de.json b/i18n/de.json index 49c7382..38d5d82 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -564,7 +564,6 @@ ". A buyer completes it in one transaction — you are paid in the same transaction that moves the name, or not at all. No escrow, nothing to trust.": ". Ein Käufer schließt es in einer Transaktion ab — du wirst in derselben Transaktion bezahlt, die den Namen überträgt, oder gar nicht. Kein Treuhänder, nichts, dem man vertrauen muss.", "Checking the market…": "Markt wird geprüft…", "Price (USD)": "Preis (USD)", - "Price (sat)": "Preis (Sat)", "List for sale →": "Zum Verkauf anbieten →", "Cancel listing →": "Angebot zurückziehen →", "Cancelling removes the offer from the market and moves your certificate once (a no-op update, a few cents), so the signed offer can never be completed later.": "Beim Zurückziehen wird das Angebot vom Markt entfernt und dein Zertifikat einmal bewegt (ein leeres Update, ein paar Cent), damit die signierte Offerte später nie mehr abgeschlossen werden kann.", @@ -581,7 +580,6 @@ "Request declined": "Anfrage abgelehnt", "Open dashboard →": "Dashboard öffnen →", "incl.": "inkl.", - "sat unconfirmed · spendable now": "Sat unbestätigt · sofort verfügbar", "Nothing is for sale right now": "Derzeit steht nichts zum Verkauf", "Names go on sale when their owners list them from the dashboard. Have one to sell, or want a fresh one instead?": "Namen kommen in den Verkauf, wenn ihre Besitzer sie im Dashboard anbieten. Hast du einen zu verkaufen — oder lieber einen neuen registrieren?", "Sell a name from your dashboard →": "Einen Namen im Dashboard verkaufen →", @@ -604,5 +602,7 @@ "Restyle selection": "Auswahl umgestalten", "Nothing selected": "Nichts ausgewählt", "Undo last change": "Letzte Änderung rückgängig", - "Stop": "Stopp" + "Stop": "Stopp", + "unconfirmed · spendable now": "unbestätigt · sofort verfügbar", + "Price (BCH)": "Preis (BCH)" } diff --git a/i18n/el.json b/i18n/el.json index 1ccaaf6..96b0b02 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -564,7 +564,6 @@ ". A buyer completes it in one transaction — you are paid in the same transaction that moves the name, or not at all. No escrow, nothing to trust.": ". Ο αγοραστής την ολοκληρώνει σε μία συναλλαγή — πληρώνεσαι στην ίδια συναλλαγή που μεταφέρει το όνομα, ή καθόλου. Χωρίς μεσεγγύηση, τίποτα προς εμπιστοσύνη.", "Checking the market…": "Έλεγχος της αγοράς…", "Price (USD)": "Τιμή (USD)", - "Price (sat)": "Τιμή (sat)", "List for sale →": "Καταχώρηση προς πώληση →", "Cancel listing →": "Ακύρωση αγγελίας →", "Cancelling removes the offer from the market and moves your certificate once (a no-op update, a few cents), so the signed offer can never be completed later.": "Η ακύρωση αφαιρεί την προσφορά από την αγορά και μετακινεί το πιστοποιητικό σου μία φορά (μια κενή ενημέρωση, λίγα λεπτά), ώστε η υπογεγραμμένη προσφορά να μην μπορεί ποτέ να ολοκληρωθεί αργότερα.", @@ -581,7 +580,6 @@ "Request declined": "Το αίτημα απορρίφθηκε", "Open dashboard →": "Άνοιγμα πίνακα ελέγχου →", "incl.": "συμπ.", - "sat unconfirmed · spendable now": "sat ανεπιβεβαίωτα · διαθέσιμα τώρα", "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 →": "Πούλησε ένα όνομα από τον πίνακα ελέγχου →", @@ -604,5 +602,7 @@ "Restyle selection": "Άλλαξε στυλ στην επιλογή", "Nothing selected": "Τίποτα επιλεγμένο", "Undo last change": "Αναίρεση τελευταίας αλλαγής", - "Stop": "Στοπ" + "Stop": "Στοπ", + "unconfirmed · spendable now": "ανεπιβεβαίωτα · διαθέσιμα τώρα", + "Price (BCH)": "Τιμή (BCH)" } diff --git a/i18n/es.json b/i18n/es.json index fb00ce9..f0cfe5a 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -564,7 +564,6 @@ ". A buyer completes it in one transaction — you are paid in the same transaction that moves the name, or not at all. No escrow, nothing to trust.": ". Un comprador la completa en una transacción — cobras en la misma transacción que mueve el nombre, o no cobras. Sin custodia, nada en lo que confiar.", "Checking the market…": "Consultando el mercado…", "Price (USD)": "Precio (USD)", - "Price (sat)": "Precio (sat)", "List for sale →": "Poner en venta →", "Cancel listing →": "Cancelar anuncio →", "Cancelling removes the offer from the market and moves your certificate once (a no-op update, a few cents), so the signed offer can never be completed later.": "Cancelar retira la oferta del mercado y mueve tu certificado una vez (una actualización vacía, unos céntimos), para que la oferta firmada nunca pueda completarse después.", @@ -581,7 +580,6 @@ "Request declined": "Solicitud rechazada", "Open dashboard →": "Abrir panel →", "incl.": "incl.", - "sat unconfirmed · spendable now": "sat sin confirmar · disponibles ya", "Nothing is for sale right now": "Ahora mismo no hay nada en venta", "Names go on sale when their owners list them from the dashboard. Have one to sell, or want a fresh one instead?": "Los nombres salen a la venta cuando sus dueños los publican desde el panel. ¿Tienes uno para vender, o prefieres uno nuevo?", "Sell a name from your dashboard →": "Vender un nombre desde tu panel →", @@ -604,5 +602,7 @@ "Restyle selection": "Cambiar estilo de la selección", "Nothing selected": "Nada seleccionado", "Undo last change": "Deshacer el último cambio", - "Stop": "Detener" + "Stop": "Detener", + "unconfirmed · spendable now": "sin confirmar · disponibles ya", + "Price (BCH)": "Precio (BCH)" } diff --git a/i18n/fr.json b/i18n/fr.json index 7419c17..56003fe 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -564,7 +564,6 @@ ". A buyer completes it in one transaction — you are paid in the same transaction that moves the name, or not at all. No escrow, nothing to trust.": ". Un acheteur la complète en une transaction — vous êtes payé dans la même transaction qui transfère le nom, ou pas du tout. Pas de séquestre, rien à devoir croire.", "Checking the market…": "Vérification du marché…", "Price (USD)": "Prix (USD)", - "Price (sat)": "Prix (sat)", "List for sale →": "Mettre en vente →", "Cancel listing →": "Annuler l'annonce →", "Cancelling removes the offer from the market and moves your certificate once (a no-op update, a few cents), so the signed offer can never be completed later.": "L'annulation retire l'offre du marché et déplace votre certificat une fois (une mise à jour vide, quelques centimes), afin que l'offre signée ne puisse jamais être complétée plus tard.", @@ -581,7 +580,6 @@ "Request declined": "Demande refusée", "Open dashboard →": "Ouvrir le tableau de bord →", "incl.": "dont", - "sat unconfirmed · spendable now": "sat non confirmés · disponibles maintenant", "Nothing is for sale right now": "Rien n'est à vendre pour l'instant", "Names go on sale when their owners list them from the dashboard. Have one to sell, or want a fresh one instead?": "Les noms sont mis en vente lorsque leurs propriétaires les listent depuis le tableau de bord. Vous en avez un à vendre, ou vous en voulez un tout neuf ?", "Sell a name from your dashboard →": "Vendre un nom depuis le tableau de bord →", @@ -604,5 +602,7 @@ "Restyle selection": "Restyler la sélection", "Nothing selected": "Rien de sélectionné", "Undo last change": "Annuler la dernière modification", - "Stop": "Arrêter" + "Stop": "Arrêter", + "unconfirmed · spendable now": "non confirmés · disponibles maintenant", + "Price (BCH)": "Prix (BCH)" } diff --git a/i18n/pt.json b/i18n/pt.json index cb295ea..8cd77fe 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -564,7 +564,6 @@ ". A buyer completes it in one transaction — you are paid in the same transaction that moves the name, or not at all. No escrow, nothing to trust.": ". Um comprador completa-a numa transação — recebe na mesma transação que move o nome, ou não recebe de todo. Sem custódia, nada em que confiar.", "Checking the market…": "A verificar o mercado…", "Price (USD)": "Preço (USD)", - "Price (sat)": "Preço (sat)", "List for sale →": "Pôr à venda →", "Cancel listing →": "Cancelar anúncio →", "Cancelling removes the offer from the market and moves your certificate once (a no-op update, a few cents), so the signed offer can never be completed later.": "Cancelar retira a oferta do mercado e move o seu certificado uma vez (uma atualização vazia, alguns cêntimos), para que a oferta assinada nunca possa ser concluída mais tarde.", @@ -581,7 +580,6 @@ "Request declined": "Pedido recusado", "Open dashboard →": "Abrir painel →", "incl.": "incl.", - "sat unconfirmed · spendable now": "sat por confirmar · disponíveis já", "Nothing is for sale right now": "De momento não há nada à venda", "Names go on sale when their owners list them from the dashboard. Have one to sell, or want a fresh one instead?": "Os nomes são postos à venda quando os donos os anunciam a partir do painel. Tem um para vender, ou prefere um novo?", "Sell a name from your dashboard →": "Vender um nome a partir do painel →", @@ -604,5 +602,7 @@ "Restyle selection": "Restilizar a seleção", "Nothing selected": "Nada selecionado", "Undo last change": "Anular a última alteração", - "Stop": "Parar" + "Stop": "Parar", + "unconfirmed · spendable now": "por confirmar · disponíveis já", + "Price (BCH)": "Preço (BCH)" } diff --git a/i18n/ru.json b/i18n/ru.json index a7fe455..334afb0 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -564,7 +564,6 @@ ". A buyer completes it in one transaction — you are paid in the same transaction that moves the name, or not at all. No escrow, nothing to trust.": ". Покупатель завершает её одной транзакцией — вы получаете оплату в той же транзакции, которая передаёт имя, либо не получаете вовсе. Без эскроу, доверять никому не нужно.", "Checking the market…": "Проверяем маркет…", "Price (USD)": "Цена (USD)", - "Price (sat)": "Цена (сат)", "List for sale →": "Выставить на продажу →", "Cancel listing →": "Снять с продажи →", "Cancelling removes the offer from the market and moves your certificate once (a no-op update, a few cents), so the signed offer can never be completed later.": "Отмена убирает оферту с маркета и один раз перемещает ваш сертификат (пустое обновление, несколько центов), чтобы подписанную оферту нельзя было исполнить позже.", @@ -581,7 +580,6 @@ "Request declined": "Запрос отклонён", "Open dashboard →": "Открыть панель →", "incl.": "вкл.", - "sat unconfirmed · spendable now": "сат не подтверждено · доступно сейчас", "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 →": "Продать имя из панели →", @@ -604,5 +602,7 @@ "Restyle selection": "Изменить стиль выбранного", "Nothing selected": "Ничего не выбрано", "Undo last change": "Отменить последнее изменение", - "Stop": "Стоп" + "Stop": "Стоп", + "unconfirmed · spendable now": "не подтверждено · доступно сейчас", + "Price (BCH)": "Цена (BCH)" } diff --git a/index.html b/index.html index 06f7f14..1af3178 100644 --- a/index.html +++ b/index.html @@ -495,13 +495,13 @@ process.exit(0);" - + - + - + diff --git a/js/dashboard.js b/js/dashboard.js index d7fa65d..3674572 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -174,6 +174,8 @@ document.querySelectorAll("[data-copy]").forEach((b) => b.addEventListener("clic // here, same as the register flow. Polled while the tab is visible and // refreshed right after anything this page broadcasts. const setText = (id, v) => { const el = $(id); if (el) el.textContent = v; }; +const fmtBits = (sats) => (window.siriusPricing?.formatBits || ((s) => (Number(s) / 100).toLocaleString("en-US") + " bits"))(sats); +const fmtBch = (sats) => (window.siriusPricing?.formatBch || ((s) => (Number(s) / 1e8).toFixed(8).replace(/0+$/, "").replace(/\.$/, "") + " BCH"))(sats); let balEl = null, balTimer = null, balBusy = false; async function refreshBalance() { if (!wallet || balBusy) return; @@ -183,10 +185,10 @@ async function refreshBalance() { if (!balEl) balEl = await BNS.connect(); const b = await BNS.getBalance(balEl, wallet.watchedAddresses); const pending = b.sats - b.confirmedSats; - setText("side-balance", fmtInt(b.sats)); - setText("w-sats", `${fmtInt(b.sats)} sat${pending > 0n ? ` (${fmtInt(pending)} unconfirmed, spendable now)` : ""}`); + setText("side-balance", fmtBits(b.sats)); + setText("w-sats", `${fmtBits(b.sats)} · ${fmtBch(b.sats)}${pending > 0n ? ` (${fmtBits(pending)} unconfirmed, spendable now)` : ""}`); setText("w-utxos", String(b.coins)); - const pend = $("side-pending"); if (pend) { pend.hidden = pending <= 0n; setText("side-pending-sats", fmtInt(pending)); } + const pend = $("side-pending"); if (pend) { pend.hidden = pending <= 0n; setText("side-pending-sats", fmtBits(pending)); } H.wallet = { ...(H.wallet || {}), sats: b.sats.toString(), utxo_count: b.coins }; try { localStorage.setItem("siriusWalletInfo", JSON.stringify({ sats: b.sats.toString(), utxos: b.coins, names: H.names.length, tlds: H.tlds.length, at: Date.now() })); } catch {} window.dispatchEvent(new CustomEvent("sirius:balance", { detail: { sats: b.sats.toString(), pending: pending.toString() } })); @@ -220,8 +222,7 @@ async function loadHoldings() { const setText = (id, v) => { const el = $(id); if (el) el.textContent = v; }; setText("cnt-names", H.names.length); setText("cnt-tlds", H.tlds.length); if (H.wallet?.sats != null && $("side-balance").textContent === "—") { - const s = fmtInt(H.wallet.sats); - setText("side-balance", s); setText("w-sats", `${s} sat`); setText("w-utxos", String(H.wallet.utxo_count)); + setText("side-balance", fmtBits(H.wallet.sats)); setText("w-sats", `${fmtBits(H.wallet.sats)} · ${fmtBch(H.wallet.sats)}`); setText("w-utxos", String(H.wallet.utxo_count)); } try { const prev = JSON.parse(localStorage.getItem("siriusWalletInfo") || "{}"); localStorage.setItem("siriusWalletInfo", JSON.stringify({ ...prev, sats: prev.sats ?? String(H.wallet?.sats ?? ""), names: H.names.length, tlds: H.tlds.length, at: Date.now() })); } catch {} if (H.chain?.height != null) setText("stat-height", fmtInt(H.chain.height)); @@ -661,12 +662,12 @@ const RATE = () => window.siriusPricing?.CHIPNET_SATS_PER_USD || 250000; let activeListing = null; function paintSell() { const st = $("sell-status"); - $("sell-rate").textContent = `rate ${fmtInt(RATE())} sat/USD (chipnet)`; + $("sell-rate").textContent = `1 BCH ≈ ${fmtUsd(Math.round(1e8 / RATE() * 100) / 100)}${window.siriusPricing?.priceInfo ? " · " + window.siriusPricing.priceInfo() : ""}`; if (activeListing) { st.className = "status ok"; st.textContent = "This name is listed on the market."; $("sell-form").classList.add("hidden"); $("sell-active").classList.remove("hidden"); const p = BigInt(activeListing.price_sats); - $("sell-kv").innerHTML = [["Price", `${fmtInt(p)} sat (≈ ${esc(fmtUsd(Math.round(Number(p) / RATE() * 100) / 100))})`], ["Listed", esc(new Date(activeListing.created_at).toLocaleString())], ["Offer", `${esc(activeListing.partial_tx.slice(0, 40))}…`], ["Market page", `open →`]] + $("sell-kv").innerHTML = [["Price", `${esc(fmtBch(p))} (≈ ${esc(fmtUsd(Math.round(Number(p) / RATE() * 100) / 100))})`], ["Listed", esc(new Date(activeListing.created_at).toLocaleString())], ["Offer", `${esc(activeListing.partial_tx.slice(0, 40))}…`], ["Market page", `open →`]] .map(([k, v]) => `
${k}
${v}
`).join(""); } else { st.className = "status"; st.textContent = "Not for sale. Set a price to list it."; @@ -678,13 +679,14 @@ async function loadListing(name) { try { const r = await fetch(`${API}/api/market/${encodeURIComponent(name)}`, { cache: "no-store" }); if (r.ok) activeListing = await r.json(); } catch {} if (current.name === name) paintSell(); } -$("sell-usd").addEventListener("input", () => { const v = Number(String($("sell-usd").value).replace(/[^\d.]/g, "")); $("sell-sats").value = v > 0 ? String(Math.round(v * RATE())) : ""; }); -$("sell-sats").addEventListener("input", () => { const s = Number(String($("sell-sats").value).replace(/\D/g, "")); $("sell-usd").value = s > 0 ? String(Math.round(s / RATE() * 100) / 100) : ""; }); +const bchStr = (sats) => (Number(sats) / 1e8).toFixed(8).replace(/0+$/, "").replace(/\.$/, ""); +$("sell-usd").addEventListener("input", () => { const v = Number(String($("sell-usd").value).replace(/[^\d.]/g, "")); $("sell-bch").value = v > 0 ? bchStr(Math.round(v * RATE())) : ""; }); +$("sell-bch").addEventListener("input", () => { const b = Number(String($("sell-bch").value).replace(/[^\d.]/g, "")); $("sell-usd").value = b > 0 ? String(Math.round(b * 1e8 / RATE() * 100) / 100) : ""; }); $("sell-list").addEventListener("click", async () => { if (!current.entry || !wallet) return; if (wallet.source === "wc") return setMsg("sell-msg", "Listing from an external wallet lands in the next drop — sign in with the built-in wallet.", "err"); - const sats = BigInt(Number(String($("sell-sats").value).replace(/\D/g, "")) || 0); - if (sats < 10000n) return setMsg("sell-msg", "Set a price of at least 10,000 sat.", "err"); + const sats = BigInt(Math.round((Number(String($("sell-bch").value).replace(/[^\d.]/g, "")) || 0) * 1e8)); + if (sats < 10000n) return setMsg("sell-msg", "Set a price of at least 0.0001 BCH.", "err"); const push = logger("sell-log"); const btn = $("sell-list"); btn.disabled = true; btn.textContent = "Signing…"; setMsg("sell-msg", ""); let el = null; try { @@ -701,7 +703,7 @@ $("sell-list").addEventListener("click", async () => { if (!r.ok) throw new Error(j.error || `API ${r.status}`); push("listed on the market"); activeListing = j.listing; paintSell(); - setMsg("sell-msg", `Listed for ${fmtInt(sats)} sat. Nothing moves until a buyer pays that price.`, "ok"); + setMsg("sell-msg", `Listed for ${fmtBch(sats)}. Nothing moves until a buyer pays that price.`, "ok"); } catch (e) { setMsg("sell-msg", "Failed: " + (e.message || e), "err"); push("error: " + (e.message || e)); } finally { btn.disabled = false; btn.textContent = "List for sale →"; try { el?.close?.(); } catch {} } }); @@ -948,12 +950,11 @@ $("tld-ed-save").addEventListener("click", async () => { }); // ---------- new TLD ---------- -const CHIPNET_SATS_PER_USD = 250_000; -function usdToSats(usd) { const n = Number(String(usd).replace(/[^\d.]/g, "")) || 0; return Math.max(0, Math.round(n * CHIPNET_SATS_PER_USD)); } +function usdToSats(usd) { const n = Number(String(usd).replace(/[^\d.]/g, "")) || 0; return Math.max(0, Math.round(n * RATE())); } function updateTldPrice() { const sats = usdToSats($("tld-usd").value.trim() || "0"); - $("tld-price").innerHTML = `= ${sats.toLocaleString()} sats · rate ${CHIPNET_SATS_PER_USD.toLocaleString()} sats/USD (chipnet)`; - $("tld-submit").textContent = `Register (~${sats.toLocaleString()} sats)`; + $("tld-price").innerHTML = `= ${esc(fmtBits(sats))} · 1 BCH ≈ ${esc(fmtUsd(Math.round(1e8 / RATE() * 100) / 100))}${window.siriusPricing?.priceInfo ? " · " + esc(window.siriusPricing.priceInfo()) : ""}`; + $("tld-submit").textContent = `Register (~${fmtBits(sats)})`; } const validateTldLabel = (l) => /^[a-z0-9-]{1,16}$/.test(l); function updateTldButtons() { const ok = validateTldLabel($("tld-label").value.trim().toLowerCase()); $("tld-check").disabled = !ok || !wallet; $("tld-submit").disabled = !ok || !wallet; } @@ -1027,3 +1028,6 @@ $("pin-save").addEventListener("click", async () => { catch (e) { setMsg("pin-msg", e.message || String(e), "err"); } }); $("set-clearpin").addEventListener("click", () => { window.siriusPin?.clear?.(); paintSettings(); setMsg("set-msg", "PIN removed.", "ok"); }); + +// Live BCH/USD rate (pricing.js) — repaint everything priced in USD. +window.addEventListener("sirius:price", () => { try { updateTldPrice(); } catch {} try { if (current.name) paintSell(); } catch {} }); diff --git a/js/market.js b/js/market.js index d83c98b..2b741f5 100644 --- a/js/market.js +++ b/js/market.js @@ -13,6 +13,8 @@ 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) : "—"); @@ -66,11 +68,11 @@ function render() { 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.`; + 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' : ""}
-
${fmtInt(l.price_sats)} sat≈ ${esc(usd(l.price_sats))}
+
${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.
`; @@ -87,11 +89,11 @@ function openBuy(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))})`], + ["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", "≈ 1,600 sat dust + fee"], + ["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; @@ -112,7 +114,7 @@ $("buy-go").addEventListener("click", async () => { 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`); + 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); @@ -131,3 +133,4 @@ $("buy-go").addEventListener("click", async () => { wallet = await restoreWallet(); await load(); })(); +window.addEventListener("sirius:price", () => { if (listings.length) render(); }); diff --git a/js/pricing.js b/js/pricing.js index be71321..c93abd7 100644 --- a/js/pricing.js +++ b/js/pricing.js @@ -16,9 +16,52 @@ // Chipnet placeholder rate: mainnet swaps this for a real BCH/USD oracle. // The numbers keep the demo transactions in the "few chipnet coins" range // so a single faucet visit covers a name or a TLD. - const CHIPNET_SATS_PER_USD = 250_000; - - const usdToSats = (usd) => BigInt(Math.max(0, Math.round(Number(usd) * CHIPNET_SATS_PER_USD))); + // BCH/USD rate. Live: the gateway's /api/price (median of several exchanges) + // or, if that is unreachable, the same public tickers straight from the + // browser; the last good rate is remembered per browser; the constant is + // only the very first paint before anything answers. Chipnet coins have no + // market, so the mainnet rate is what makes "$7 per name" mean something. + const FALLBACK_SATS_PER_USD = 400_000; + let SATS_PER_USD = FALLBACK_SATS_PER_USD; + let priceMeta = { usd: null, at: 0, sources: 0, origin: "fallback" }; + try { const c = JSON.parse(localStorage.getItem("siriusPrice") || "null"); if (c && c.usd > 0) { SATS_PER_USD = Math.round(1e8 / c.usd); priceMeta = { ...c, origin: "cached" }; } } catch {} + const usdToSats = (usd) => BigInt(Math.max(0, Math.round(Number(usd) * SATS_PER_USD))); + const PRICE_API = "https://silentmode.st/api/price"; + const BROWSER_SOURCES = [ + ["coinbase", "https://api.coinbase.com/v2/prices/BCH-USD/spot", (j) => Number(j?.data?.amount)], + ["kraken", "https://api.kraken.com/0/public/Ticker?pair=BCHUSD", (j) => Number(Object.values(j?.result || {})[0]?.c?.[0])], + ["binance", "https://api.binance.com/api/v3/ticker/price?symbol=BCHUSDT", (j) => Number(j?.price)], + ["coingecko", "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin-cash&vs_currencies=usd", (j) => Number(j?.["bitcoin-cash"]?.usd)], + ]; + function applyPrice(usd, sources, origin) { + if (!(usd > 0)) return false; + SATS_PER_USD = Math.round(1e8 / usd); + priceMeta = { usd, at: Date.now(), sources, origin }; + try { localStorage.setItem("siriusPrice", JSON.stringify(priceMeta)); } catch {} + try { window.dispatchEvent(new CustomEvent("sirius:price", { detail: priceMeta })); } catch {} + return true; + } + let priceLoad = null; + function loadPrice({ force = false } = {}) { + if (priceLoad && !force) return priceLoad; + priceLoad = (async () => { + try { + const r = await fetch(PRICE_API, { cache: "no-store", signal: AbortSignal.timeout(6000) }); + const j = await r.json(); + if (j && j.usd > 0 && !j.stale) return applyPrice(j.usd, j.median_of || 0, "gateway"); + } catch {} + const got = (await Promise.allSettled(BROWSER_SOURCES.map(async ([name, url, pick]) => { + const r = await fetch(url, { signal: AbortSignal.timeout(6000) }); if (!r.ok) throw new Error(String(r.status)); + const v = pick(await r.json()); if (!(v > 0)) throw new Error("no price"); return v; + }))).filter((x) => x.status === "fulfilled").map((x) => x.value).sort((a, b) => a - b); + if (got.length) { const m = Math.floor(got.length / 2); return applyPrice(got.length % 2 ? got[m] : (got[m - 1] + got[m]) / 2, got.length, "exchanges"); } + return false; + })(); + return priceLoad; + } + const priceInfo = () => priceMeta.usd ? `${priceMeta.origin === "gateway" || priceMeta.origin === "exchanges" ? `median of ${priceMeta.sources} exchange${priceMeta.sources === 1 ? "" : "s"}` : priceMeta.origin === "cached" ? "last known rate" : "fallback rate"}${priceMeta.at ? ", " + Math.max(0, Math.round((Date.now() - priceMeta.at) / 60000)) + " min ago" : ""}` : "fallback rate"; + loadPrice(); + setInterval(() => loadPrice({ force: true }), 5 * 60 * 1000); const cleanLabel = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9-]/g, ""); @@ -201,9 +244,22 @@ if (n >= 1 && n === Math.floor(n)) return `$${n}`; return `$${n.toFixed(2)}`; } - function formatSats(sats) { - return `${BigInt(sats || 0n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} sat`; + // Money is shown in BCH (prices) and bits (fees, balances); 1 bit = 100 + // satoshi, 1 BCH = 1,000,000 bits. Satoshi never appears in the UI. + const group = (s) => String(s).replace(/\B(?=(\d{3})+(?!\d))/g, ","); + function formatBch(sats) { + const n = BigInt(sats || 0n); const neg = n < 0n; const a = neg ? -n : n; + const whole = a / 100000000n, frac = (a % 100000000n).toString().padStart(8, "0").replace(/0+$/, ""); + return `${neg ? "-" : ""}${group(whole)}${frac ? "." + frac : ""} BCH`; } + function formatBits(sats) { + const n = BigInt(sats || 0n); const neg = n < 0n; const a = neg ? -n : n; + const whole = a / 100n, frac = (a % 100n).toString().padStart(2, "0").replace(/0+$/, ""); + return `${neg ? "-" : ""}${group(whole)}${frac ? "." + frac : ""} bits`; + } + const formatSats = formatBits; // legacy name, same unit policy + const bchToSats = (bch) => BigInt(Math.max(0, Math.round(Number(String(bch).replace(/[^\d.]/g, "")) * 1e8))); + const satsToBch = (sats) => Number(BigInt(sats || 0n)) / 1e8; function round2(n) { return Math.round(Number(n) * 100) / 100; } window.siriusPricing = { @@ -211,8 +267,18 @@ priceForTld, formatUsd, formatSats, + formatBch, + formatBits, + bchToSats, + satsToBch, usdToSats, - CHIPNET_SATS_PER_USD, + // live rate (getters: callers that read the old constant see the current value) + get CHIPNET_SATS_PER_USD() { return SATS_PER_USD; }, + get SATS_PER_USD() { return SATS_PER_USD; }, + get bchUsd() { return priceMeta.usd || 1e8 / SATS_PER_USD; }, + priceInfo, + loadPrice, + priceReady: () => priceLoad || loadPrice(), // TLD owner policy layer loadTldPolicies, setTldPolicies, diff --git a/js/profile-menu.js b/js/profile-menu.js index 5c63000..abd3592 100644 --- a/js/profile-menu.js +++ b/js/profile-menu.js @@ -66,7 +66,7 @@ // Wallet-only menu: identity, balance, receive, settings, sign out. // Names/TLDs/admin live behind the Dashboard button next to this one. const info = readWalletInfo(); - const bal = info?.sats != null ? `${fmtInt(info.sats)} sat` : "—"; + const bal = info?.sats != null ? (window.siriusPricing?.formatBits ? window.siriusPricing.formatBits(info.sats) : `${(Number(info.sats) / 100).toLocaleString("en-US")} bits`) : "—"; const held = info ? `${info.names ?? 0} name${info.names === 1 ? "" : "s"} · ${info.tlds ?? 0} TLD${info.tlds === 1 ? "" : "s"}` : "open the dashboard to load"; menu.innerHTML = `
diff --git a/js/register-flow.js b/js/register-flow.js index d22560d..a10a0e5 100644 --- a/js/register-flow.js +++ b/js/register-flow.js @@ -11,6 +11,8 @@ import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market" const $ = (id) => document.getElementById(id); const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[c])); +// Amounts are shown in bits (1 bit = 100 satoshi); satoshi never reach the UI. +const bits = (v) => (window.siriusPricing?.formatBits || ((s) => (Number(s) / 100).toLocaleString("en-US") + " bits"))(v); const sats = (v) => Number(v).toLocaleString("en-US"); // ---------- inject CSS + modal HTML ---------- @@ -632,7 +634,7 @@ async function stepExternalConfirm(session, label) { const quote = await BNS.quoteRegistration(c, { name: state.name, ownerAddress: owner, records: {}, ...feeOverride }); const rows = BNS.priceSummary(quote.costs).map((l) => ` ${esc(l.label)}${l.note ? `${esc(l.note)}` : ""} - ${sats(l.sats)} sat`).join(""); + ${bits(l.sats)}`).join(""); render(`

Confirm ${esc(quote.displayName)}

@@ -900,7 +902,7 @@ async function stepFund() { try { const prev = JSON.parse(localStorage.getItem("siriusWalletInfo") || "{}"); localStorage.setItem("siriusWalletInfo", JSON.stringify({ ...prev, sats: b.sats.toString(), utxos: b.coins, at: Date.now() })); } catch {} const enough = b.sats >= 20000n; $("bal").innerHTML = b.sats > 0n - ? `${sats(b.sats)} sat available (${b.coins} coin${b.coins===1?"":"s"})` + ? `${bits(b.sats)} available (${b.coins} coin${b.coins===1?"":"s"})` + (enough ? "" : ` — a little more is needed to cover the registration.`) : `Waiting for coins… this page is watching the chain live.`; $("next").disabled = !enough; @@ -933,7 +935,7 @@ async function stepConfirm() { const rows = BNS.priceSummary(quote.costs).map((l) => ` ${esc(l.label)}${l.note ? `${esc(l.note)}` : ""} - ${sats(l.sats)} sat`).join(""); + ${bits(l.sats)}`).join(""); render(`

Confirm ${esc(quote.displayName)}

@@ -967,9 +969,9 @@ async function stepConfirmTld() { the registry operator for every second-level name under it.

- + - +
Service fee${feeSats === 0n ? " (none on chipnet)" : ""}${sats(feeSats)} sat
${bits(feeSats)}
Beacon dust + chain fee (covered by wallet)~1,300 sat
~13 bits
Minted to ${esc(owner)}. That wallet becomes the operator for this TLD.
@@ -990,7 +992,7 @@ async function stepConfirmTld() { // opts: { feeSats, kind: "name"|"tld", back, next } function stepApprove(opts) { const { feeSats, kind, back, next } = opts; - const feeLine = feeSats && feeSats > 0n ? `${sats(feeSats)} sat` : `Service fee waived on chipnet`; + const feeLine = feeSats && feeSats > 0n ? `${bits(feeSats)}` : `Service fee waived on chipnet`; const target = kind === "tld" ? `.${esc(state.name)}` : `${esc(state.name)}`; // Signed in with a live wallet and no "ask for PIN before payments" diff --git a/portal.html b/portal.html index cc000f8..1aca912 100644 --- a/portal.html +++ b/portal.html @@ -298,8 +298,8 @@
Signed in
—
-
chipnet · — sat
- +
chipnet · —
+
@@ -364,7 +364,7 @@
-
+
@@ -561,10 +561,10 @@ -
= 0 sats · rate 0 sats/USD
+
= 0 bits
- +
@@ -677,11 +677,11 @@ - - + + - - + + diff --git a/tld.html b/tld.html index 9c2b56f..f183026 100644 --- a/tld.html +++ b/tld.html @@ -171,7 +171,7 @@ - - + + - +