feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges

Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.

Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
This commit is contained in:
Local Dev 2026-09-20 18:18:43 +02:00
parent 6618b38b2d
commit b5eea9422b
15 changed files with 146 additions and 71 deletions

View file

@ -604,9 +604,9 @@ process.exit(0);"</pre>
<script src="../js/session.js?v=20260917dash"></script> <script src="../js/session.js?v=20260917dash"></script>
<script src="../js/pin-escrow.js?v=20260909pin"></script> <script src="../js/pin-escrow.js?v=20260909pin"></script>
<script src="../js/pricing.js?v=20260917market"></script> <script src="../js/pricing.js?v=20260920bits"></script>
<script defer src="../js/site-footer.js?v=20260917market"></script> <script defer src="../js/site-footer.js?v=20260917market"></script>
<script defer src="../js/theme.js?v=20260920compact"></script> <script defer src="../js/theme.js?v=20260920compact"></script>
<script defer src="../js/profile-menu.js?v=20260920m"></script> <script defer src="../js/profile-menu.js?v=20260920bits"></script>
</body> </body>
</html> </html>

View file

@ -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.", ". 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…", "Checking the market…": "Markt wird geprüft…",
"Price (USD)": "Preis (USD)", "Price (USD)": "Preis (USD)",
"Price (sat)": "Preis (Sat)",
"List for sale →": "Zum Verkauf anbieten →", "List for sale →": "Zum Verkauf anbieten →",
"Cancel listing →": "Angebot zurückziehen →", "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.", "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", "Request declined": "Anfrage abgelehnt",
"Open dashboard →": "Dashboard öffnen →", "Open dashboard →": "Dashboard öffnen →",
"incl.": "inkl.", "incl.": "inkl.",
"sat unconfirmed · spendable now": "Sat unbestätigt · sofort verfügbar",
"Nothing is for sale right now": "Derzeit steht nichts zum Verkauf", "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?", "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 →", "Sell a name from your dashboard →": "Einen Namen im Dashboard verkaufen →",
@ -604,5 +602,7 @@
"Restyle selection": "Auswahl umgestalten", "Restyle selection": "Auswahl umgestalten",
"Nothing selected": "Nichts ausgewählt", "Nothing selected": "Nichts ausgewählt",
"Undo last change": "Letzte Änderung rückgängig", "Undo last change": "Letzte Änderung rückgängig",
"Stop": "Stopp" "Stop": "Stopp",
"unconfirmed · spendable now": "unbestätigt · sofort verfügbar",
"Price (BCH)": "Preis (BCH)"
} }

View file

@ -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.": ". Ο αγοραστής την ολοκληρώνει σε μία συναλλαγή — πληρώνεσαι στην ίδια συναλλαγή που μεταφέρει το όνομα, ή καθόλου. Χωρίς μεσεγγύηση, τίποτα προς εμπιστοσύνη.", ". 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…": "Έλεγχος της αγοράς…", "Checking the market…": "Έλεγχος της αγοράς…",
"Price (USD)": "Τιμή (USD)", "Price (USD)": "Τιμή (USD)",
"Price (sat)": "Τιμή (sat)",
"List for sale →": "Καταχώρηση προς πώληση →", "List for sale →": "Καταχώρηση προς πώληση →",
"Cancel listing →": "Ακύρωση αγγελίας →", "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.": "Η ακύρωση αφαιρεί την προσφορά από την αγορά και μετακινεί το πιστοποιητικό σου μία φορά (μια κενή ενημέρωση, λίγα λεπτά), ώστε η υπογεγραμμένη προσφορά να μην μπορεί ποτέ να ολοκληρωθεί αργότερα.", "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": "Το αίτημα απορρίφθηκε", "Request declined": "Το αίτημα απορρίφθηκε",
"Open dashboard →": "Άνοιγμα πίνακα ελέγχου →", "Open dashboard →": "Άνοιγμα πίνακα ελέγχου →",
"incl.": "συμπ.", "incl.": "συμπ.",
"sat unconfirmed · spendable now": "sat ανεπιβεβαίωτα · διαθέσιμα τώρα",
"Nothing is for sale right 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?": "Τα ονόματα βγαίνουν προς πώληση όταν οι ιδιοκτήτες τους τα καταχωρούν από τον πίνακα ελέγχου. Έχεις ένα να πουλήσεις ή προτιμάς ένα καινούργιο;", "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 →": "Πούλησε ένα όνομα από τον πίνακα ελέγχου →", "Sell a name from your dashboard →": "Πούλησε ένα όνομα από τον πίνακα ελέγχου →",
@ -604,5 +602,7 @@
"Restyle selection": "Άλλαξε στυλ στην επιλογή", "Restyle selection": "Άλλαξε στυλ στην επιλογή",
"Nothing selected": "Τίποτα επιλεγμένο", "Nothing selected": "Τίποτα επιλεγμένο",
"Undo last change": "Αναίρεση τελευταίας αλλαγής", "Undo last change": "Αναίρεση τελευταίας αλλαγής",
"Stop": "Στοπ" "Stop": "Στοπ",
"unconfirmed · spendable now": "ανεπιβεβαίωτα · διαθέσιμα τώρα",
"Price (BCH)": "Τιμή (BCH)"
} }

View file

@ -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.", ". 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…", "Checking the market…": "Consultando el mercado…",
"Price (USD)": "Precio (USD)", "Price (USD)": "Precio (USD)",
"Price (sat)": "Precio (sat)",
"List for sale →": "Poner en venta →", "List for sale →": "Poner en venta →",
"Cancel listing →": "Cancelar anuncio →", "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.", "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", "Request declined": "Solicitud rechazada",
"Open dashboard →": "Abrir panel →", "Open dashboard →": "Abrir panel →",
"incl.": "incl.", "incl.": "incl.",
"sat unconfirmed · spendable now": "sat sin confirmar · disponibles ya",
"Nothing is for sale right now": "Ahora mismo no hay nada en venta", "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?", "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 →", "Sell a name from your dashboard →": "Vender un nombre desde tu panel →",
@ -604,5 +602,7 @@
"Restyle selection": "Cambiar estilo de la selección", "Restyle selection": "Cambiar estilo de la selección",
"Nothing selected": "Nada seleccionado", "Nothing selected": "Nada seleccionado",
"Undo last change": "Deshacer el último cambio", "Undo last change": "Deshacer el último cambio",
"Stop": "Detener" "Stop": "Detener",
"unconfirmed · spendable now": "sin confirmar · disponibles ya",
"Price (BCH)": "Precio (BCH)"
} }

View file

@ -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.", ". 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é…", "Checking the market…": "Vérification du marché…",
"Price (USD)": "Prix (USD)", "Price (USD)": "Prix (USD)",
"Price (sat)": "Prix (sat)",
"List for sale →": "Mettre en vente →", "List for sale →": "Mettre en vente →",
"Cancel listing →": "Annuler l'annonce →", "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.", "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", "Request declined": "Demande refusée",
"Open dashboard →": "Ouvrir le tableau de bord →", "Open dashboard →": "Ouvrir le tableau de bord →",
"incl.": "dont", "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", "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 ?", "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 →", "Sell a name from your dashboard →": "Vendre un nom depuis le tableau de bord →",
@ -604,5 +602,7 @@
"Restyle selection": "Restyler la sélection", "Restyle selection": "Restyler la sélection",
"Nothing selected": "Rien de sélectionné", "Nothing selected": "Rien de sélectionné",
"Undo last change": "Annuler la dernière modification", "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)"
} }

View file

@ -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.", ". 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…", "Checking the market…": "A verificar o mercado…",
"Price (USD)": "Preço (USD)", "Price (USD)": "Preço (USD)",
"Price (sat)": "Preço (sat)",
"List for sale →": "Pôr à venda →", "List for sale →": "Pôr à venda →",
"Cancel listing →": "Cancelar anúncio →", "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.", "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", "Request declined": "Pedido recusado",
"Open dashboard →": "Abrir painel →", "Open dashboard →": "Abrir painel →",
"incl.": "incl.", "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", "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?", "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 →", "Sell a name from your dashboard →": "Vender um nome a partir do painel →",
@ -604,5 +602,7 @@
"Restyle selection": "Restilizar a seleção", "Restyle selection": "Restilizar a seleção",
"Nothing selected": "Nada selecionado", "Nothing selected": "Nada selecionado",
"Undo last change": "Anular a última alteração", "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)"
} }

View file

@ -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.": ". Покупатель завершает её одной транзакцией — вы получаете оплату в той же транзакции, которая передаёт имя, либо не получаете вовсе. Без эскроу, доверять никому не нужно.", ". 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…": "Проверяем маркет…", "Checking the market…": "Проверяем маркет…",
"Price (USD)": "Цена (USD)", "Price (USD)": "Цена (USD)",
"Price (sat)": "Цена (сат)",
"List for sale →": "Выставить на продажу →", "List for sale →": "Выставить на продажу →",
"Cancel listing →": "Снять с продажи →", "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.": "Отмена убирает оферту с маркета и один раз перемещает ваш сертификат (пустое обновление, несколько центов), чтобы подписанную оферту нельзя было исполнить позже.", "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": "Запрос отклонён", "Request declined": "Запрос отклонён",
"Open dashboard →": "Открыть панель →", "Open dashboard →": "Открыть панель →",
"incl.": "вкл.", "incl.": "вкл.",
"sat unconfirmed · spendable now": "сат не подтверждено · доступно сейчас",
"Nothing is for sale right 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?": "Имена появляются в продаже, когда владельцы выставляют их из панели. Есть что продать — или лучше зарегистрировать новое?", "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 →": "Продать имя из панели →", "Sell a name from your dashboard →": "Продать имя из панели →",
@ -604,5 +602,7 @@
"Restyle selection": "Изменить стиль выбранного", "Restyle selection": "Изменить стиль выбранного",
"Nothing selected": "Ничего не выбрано", "Nothing selected": "Ничего не выбрано",
"Undo last change": "Отменить последнее изменение", "Undo last change": "Отменить последнее изменение",
"Stop": "Стоп" "Stop": "Стоп",
"unconfirmed · spendable now": "не подтверждено · доступно сейчас",
"Price (BCH)": "Цена (BCH)"
} }

View file

@ -495,13 +495,13 @@ process.exit(0);"</pre>
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script> </script>
<script type="module" src="./js/register-flow.js?v=20260917bal"></script> <script type="module" src="./js/register-flow.js?v=20260920bits"></script>
<script src="./js/pricing.js?v=20260917market"></script> <script src="./js/pricing.js?v=20260920bits"></script>
<script src="./js/session.js?v=20260917dash"></script> <script src="./js/session.js?v=20260917dash"></script>
<script src="./js/pin-escrow.js?v=20260909pin"></script> <script src="./js/pin-escrow.js?v=20260909pin"></script>
<script defer src="./js/theme.js?v=20260920compact"></script> <script defer src="./js/theme.js?v=20260920compact"></script>
<script defer src="./js/site-footer.js?v=20260917market"></script> <script defer src="./js/site-footer.js?v=20260917market"></script>
<script defer src="./js/profile-menu.js?v=20260920m"></script> <script defer src="./js/profile-menu.js?v=20260920bits"></script>
</body> </body>
</html> </html>

View file

@ -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 // here, same as the register flow. Polled while the tab is visible and
// refreshed right after anything this page broadcasts. // refreshed right after anything this page broadcasts.
const setText = (id, v) => { const el = $(id); if (el) el.textContent = v; }; 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; let balEl = null, balTimer = null, balBusy = false;
async function refreshBalance() { async function refreshBalance() {
if (!wallet || balBusy) return; if (!wallet || balBusy) return;
@ -183,10 +185,10 @@ async function refreshBalance() {
if (!balEl) balEl = await BNS.connect(); if (!balEl) balEl = await BNS.connect();
const b = await BNS.getBalance(balEl, wallet.watchedAddresses); const b = await BNS.getBalance(balEl, wallet.watchedAddresses);
const pending = b.sats - b.confirmedSats; const pending = b.sats - b.confirmedSats;
setText("side-balance", fmtInt(b.sats)); setText("side-balance", fmtBits(b.sats));
setText("w-sats", `${fmtInt(b.sats)} sat${pending > 0n ? ` (${fmtInt(pending)} unconfirmed, spendable now)` : ""}`); setText("w-sats", `${fmtBits(b.sats)} · ${fmtBch(b.sats)}${pending > 0n ? ` (${fmtBits(pending)} unconfirmed, spendable now)` : ""}`);
setText("w-utxos", String(b.coins)); 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 }; 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 {} 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() } })); 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; }; const setText = (id, v) => { const el = $(id); if (el) el.textContent = v; };
setText("cnt-names", H.names.length); setText("cnt-tlds", H.tlds.length); setText("cnt-names", H.names.length); setText("cnt-tlds", H.tlds.length);
if (H.wallet?.sats != null && $("side-balance").textContent === "—") { if (H.wallet?.sats != null && $("side-balance").textContent === "—") {
const s = fmtInt(H.wallet.sats); 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));
setText("side-balance", s); setText("w-sats", `${s} sat`); 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 {} 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)); 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; let activeListing = null;
function paintSell() { function paintSell() {
const st = $("sell-status"); 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) { if (activeListing) {
st.className = "status ok"; st.textContent = "This name is listed on the market."; st.className = "status ok"; st.textContent = "This name is listed on the market.";
$("sell-form").classList.add("hidden"); $("sell-active").classList.remove("hidden"); $("sell-form").classList.add("hidden"); $("sell-active").classList.remove("hidden");
const p = BigInt(activeListing.price_sats); 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", `<code>${esc(activeListing.partial_tx.slice(0, 40))}…</code>`], ["Market page", `<a href="./market/" target="_blank" rel="noopener">open →</a>`]] $("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", `<code>${esc(activeListing.partial_tx.slice(0, 40))}…</code>`], ["Market page", `<a href="./market/" target="_blank" rel="noopener">open →</a>`]]
.map(([k, v]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join(""); .map(([k, v]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join("");
} else { } else {
st.className = "status"; st.textContent = "Not for sale. Set a price to list it."; 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 {} 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(); 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())) : ""; }); const bchStr = (sats) => (Number(sats) / 1e8).toFixed(8).replace(/0+$/, "").replace(/\.$/, "");
$("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) : ""; }); $("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 () => { $("sell-list").addEventListener("click", async () => {
if (!current.entry || !wallet) return; 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"); 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); 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 10,000 sat.", "err"); 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", ""); const push = logger("sell-log"); const btn = $("sell-list"); btn.disabled = true; btn.textContent = "Signing…"; setMsg("sell-msg", "");
let el = null; let el = null;
try { try {
@ -701,7 +703,7 @@ $("sell-list").addEventListener("click", async () => {
if (!r.ok) throw new Error(j.error || `API ${r.status}`); if (!r.ok) throw new Error(j.error || `API ${r.status}`);
push("listed on the market"); push("listed on the market");
activeListing = j.listing; paintSell(); 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)); } } 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 {} } 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 ---------- // ---------- 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 * RATE())); }
function usdToSats(usd) { const n = Number(String(usd).replace(/[^\d.]/g, "")) || 0; return Math.max(0, Math.round(n * CHIPNET_SATS_PER_USD)); }
function updateTldPrice() { function updateTldPrice() {
const sats = usdToSats($("tld-usd").value.trim() || "0"); const sats = usdToSats($("tld-usd").value.trim() || "0");
$("tld-price").innerHTML = `= <b>${sats.toLocaleString()}</b> sats · rate <b>${CHIPNET_SATS_PER_USD.toLocaleString()}</b> sats/USD (chipnet)`; $("tld-price").innerHTML = `= <b>${esc(fmtBits(sats))}</b> · 1 BCH ≈ <b>${esc(fmtUsd(Math.round(1e8 / RATE() * 100) / 100))}</b>${window.siriusPricing?.priceInfo ? " · " + esc(window.siriusPricing.priceInfo()) : ""}`;
$("tld-submit").textContent = `Register (~${sats.toLocaleString()} sats)`; $("tld-submit").textContent = `Register (~${fmtBits(sats)})`;
} }
const validateTldLabel = (l) => /^[a-z0-9-]{1,16}$/.test(l); 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; } 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"); } catch (e) { setMsg("pin-msg", e.message || String(e), "err"); }
}); });
$("set-clearpin").addEventListener("click", () => { window.siriusPin?.clear?.(); paintSettings(); setMsg("set-msg", "PIN removed.", "ok"); }); $("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 {} });

View file

@ -13,6 +13,8 @@ const API = "https://silentmode.st";
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])); const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const fmtInt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ","); 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 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) : "—"); const shortAddr = (a) => (a ? a.replace(/^[^:]+:/, "").slice(0, 8) + "…" + a.slice(-5) : "—");
@ -66,11 +68,11 @@ function render() {
return; return;
} }
status.hidden = false; 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) => ` $("list").innerHTML = rows.length ? rows.map((l) => `
<div class="row"> <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="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 class="price"><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><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 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>`; </div>`).join("") : `<div class="empty">Nothing matches that filter.</div>`;
@ -87,11 +89,11 @@ function openBuy(name) {
const price = BigInt(selected.price_sats); const price = BigInt(selected.price_sats);
const check = BNS.verifyListing(selected, { category: selected.category }); const check = BNS.verifyListing(selected, { category: selected.category });
$("buy-kv").innerHTML = [ $("buy-kv").innerHTML = [
["Price", `${fmtInt(price)} sat (≈ ${esc(usd(price))})`], ["Price", `${esc(bch(price))} (≈ ${esc(usd(price))})`],
["Seller", esc(selected.seller)], ["Seller", esc(selected.seller)],
["You receive", "the name's certificate, to your token address"], ["You receive", "the name's certificate, to your token address"],
["Offer check", check.ok ? "seller signature valid" : "INVALID: " + esc(check.error)], ["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]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join(""); ].map(([k, v]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join("");
$("buy-steps").className = "steps"; $("buy-steps").innerHTML = ""; setMsg(""); $("buy-steps").className = "steps"; $("buy-steps").innerHTML = ""; setMsg("");
const canBuy = !!wallet && check.ok && wallet.address !== selected.seller; 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); const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses);
push(`wallet has ${utxos.filter((u) => !u.token).length} spendable coin(s)`); 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 }); 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] }); 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)"); push("signed your inputs (the seller's signature is already in place)");
const txid = await BNS.broadcast(el, signed.hex); const txid = await BNS.broadcast(el, signed.hex);
@ -131,3 +133,4 @@ $("buy-go").addEventListener("click", async () => {
wallet = await restoreWallet(); wallet = await restoreWallet();
await load(); await load();
})(); })();
window.addEventListener("sirius:price", () => { if (listings.length) render(); });

View file

@ -16,9 +16,52 @@
// Chipnet placeholder rate: mainnet swaps this for a real BCH/USD oracle. // Chipnet placeholder rate: mainnet swaps this for a real BCH/USD oracle.
// The numbers keep the demo transactions in the "few chipnet coins" range // The numbers keep the demo transactions in the "few chipnet coins" range
// so a single faucet visit covers a name or a TLD. // so a single faucet visit covers a name or a TLD.
const CHIPNET_SATS_PER_USD = 250_000; // 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
const usdToSats = (usd) => BigInt(Math.max(0, Math.round(Number(usd) * CHIPNET_SATS_PER_USD))); // 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, ""); const cleanLabel = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9-]/g, "");
@ -201,9 +244,22 @@
if (n >= 1 && n === Math.floor(n)) return `$${n}`; if (n >= 1 && n === Math.floor(n)) return `$${n}`;
return `$${n.toFixed(2)}`; return `$${n.toFixed(2)}`;
} }
function formatSats(sats) { // Money is shown in BCH (prices) and bits (fees, balances); 1 bit = 100
return `${BigInt(sats || 0n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} sat`; // 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; } function round2(n) { return Math.round(Number(n) * 100) / 100; }
window.siriusPricing = { window.siriusPricing = {
@ -211,8 +267,18 @@
priceForTld, priceForTld,
formatUsd, formatUsd,
formatSats, formatSats,
formatBch,
formatBits,
bchToSats,
satsToBch,
usdToSats, 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 // TLD owner policy layer
loadTldPolicies, loadTldPolicies,
setTldPolicies, setTldPolicies,

View file

@ -66,7 +66,7 @@
// Wallet-only menu: identity, balance, receive, settings, sign out. // Wallet-only menu: identity, balance, receive, settings, sign out.
// Names/TLDs/admin live behind the Dashboard button next to this one. // Names/TLDs/admin live behind the Dashboard button next to this one.
const info = readWalletInfo(); 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"; 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 = ` menu.innerHTML = `
<div class="profile-header"> <div class="profile-header">

View file

@ -11,6 +11,8 @@ import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market"
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c])); const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[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"); const sats = (v) => Number(v).toLocaleString("en-US");
// ---------- inject CSS + modal HTML ---------- // ---------- 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 quote = await BNS.quoteRegistration(c, { name: state.name, ownerAddress: owner, records: {}, ...feeOverride });
const rows = BNS.priceSummary(quote.costs).map((l) => ` const rows = BNS.priceSummary(quote.costs).map((l) => `
<tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td> <tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td>
<td>${sats(l.sats)} sat</td></tr>`).join(""); <td>${bits(l.sats)}</td></tr>`).join("");
render(` render(`
<h3>Confirm ${esc(quote.displayName)}</h3> <h3>Confirm ${esc(quote.displayName)}</h3>
@ -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 {} 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; const enough = b.sats >= 20000n;
$("bal").innerHTML = b.sats > 0n $("bal").innerHTML = b.sats > 0n
? `<b class="ok">${sats(b.sats)} sat</b> available (${b.coins} coin${b.coins===1?"":"s"})` ? `<b class="ok">${bits(b.sats)}</b> available (${b.coins} coin${b.coins===1?"":"s"})`
+ (enough ? "" : ` — a little more is needed to cover the registration.`) + (enough ? "" : ` — a little more is needed to cover the registration.`)
: `Waiting for coins… this page is watching the chain live.`; : `Waiting for coins… this page is watching the chain live.`;
$("next").disabled = !enough; $("next").disabled = !enough;
@ -933,7 +935,7 @@ async function stepConfirm() {
const rows = BNS.priceSummary(quote.costs).map((l) => ` const rows = BNS.priceSummary(quote.costs).map((l) => `
<tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td> <tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td>
<td>${sats(l.sats)} sat</td></tr>`).join(""); <td>${bits(l.sats)}</td></tr>`).join("");
render(` render(`
<h3>Confirm ${esc(quote.displayName)}</h3> <h3>Confirm ${esc(quote.displayName)}</h3>
@ -967,9 +969,9 @@ async function stepConfirmTld() {
the registry operator for every second-level name under it.</p> the registry operator for every second-level name under it.</p>
<table class="price"> <table class="price">
<tr><td>Service fee${feeSats === 0n ? " <small>(none on chipnet)</small>" : ""}</td> <tr><td>Service fee${feeSats === 0n ? " <small>(none on chipnet)</small>" : ""}</td>
<td>${sats(feeSats)} sat</td></tr> <td>${bits(feeSats)}</td></tr>
<tr><td>Beacon dust + chain fee <small>(covered by wallet)</small></td> <tr><td>Beacon dust + chain fee <small>(covered by wallet)</small></td>
<td>~1,300 sat</td></tr> <td>~13 bits</td></tr>
</table> </table>
<div class="note" style="margin-top:16px">Minted to <span class="mono">${esc(owner)}</span>. <div class="note" style="margin-top:16px">Minted to <span class="mono">${esc(owner)}</span>.
That wallet becomes the operator for this TLD.</div> That wallet becomes the operator for this TLD.</div>
@ -990,7 +992,7 @@ async function stepConfirmTld() {
// opts: { feeSats, kind: "name"|"tld", back, next } // opts: { feeSats, kind: "name"|"tld", back, next }
function stepApprove(opts) { function stepApprove(opts) {
const { feeSats, kind, back, next } = opts; const { feeSats, kind, back, next } = opts;
const feeLine = feeSats && feeSats > 0n ? `<b class="ok">${sats(feeSats)} sat</b>` : `<b class="ok">Service fee waived on chipnet</b>`; const feeLine = feeSats && feeSats > 0n ? `<b class="ok">${bits(feeSats)}</b>` : `<b class="ok">Service fee waived on chipnet</b>`;
const target = kind === "tld" ? `.<b>${esc(state.name)}</b>` : `<b>${esc(state.name)}</b>`; const target = kind === "tld" ? `.<b>${esc(state.name)}</b>` : `<b>${esc(state.name)}</b>`;
// Signed in with a live wallet and no "ask for PIN before payments" // Signed in with a live wallet and no "ask for PIN before payments"

View file

@ -298,8 +298,8 @@
<div class="who"> <div class="who">
<div class="lbl">Signed in</div> <div class="lbl">Signed in</div>
<div class="addr" id="side-addr"></div> <div class="addr" id="side-addr"></div>
<div class="net">chipnet · <span id="side-balance"></span> sat <button class="refresh" id="side-refresh" title="Refresh balance" aria-label="Refresh balance"></button></div> <div class="net">chipnet · <span id="side-balance"></span> <button class="refresh" id="side-refresh" title="Refresh balance" aria-label="Refresh balance"></button></div>
<div class="net pending" id="side-pending" hidden>incl. <span id="side-pending-sats">0</span> sat unconfirmed · spendable now</div> <div class="net pending" id="side-pending" hidden>incl. <span id="side-pending-sats">0</span> unconfirmed · spendable now</div>
</div> </div>
<button class="item" data-go="studio"><span class="ic">🎨</span>Web-Builder</button> <button class="item" data-go="studio"><span class="ic">🎨</span>Web-Builder</button>
<button class="item active" data-go="domains"><span class="ic">🌐</span>Domain names<span class="cnt" id="cnt-names">0</span></button> <button class="item active" data-go="domains"><span class="ic">🌐</span>Domain names<span class="cnt" id="cnt-names">0</span></button>
@ -364,7 +364,7 @@
<div id="sell-form"> <div id="sell-form">
<div class="row" style="align-items:flex-end"> <div class="row" style="align-items:flex-end">
<div><label style="margin-top:0">Price (USD)</label><input type="text" id="sell-usd" placeholder="25" inputmode="decimal" style="max-width:160px"></div> <div><label style="margin-top:0">Price (USD)</label><input type="text" id="sell-usd" placeholder="25" inputmode="decimal" style="max-width:160px"></div>
<div><label style="margin-top:0">Price (sat)</label><input type="text" id="sell-sats" placeholder="6250000" inputmode="numeric" style="max-width:200px"></div> <div><label style="margin-top:0">Price (BCH)</label><input type="text" id="sell-bch" placeholder="0.0625" inputmode="decimal" style="max-width:200px"></div>
<span class="dim" id="sell-rate"></span> <span class="dim" id="sell-rate"></span>
</div> </div>
<div class="row end"><button class="btn acid" id="sell-list">List for sale →</button></div> <div class="row end"><button class="btn acid" id="sell-list">List for sale →</button></div>
@ -561,10 +561,10 @@
<input type="text" id="tld-label" placeholder="myregistry" autocomplete="off" spellcheck="false" maxlength="16"> <input type="text" id="tld-label" placeholder="myregistry" autocomplete="off" spellcheck="false" maxlength="16">
<label style="margin-top:12px">Service fee (USD) <span class="dim" style="margin-left:6px">chipnet test rate</span></label> <label style="margin-top:12px">Service fee (USD) <span class="dim" style="margin-left:6px">chipnet test rate</span></label>
<input type="text" id="tld-usd" value="5"> <input type="text" id="tld-usd" value="5">
<div class="status" id="tld-price">= <b>0</b> sats · rate <b>0</b> sats/USD</div> <div class="status" id="tld-price">= <b>0 bits</b></div>
<div class="row"> <div class="row">
<button class="btn ghost small" id="tld-check" disabled>Check availability</button> <button class="btn ghost small" id="tld-check" disabled>Check availability</button>
<button class="btn acid small" id="tld-submit" disabled>Register (~0 sats)</button> <button class="btn acid small" id="tld-submit" disabled>Register (~0 bits)</button>
</div> </div>
<div id="tld-log" class="status" style="display:none;font-family:var(--mono);font-size:12.5px;margin-top:12px"></div> <div id="tld-log" class="status" style="display:none;font-family:var(--mono);font-size:12.5px;margin-top:12px"></div>
<div id="tld-msg" style="margin-top:10px;font-size:13.5px"></div> <div id="tld-msg" style="margin-top:10px;font-size:13.5px"></div>
@ -677,11 +677,11 @@
</script> </script>
<script src="./js/session.js?v=20260917dash"></script> <script src="./js/session.js?v=20260917dash"></script>
<script src="./js/pin-escrow.js?v=20260909pin"></script> <script src="./js/pin-escrow.js?v=20260909pin"></script>
<script src="./js/pricing.js?v=20260917market"></script> <script src="./js/pricing.js?v=20260920bits"></script>
<script type="module" src="./js/dashboard.js?v=20260917bal"></script> <script type="module" src="./js/dashboard.js?v=20260920bits"></script>
<script defer src="./js/theme.js?v=20260920compact"></script> <script defer src="./js/theme.js?v=20260920compact"></script>
<script defer src="./js/site-footer.js?v=20260917market"></script> <script defer src="./js/site-footer.js?v=20260917market"></script>
<script type="module" src="./js/register-flow.js?v=20260917bal"></script> <script type="module" src="./js/register-flow.js?v=20260920bits"></script>
<script defer src="./js/profile-menu.js?v=20260920m"></script> <script defer src="./js/profile-menu.js?v=20260920bits"></script>
</body> </body>
</html> </html>

View file

@ -171,7 +171,7 @@
<script> <script>
// Simple oracle stub: static chipnet rate for testing. Same number the // Simple oracle stub: static chipnet rate for testing. Same number the
// portal uses. Mainnet swaps for a real price feed. // portal uses. Mainnet swaps for a real price feed.
const CHIPNET_SATS_PER_USD = 250_000; const CHIPNET_SATS_PER_USD = 400_000; // first-paint fallback only; pricing.js serves the live rate
const DEFAULT_USD = 5; const DEFAULT_USD = 5;
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
@ -273,7 +273,7 @@
// landing / tld / mint agree. Fall back to the flat DEFAULT_USD if the // landing / tld / mint agree. Fall back to the flat DEFAULT_USD if the
// pricing module hasn't loaded for some reason. // pricing module hasn't loaded for some reason.
const p = window.siriusPricing?.priceForTld(label); const p = window.siriusPricing?.priceForTld(label);
const fee = p ? p.sats : BigInt(DEFAULT_USD * CHIPNET_SATS_PER_USD); const fee = p ? p.sats : BigInt(DEFAULT_USD * (window.siriusPricing?.SATS_PER_USD || CHIPNET_SATS_PER_USD));
const call = () => window.siriusRegisterTld(label, { serviceFeeSats: fee }); const call = () => window.siriusRegisterTld(label, { serviceFeeSats: fee });
if (typeof window.siriusRegisterTld === "function") { call(); return; } if (typeof window.siriusRegisterTld === "function") { call(); return; }
// Module still loading — retry until it lands. // Module still loading — retry until it lands.
@ -303,12 +303,12 @@
})(); })();
</script> </script>
<script type="module" src="./js/register-flow.js?v=20260917bal"></script> <script type="module" src="./js/register-flow.js?v=20260920bits"></script>
<script src="./js/pricing.js?v=20260917market"></script> <script src="./js/pricing.js?v=20260920bits"></script>
<script src="./js/session.js?v=20260917dash"></script> <script src="./js/session.js?v=20260917dash"></script>
<script src="./js/pin-escrow.js?v=20260909pin"></script> <script src="./js/pin-escrow.js?v=20260909pin"></script>
<script defer src="./js/theme.js?v=20260920compact"></script> <script defer src="./js/theme.js?v=20260920compact"></script>
<script defer src="./js/site-footer.js?v=20260917market"></script> <script defer src="./js/site-footer.js?v=20260917market"></script>
<script defer src="./js/profile-menu.js?v=20260920m"></script> <script defer src="./js/profile-menu.js?v=20260920bits"></script>
</body> </body>
</html> </html>