diff --git a/docs/index.html b/docs/index.html index 39676b8..84882fb 100644 --- a/docs/index.html +++ b/docs/index.html @@ -92,7 +92,7 @@ - + @@ -101,6 +101,7 @@ Try it ✏️ Register 🌐 TLD + 🛒 Market Theseus 🔑 Sign in @@ -525,8 +526,8 @@ process.exit(0);" - - + + diff --git a/i18n/de.json b/i18n/de.json index c35c1c6..9f9644e 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -544,5 +544,44 @@ "Wallet settings": "Wallet-Einstellungen", "session, PIN, forget device": "Sitzung, PIN, Gerät vergessen", "Holdings": "Bestände", - "open the dashboard to load": "Dashboard öffnen zum Laden" + "open the dashboard to load": "Dashboard öffnen zum Laden", + "🛒 Market": "🛒 Markt", + "🛒 Names for sale": "🛒 Namen zum Verkauf", + "Market — names for sale on Sirius.X": "Markt — Namen zum Verkauf auf Sirius.X", + "for sale": "zum Verkauf", + "Every sale is a single Bitcoin Cash transaction. The seller signed an offer that pays them only when the certificate moves to you — you add your coins, sign, broadcast. No escrow, no middleman, no way for either side to cheat.": "Jeder Verkauf ist eine einzige Bitcoin-Cash-Transaktion. Der Verkäufer hat ein Angebot signiert, das ihn nur bezahlt, wenn das Zertifikat zu dir wechselt — du fügst deine Coins hinzu, signierst, sendest. Kein Treuhänder, kein Mittelsmann, keine Möglichkeit zu betrügen.", + "Sort: price": "Sortieren: Preis", + "Loading listings…": "Angebote werden geladen…", + "Selling a name?": "Einen Namen verkaufen?", + "Open it in your": "Öffne ihn in deinem", + ", choose": ", wähle", + "Sell": "Verkaufen", + ", set a price. The listing is a signed offer stored by the gateway; cancelling it moves your certificate once so the old offer can never be used.": ", lege einen Preis fest. Das Angebot ist eine signierte Offerte, die das Gateway speichert; beim Zurückziehen wird dein Zertifikat einmal bewegt, damit die alte Offerte nie mehr verwendet werden kann.", + "One transaction: your coins pay the seller, the certificate lands in your wallet, and the registry is told about the new owner. Records stay as they are — you can change them in the dashboard right after.": "Eine Transaktion: deine Coins bezahlen den Verkäufer, das Zertifikat landet in deiner Wallet, und das Register erfährt vom neuen Besitzer. Die Einträge bleiben wie sie sind — du kannst sie direkt danach im Dashboard ändern.", + "Sign in to buy →": "Zum Kauf anmelden →", + "Buy — sign & broadcast": "Kaufen — signieren & senden", + "Buy →": "Kaufen →", + "Nothing is for sale right now. Owners list names from their dashboard.": "Derzeit steht nichts zum Verkauf. Besitzer bieten Namen über ihr Dashboard an.", + "Sell this name": "Diesen Namen verkaufen", + "You sign an offer once: \"whoever pays me this price gets the certificate\". The offer is stored by the gateway and shown on the": "Du signierst einmal ein Angebot: „wer mir diesen Preis zahlt, bekommt das Zertifikat“. Das Angebot speichert das Gateway und zeigt es auf dem", + "market": "Markt", + ". 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.", + "Not for sale. Set a price to list it.": "Nicht zum Verkauf. Lege einen Preis fest, um ihn anzubieten.", + "This name is listed on the market.": "Dieser Name ist auf dem Markt gelistet.", + "Pending approvals": "Ausstehende Freigaben", + "Registrations under this TLD that wait for your co-signature. Approving signs your TLD certificate into the buyer's transaction and broadcasts it; the certificate returns to you in the same transaction. Nothing can be registered under a co-sign or private TLD without this step.": "Registrierungen unter dieser TLD, die auf deine Gegensignatur warten. Beim Freigeben wird dein TLD-Zertifikat in die Transaktion des Käufers signiert und gesendet; das Zertifikat kommt in derselben Transaktion zu dir zurück. Ohne diesen Schritt kann unter einer Co-Sign- oder privaten TLD nichts registriert werden.", + "No requests waiting.": "Keine Anfragen offen.", + "awaiting you": "wartet auf dich", + "Approve & broadcast": "Freigeben & senden", + "Decline": "Ablehnen", + "Request approval — sign": "Freigabe anfragen — signieren", + "Withdraw request": "Anfrage zurückziehen", + "Request declined": "Anfrage abgelehnt", + "Open dashboard →": "Dashboard öffnen →" } diff --git a/i18n/el.json b/i18n/el.json index dc8f181..18dc692 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -459,7 +459,7 @@ "No names yet.": "Δεν υπάρχουν ακόμα ονόματα.", "Register your first one →": "Καταχώρησε το πρώτο σου →", "Everything points somewhere and has DNS. Nice.": "Όλα δείχνουν κάπου και έχουν DNS. Ωραία.", - "Nothing matches that filter.": "Τίποτα δεν ταιριάζει σε αυτό το φίλτρο.", + "Nothing matches that filter.": "Τίποτα δεν ταιριάζει με αυτό το φίλτρο.", "Status": "Κατάσταση", "point it somewhere →": "δείξε κάπου →", "Add some →": "Πρόσθεσε →", @@ -544,5 +544,44 @@ "Wallet settings": "Ρυθμίσεις πορτοφολιού", "session, PIN, forget device": "συνεδρία, PIN, λήθη συσκευής", "Holdings": "Κατοχές", - "open the dashboard to load": "άνοιξε τον πίνακα για φόρτωση" + "open the dashboard to load": "άνοιξε τον πίνακα για φόρτωση", + "🛒 Market": "🛒 Αγορά", + "🛒 Names for sale": "🛒 Ονόματα προς πώληση", + "Market — names for sale on Sirius.X": "Αγορά — ονόματα προς πώληση στο Sirius.X", + "for sale": "προς πώληση", + "Every sale is a single Bitcoin Cash transaction. The seller signed an offer that pays them only when the certificate moves to you — you add your coins, sign, broadcast. No escrow, no middleman, no way for either side to cheat.": "Κάθε πώληση είναι μία μόνο συναλλαγή Bitcoin Cash. Ο πωλητής υπέγραψε μια προσφορά που τον πληρώνει μόνο όταν το πιστοποιητικό περάσει σε εσένα — προσθέτεις τα κέρματά σου, υπογράφεις, μεταδίδεις. Χωρίς μεσεγγύηση, χωρίς μεσάζοντα, χωρίς τρόπο να εξαπατήσει κανείς.", + "Sort: price": "Ταξινόμηση: τιμή", + "Loading listings…": "Φόρτωση αγγελιών…", + "Selling a name?": "Πουλάς ένα όνομα;", + "Open it in your": "Άνοιξέ το στον", + ", choose": ", επίλεξε", + "Sell": "Πώληση", + ", set a price. The listing is a signed offer stored by the gateway; cancelling it moves your certificate once so the old offer can never be used.": ", όρισε τιμή. Η αγγελία είναι μια υπογεγραμμένη προσφορά που αποθηκεύει η πύλη· η ακύρωση μετακινεί το πιστοποιητικό σου μία φορά ώστε η παλιά προσφορά να μην μπορεί ποτέ να χρησιμοποιηθεί.", + "One transaction: your coins pay the seller, the certificate lands in your wallet, and the registry is told about the new owner. Records stay as they are — you can change them in the dashboard right after.": "Μία συναλλαγή: τα κέρματά σου πληρώνουν τον πωλητή, το πιστοποιητικό φτάνει στο πορτοφόλι σου και το μητρώο ενημερώνεται για τον νέο ιδιοκτήτη. Οι εγγραφές μένουν ως έχουν — μπορείς να τις αλλάξεις αμέσως μετά στον πίνακα ελέγχου.", + "Sign in to buy →": "Σύνδεση για αγορά →", + "Buy — sign & broadcast": "Αγορά — υπογραφή & μετάδοση", + "Buy →": "Αγορά →", + "Nothing is for sale right now. Owners list names from their dashboard.": "Τίποτα δεν πωλείται αυτή τη στιγμή. Οι ιδιοκτήτες καταχωρούν ονόματα από τον πίνακα ελέγχου τους.", + "Sell this name": "Πώληση αυτού του ονόματος", + "You sign an offer once: \"whoever pays me this price gets the certificate\". The offer is stored by the gateway and shown on the": "Υπογράφεις μία προσφορά μία φορά: «όποιος μου πληρώσει αυτή την τιμή παίρνει το πιστοποιητικό». Η προσφορά αποθηκεύεται από την πύλη και εμφανίζεται στην", + "market": "αγορά", + ". 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.": "Η ακύρωση αφαιρεί την προσφορά από την αγορά και μετακινεί το πιστοποιητικό σου μία φορά (μια κενή ενημέρωση, λίγα λεπτά), ώστε η υπογεγραμμένη προσφορά να μην μπορεί ποτέ να ολοκληρωθεί αργότερα.", + "Not for sale. Set a price to list it.": "Δεν πωλείται. Όρισε τιμή για να το καταχωρήσεις.", + "This name is listed on the market.": "Αυτό το όνομα είναι καταχωρημένο στην αγορά.", + "Pending approvals": "Εκκρεμείς εγκρίσεις", + "Registrations under this TLD that wait for your co-signature. Approving signs your TLD certificate into the buyer's transaction and broadcasts it; the certificate returns to you in the same transaction. Nothing can be registered under a co-sign or private TLD without this step.": "Καταχωρήσεις κάτω από αυτό το TLD που περιμένουν τη συνυπογραφή σου. Η έγκριση υπογράφει το πιστοποιητικό TLD σου μέσα στη συναλλαγή του αγοραστή και τη μεταδίδει· το πιστοποιητικό επιστρέφει σε εσένα στην ίδια συναλλαγή. Τίποτα δεν μπορεί να καταχωρηθεί κάτω από ένα TLD συνυπογραφής ή ιδιωτικό χωρίς αυτό το βήμα.", + "No requests waiting.": "Δεν εκκρεμούν αιτήματα.", + "awaiting you": "περιμένει εσένα", + "Approve & broadcast": "Έγκριση & μετάδοση", + "Decline": "Απόρριψη", + "Request approval — sign": "Αίτημα έγκρισης — υπογραφή", + "Withdraw request": "Απόσυρση αιτήματος", + "Request declined": "Το αίτημα απορρίφθηκε", + "Open dashboard →": "Άνοιγμα πίνακα ελέγχου →" } diff --git a/i18n/es.json b/i18n/es.json index 040fe9b..7a28777 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -544,5 +544,44 @@ "Wallet settings": "Ajustes de la cartera", "session, PIN, forget device": "sesión, PIN, olvidar dispositivo", "Holdings": "Propiedades", - "open the dashboard to load": "abre el panel para cargar" + "open the dashboard to load": "abre el panel para cargar", + "🛒 Market": "🛒 Mercado", + "🛒 Names for sale": "🛒 Nombres en venta", + "Market — names for sale on Sirius.X": "Mercado — nombres en venta en Sirius.X", + "for sale": "en venta", + "Every sale is a single Bitcoin Cash transaction. The seller signed an offer that pays them only when the certificate moves to you — you add your coins, sign, broadcast. No escrow, no middleman, no way for either side to cheat.": "Cada venta es una única transacción de Bitcoin Cash. El vendedor firmó una oferta que solo le paga cuando el certificado pasa a ti — añades tus monedas, firmas, transmites. Sin custodia, sin intermediarios, sin forma de que nadie haga trampa.", + "Sort: price": "Ordenar: precio", + "Loading listings…": "Cargando anuncios…", + "Selling a name?": "¿Vendes un nombre?", + "Open it in your": "Ábrelo en tu", + ", choose": ", elige", + "Sell": "Vender", + ", set a price. The listing is a signed offer stored by the gateway; cancelling it moves your certificate once so the old offer can never be used.": ", fija un precio. El anuncio es una oferta firmada que guarda la pasarela; cancelarlo mueve tu certificado una vez para que la oferta antigua nunca pueda usarse.", + "One transaction: your coins pay the seller, the certificate lands in your wallet, and the registry is told about the new owner. Records stay as they are — you can change them in the dashboard right after.": "Una transacción: tus monedas pagan al vendedor, el certificado llega a tu billetera y el registro conoce al nuevo dueño. Los registros quedan como están — puedes cambiarlos en el panel justo después.", + "Sign in to buy →": "Inicia sesión para comprar →", + "Buy — sign & broadcast": "Comprar — firmar y transmitir", + "Buy →": "Comprar →", + "Nothing is for sale right now. Owners list names from their dashboard.": "Ahora mismo no hay nada en venta. Los dueños publican nombres desde su panel.", + "Sell this name": "Vender este nombre", + "You sign an offer once: \"whoever pays me this price gets the certificate\". The offer is stored by the gateway and shown on the": "Firmas una oferta una vez: «quien me pague este precio se queda con el certificado». La pasarela guarda la oferta y la muestra en el", + "market": "mercado", + ". 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.", + "Not for sale. Set a price to list it.": "No está en venta. Fija un precio para publicarlo.", + "This name is listed on the market.": "Este nombre está publicado en el mercado.", + "Pending approvals": "Aprobaciones pendientes", + "Registrations under this TLD that wait for your co-signature. Approving signs your TLD certificate into the buyer's transaction and broadcasts it; the certificate returns to you in the same transaction. Nothing can be registered under a co-sign or private TLD without this step.": "Registros bajo este TLD que esperan tu cofirma. Aprobar firma tu certificado de TLD dentro de la transacción del comprador y la transmite; el certificado vuelve a ti en la misma transacción. Nada puede registrarse bajo un TLD de cofirma o privado sin este paso.", + "No requests waiting.": "No hay solicitudes en espera.", + "awaiting you": "te espera", + "Approve & broadcast": "Aprobar y transmitir", + "Decline": "Rechazar", + "Request approval — sign": "Solicitar aprobación — firmar", + "Withdraw request": "Retirar solicitud", + "Request declined": "Solicitud rechazada", + "Open dashboard →": "Abrir panel →" } diff --git a/i18n/fr.json b/i18n/fr.json index d5b508b..08b342e 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -544,5 +544,44 @@ "Wallet settings": "Réglages du portefeuille", "session, PIN, forget device": "session, PIN, oublier l'appareil", "Holdings": "Avoirs", - "open the dashboard to load": "ouvrez le tableau de bord pour charger" + "open the dashboard to load": "ouvrez le tableau de bord pour charger", + "🛒 Market": "🛒 Marché", + "🛒 Names for sale": "🛒 Noms à vendre", + "Market — names for sale on Sirius.X": "Marché — noms à vendre sur Sirius.X", + "for sale": "à vendre", + "Every sale is a single Bitcoin Cash transaction. The seller signed an offer that pays them only when the certificate moves to you — you add your coins, sign, broadcast. No escrow, no middleman, no way for either side to cheat.": "Chaque vente est une seule transaction Bitcoin Cash. Le vendeur a signé une offre qui ne le paie que lorsque le certificat vous est transféré — vous ajoutez vos pièces, signez, diffusez. Pas de séquestre, pas d'intermédiaire, aucun moyen de tricher.", + "Sort: price": "Trier : prix", + "Loading listings…": "Chargement des annonces…", + "Selling a name?": "Vous vendez un nom ?", + "Open it in your": "Ouvrez-le dans votre", + ", choose": ", choisissez", + "Sell": "Vendre", + ", set a price. The listing is a signed offer stored by the gateway; cancelling it moves your certificate once so the old offer can never be used.": ", fixez un prix. L'annonce est une offre signée conservée par la passerelle ; l'annuler déplace votre certificat une fois afin que l'ancienne offre ne puisse plus jamais servir.", + "One transaction: your coins pay the seller, the certificate lands in your wallet, and the registry is told about the new owner. Records stay as they are — you can change them in the dashboard right after.": "Une transaction : vos pièces paient le vendeur, le certificat arrive dans votre portefeuille et le registre apprend le nouveau propriétaire. Les enregistrements restent tels quels — vous pouvez les modifier dans le tableau de bord juste après.", + "Sign in to buy →": "Se connecter pour acheter →", + "Buy — sign & broadcast": "Acheter — signer et diffuser", + "Buy →": "Acheter →", + "Nothing is for sale right now. Owners list names from their dashboard.": "Rien n'est à vendre pour l'instant. Les propriétaires mettent leurs noms en vente depuis leur tableau de bord.", + "Sell this name": "Vendre ce nom", + "You sign an offer once: \"whoever pays me this price gets the certificate\". The offer is stored by the gateway and shown on the": "Vous signez une offre une seule fois : « quiconque me paie ce prix obtient le certificat ». La passerelle conserve l'offre et l'affiche sur le", + "market": "marché", + ". 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.", + "Not for sale. Set a price to list it.": "Pas à vendre. Fixez un prix pour le mettre en vente.", + "This name is listed on the market.": "Ce nom est en vente sur le marché.", + "Pending approvals": "Approbations en attente", + "Registrations under this TLD that wait for your co-signature. Approving signs your TLD certificate into the buyer's transaction and broadcasts it; the certificate returns to you in the same transaction. Nothing can be registered under a co-sign or private TLD without this step.": "Enregistrements sous ce TLD qui attendent votre cosignature. Approuver signe votre certificat de TLD dans la transaction de l'acheteur et la diffuse ; le certificat vous revient dans la même transaction. Rien ne peut être enregistré sous un TLD à cosignature ou privé sans cette étape.", + "No requests waiting.": "Aucune demande en attente.", + "awaiting you": "vous attend", + "Approve & broadcast": "Approuver et diffuser", + "Decline": "Refuser", + "Request approval — sign": "Demander l'approbation — signer", + "Withdraw request": "Retirer la demande", + "Request declined": "Demande refusée", + "Open dashboard →": "Ouvrir le tableau de bord →" } diff --git a/i18n/pt.json b/i18n/pt.json index 0c5ab8e..e596fda 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -544,5 +544,44 @@ "Wallet settings": "Definições da carteira", "session, PIN, forget device": "sessão, PIN, esquecer dispositivo", "Holdings": "Posses", - "open the dashboard to load": "abra o painel para carregar" + "open the dashboard to load": "abra o painel para carregar", + "🛒 Market": "🛒 Mercado", + "🛒 Names for sale": "🛒 Nomes à venda", + "Market — names for sale on Sirius.X": "Mercado — nomes à venda no Sirius.X", + "for sale": "à venda", + "Every sale is a single Bitcoin Cash transaction. The seller signed an offer that pays them only when the certificate moves to you — you add your coins, sign, broadcast. No escrow, no middleman, no way for either side to cheat.": "Cada venda é uma única transação Bitcoin Cash. O vendedor assinou uma oferta que só lhe paga quando o certificado passa para si — adiciona as suas moedas, assina, transmite. Sem custódia, sem intermediários, sem forma de alguém fazer batota.", + "Sort: price": "Ordenar: preço", + "Loading listings…": "A carregar anúncios…", + "Selling a name?": "Vai vender um nome?", + "Open it in your": "Abra-o no seu", + ", choose": ", escolha", + "Sell": "Vender", + ", set a price. The listing is a signed offer stored by the gateway; cancelling it moves your certificate once so the old offer can never be used.": ", defina um preço. O anúncio é uma oferta assinada guardada pelo gateway; cancelá-lo move o seu certificado uma vez para que a oferta antiga nunca possa ser usada.", + "One transaction: your coins pay the seller, the certificate lands in your wallet, and the registry is told about the new owner. Records stay as they are — you can change them in the dashboard right after.": "Uma transação: as suas moedas pagam ao vendedor, o certificado chega à sua carteira e o registo fica a saber do novo dono. Os registos ficam como estão — pode alterá-los no painel logo a seguir.", + "Sign in to buy →": "Iniciar sessão para comprar →", + "Buy — sign & broadcast": "Comprar — assinar e transmitir", + "Buy →": "Comprar →", + "Nothing is for sale right now. Owners list names from their dashboard.": "De momento não há nada à venda. Os donos anunciam nomes a partir do seu painel.", + "Sell this name": "Vender este nome", + "You sign an offer once: \"whoever pays me this price gets the certificate\". The offer is stored by the gateway and shown on the": "Assina uma oferta uma vez: «quem me pagar este preço fica com o certificado». O gateway guarda a oferta e mostra-a no", + "market": "mercado", + ". 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.", + "Not for sale. Set a price to list it.": "Não está à venda. Defina um preço para o anunciar.", + "This name is listed on the market.": "Este nome está anunciado no mercado.", + "Pending approvals": "Aprovações pendentes", + "Registrations under this TLD that wait for your co-signature. Approving signs your TLD certificate into the buyer's transaction and broadcasts it; the certificate returns to you in the same transaction. Nothing can be registered under a co-sign or private TLD without this step.": "Registos sob este TLD que aguardam a sua coassinatura. Aprovar assina o seu certificado de TLD na transação do comprador e transmite-a; o certificado volta para si na mesma transação. Nada pode ser registado sob um TLD de coassinatura ou privado sem este passo.", + "No requests waiting.": "Sem pedidos em espera.", + "awaiting you": "à sua espera", + "Approve & broadcast": "Aprovar e transmitir", + "Decline": "Recusar", + "Request approval — sign": "Pedir aprovação — assinar", + "Withdraw request": "Retirar pedido", + "Request declined": "Pedido recusado", + "Open dashboard →": "Abrir painel →" } diff --git a/i18n/ru.json b/i18n/ru.json index c5da7b1..cc91a67 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -544,5 +544,44 @@ "Wallet settings": "Настройки кошелька", "session, PIN, forget device": "сессия, PIN, забыть устройство", "Holdings": "Владения", - "open the dashboard to load": "откройте панель для загрузки" + "open the dashboard to load": "откройте панель для загрузки", + "🛒 Market": "🛒 Маркет", + "🛒 Names for sale": "🛒 Имена на продажу", + "Market — names for sale on Sirius.X": "Маркет — имена на продажу на Sirius.X", + "for sale": "на продажу", + "Every sale is a single Bitcoin Cash transaction. The seller signed an offer that pays them only when the certificate moves to you — you add your coins, sign, broadcast. No escrow, no middleman, no way for either side to cheat.": "Каждая продажа — одна транзакция Bitcoin Cash. Продавец подписал оферту, по которой он получает деньги только когда сертификат переходит к вам — вы добавляете свои монеты, подписываете, отправляете. Без эскроу, без посредников, без возможности обмана с любой стороны.", + "Sort: price": "Сортировка: цена", + "Loading listings…": "Загрузка объявлений…", + "Selling a name?": "Продаёте имя?", + "Open it in your": "Откройте его в своей", + ", choose": ", выберите", + "Sell": "Продать", + ", set a price. The listing is a signed offer stored by the gateway; cancelling it moves your certificate once so the old offer can never be used.": ", укажите цену. Объявление — это подписанная оферта, которую хранит шлюз; при отмене ваш сертификат перемещается один раз, чтобы старую оферту нельзя было использовать.", + "One transaction: your coins pay the seller, the certificate lands in your wallet, and the registry is told about the new owner. Records stay as they are — you can change them in the dashboard right after.": "Одна транзакция: ваши монеты оплачивают продавца, сертификат попадает в ваш кошелёк, а реестр узнаёт о новом владельце. Записи остаются прежними — вы можете изменить их в панели сразу после.", + "Sign in to buy →": "Войти, чтобы купить →", + "Buy — sign & broadcast": "Купить — подписать и отправить", + "Buy →": "Купить →", + "Nothing is for sale right now. Owners list names from their dashboard.": "Сейчас ничего не продаётся. Владельцы выставляют имена из своей панели.", + "Sell this name": "Продать это имя", + "You sign an offer once: \"whoever pays me this price gets the certificate\". The offer is stored by the gateway and shown on the": "Вы один раз подписываете оферту: «кто заплатит мне эту цену, получит сертификат». Шлюз хранит оферту и показывает её на", + "market": "маркете", + ". 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.": "Отмена убирает оферту с маркета и один раз перемещает ваш сертификат (пустое обновление, несколько центов), чтобы подписанную оферту нельзя было исполнить позже.", + "Not for sale. Set a price to list it.": "Не продаётся. Укажите цену, чтобы выставить.", + "This name is listed on the market.": "Это имя выставлено на маркете.", + "Pending approvals": "Ожидают одобрения", + "Registrations under this TLD that wait for your co-signature. Approving signs your TLD certificate into the buyer's transaction and broadcasts it; the certificate returns to you in the same transaction. Nothing can be registered under a co-sign or private TLD without this step.": "Регистрации под этим TLD, ожидающие вашей подписи. Одобрение подписывает ваш сертификат TLD в транзакции покупателя и отправляет её; сертификат возвращается к вам в той же транзакции. Без этого шага под TLD с со-подписью или приватным TLD ничего зарегистрировать нельзя.", + "No requests waiting.": "Запросов нет.", + "awaiting you": "ждёт вас", + "Approve & broadcast": "Одобрить и отправить", + "Decline": "Отклонить", + "Request approval — sign": "Запросить одобрение — подписать", + "Withdraw request": "Отозвать запрос", + "Request declined": "Запрос отклонён", + "Open dashboard →": "Открыть панель →" } diff --git a/index.html b/index.html index 07d2c68..424937d 100644 --- a/index.html +++ b/index.html @@ -130,7 +130,7 @@ .verify dd{margin:2px 0 0;font-family:ui-monospace,monospace;font-size:12.5px;word-break:break-all;color:var(--ink)} footer{border-top:1px solid var(--line);padding:2.4rem 0 3.5rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem} - + @@ -139,6 +139,7 @@ Try it ✏️ Register 🌐 TLD + 🛒 Market Theseus 🔑 Sign in @@ -507,13 +508,13 @@ process.exit(0);" - + - + - + diff --git a/js/dashboard.js b/js/dashboard.js index aa4b8ad..b0129dd 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -10,7 +10,7 @@ // is the persistent device session (js/session.js): sign in once, come back // without a prompt until you sign out. -import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917sig"; +import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market"; const API = "https://silentmode.st"; const TLD_API = "https://navigate.st/api/tlds?include_hidden=1"; @@ -364,6 +364,8 @@ async function openDomain(name) { $("tr-addr").value = ""; $("tr-ack").checked = false; $("tr-send").disabled = true; setMsg("tr-msg", ""); setMsg("dns-msg", ""); setMsg("ct-msg", ""); setMsg("rd-msg", ""); setSub(pendingSub || "summary"); pendingSub = "summary"; + $("sell-log").className = "steps-log"; $("sell-log").innerHTML = ""; setMsg("sell-msg", ""); + loadListing(name); await loadDns(name, true); } function setMsg(id, text, cls = "") { const m = $(id); if (!m) return; m.textContent = text; m.className = "msg " + cls; } @@ -610,6 +612,81 @@ $("tr-send").addEventListener("click", async () => { finally { try { el?.close?.(); } catch {} } }); +// ---------- signed API calls (owner proves control of the name's key) ---------- +async function signedHeaders(msg) { + const ts = String(Date.now()); + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg.replace("{ts}", ts)))); + const sig = wallet.signMessage(digest); + return { "x-bns-ts": ts, "x-bns-sig": btoa(String.fromCharCode(...sig)) }; +} + +// ---------- sell (marketplace listing) ---------- +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)`; + 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 →`]] + .map(([k, v]) => `
${k}
${v}
`).join(""); + } else { + st.className = "status"; st.textContent = "Not for sale. Set a price to list it."; + $("sell-form").classList.remove("hidden"); $("sell-active").classList.add("hidden"); + } +} +async function loadListing(name) { + activeListing = null; + 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) : ""; }); +$("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 push = logger("sell-log"); const btn = $("sell-list"); btn.disabled = true; btn.textContent = "Signing…"; setMsg("sell-msg", ""); + let el = null; + try { + el = await BNS.connect(); push("connected to chipnet"); + const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses); + const raw = BNS.normalizeName(current.name); + const commitment = [...new TextEncoder().encode(raw)].map((b) => b.toString(16).padStart(2, "0")).join(""); + const cert = utxos.find((u) => u.token?.category === current.entry.category && u.token?.nft?.commitment === commitment); + if (!cert) throw new Error("certificate UTXO not found in this wallet"); + push("found certificate · signing the offer (SINGLE|ANYONECANPAY)"); + const listing = BNS.buildListing({ name: current.name, certificateUtxo: cert, priceSats: sats, sellerAddress: cert.address, addressToLockingBytecode: BNS.addressToLockingBytecode, keyFor: (h) => wallet.keyFor(h) }); + const r = await fetch(`${API}/api/market`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(listing) }); + const j = await r.json().catch(() => ({})); + 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"); + } 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 {} } +}); +$("sell-cancel").addEventListener("click", async () => { + if (!current.entry || !wallet || !activeListing) return; + if (!confirm(`Cancel the listing for ${current.name}? This also moves the certificate once so the signed offer becomes void.`)) return; + const push = logger("sell-log"); const btn = $("sell-cancel"); btn.disabled = true; btn.textContent = "Cancelling…"; setMsg("sell-msg", ""); + try { + const h = await signedHeaders(`BNS-MARKET1\n${current.name}\n{ts}`); + const r = await fetch(`${API}/api/market/${encodeURIComponent(current.name)}`, { method: "DELETE", headers: h }); + const j = await r.json().catch(() => ({})); + if (!r.ok) throw new Error(j.error || `API ${r.status}`); + push("removed from the market"); + activeListing = null; paintSell(); + push("invalidating the signed offer on chain (no-op update)…"); + const ok = await publishRecords(current.entry.records || {}, "sell-log", "sell-msg", "sell-cancel", "Listing cancelled and the offer voided on chain."); + if (!ok) setMsg("sell-msg", "Removed from the market, but the on-chain invalidation failed — retry from Content & hosting (Save records) so the old offer cannot be used.", "err"); + } catch (e) { setMsg("sell-msg", "Failed: " + (e.message || e), "err"); push("error: " + (e.message || e)); } + finally { btn.disabled = false; btn.textContent = "Cancel listing →"; } +}); + // ---------- export ---------- function zoneText() { const e = current.entry; if (!e) return ""; @@ -699,6 +776,7 @@ async function openTld(label) { ["Policy", esc(rec.policy || "open") + (rec.min_len ? ` · min length ${rec.min_len}` : "") + (rec.reserved ? ` · reserved: ${esc(rec.reserved)}` : "")], ]; $("td-kv").innerHTML = kv.map(([k, v]) => `
${k}
${v}
`).join(""); + loadCosign(label); const names = (await registry()).filter((n) => n.tld === label); $("td-names").innerHTML = names.length ? names.map((n) => `
${esc(n.name)}${H.names.some((m) => m.name === n.name) ? 'yours' : ""}
@@ -706,6 +784,49 @@ async function openTld(label) {
block ${n.height ? fmtInt(n.height) : "mempool"}
`).join("") : `
No names registered under .${esc(label)} yet.
`; } +// ---------- co-sign queue (registrations under this TLD awaiting the owner) ---------- +async function loadCosign(label) { + const box = $("td-cosign"); + box.innerHTML = `
Checking…
`; + let list = []; + try { const j = await (await fetch(`${API}/api/cosign?tld=${encodeURIComponent(label)}`, { cache: "no-store" })).json(); list = j.requests || []; } catch {} + if (currentTld?.tld !== label) return; + box.innerHTML = list.length + ? list.map((r) => `
${esc(r.name)}awaiting you
+
+
buyer ${esc(r.buyer.replace(/^[^:]+:/, "").slice(0, 10))}…·${esc(new Date(r.created_at).toLocaleString())}${r.note ? `·“${esc(r.note)}”` : ""}
`).join("") + : `
No requests waiting.
`; +} +$("td-cosign").addEventListener("click", async (e) => { + const a = e.target.closest("[data-approve]"), d = e.target.closest("[data-decline]"); + if (!a && !d) return; + if (!currentTld || !wallet) return; + if (wallet.source === "wc") return setMsg("td-cosign-msg", "Co-signing from an external wallet lands in the next drop — sign in with the built-in wallet.", "err"); + const id = (a || d).dataset.approve || (a || d).dataset.decline; + const btn = a || d; btn.disabled = true; setMsg("td-cosign-msg", ""); + let el = null; + try { + const h = await signedHeaders(`BNS-COSIGN1 +${id} +{ts}`); + if (a) { + const r = await fetch(`${API}/api/cosign/${encodeURIComponent(id)}`, { cache: "no-store" }); + if (!r.ok) throw new Error("request is gone (withdrawn or expired)"); + const req = await r.json(); + const signed = BNS.signCosignRequest(req, (lock) => wallet.keyFor(lock)); + if (!signed.signedInputs) throw new Error("this wallet does not hold the certificate input of that transaction"); + el = await BNS.connect(); + const txid = await BNS.broadcast(el, signed.hex); + setMsg("td-cosign-msg", `Approved — ${req.name} registered in ${txid.slice(0, 16)}…. Your TLD certificate came back to you in the same transaction.`, "ok"); + } + const del = await fetch(`${API}/api/cosign/${encodeURIComponent(id)}`, { method: "DELETE", headers: h }); + if (!del.ok && d) throw new Error(`API ${del.status}`); + if (d) setMsg("td-cosign-msg", "Declined — the request was removed. The buyer's coins never left their wallet.", "ok"); + await loadCosign(currentTld.tld); + if (a) { registryCache = { at: 0, names: [] }; openTld(currentTld.tld); } + } catch (err) { setMsg("td-cosign-msg", "Failed: " + (err.message || err), "err"); btn.disabled = false; } + finally { try { el?.close?.(); } catch {} } +}); $("td-register").addEventListener("click", () => { if (!currentTld) return; const input = $("td-label"); const label = String(input.value || "").toLowerCase().replace(/[^a-z0-9-]/g, ""); diff --git a/js/i18n.js b/js/i18n.js index 827ab52..03ab07c 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -22,7 +22,7 @@ (function () { var STORE = "sirius-lang"; var CACHE = "sirius-i18n:"; - var VERSION = "20260917f"; // bump when dictionaries change + var VERSION = "20260917h"; // bump when dictionaries change var LANGS = { en: "English", de: "Deutsch", diff --git a/js/market.js b/js/market.js new file mode 100644 index 0000000..2afe6ec --- /dev/null +++ b/js/market.js @@ -0,0 +1,122 @@ +// Marketplace page: list the gateway's active offers and complete a purchase. +// +// A listing is the seller's partially signed transaction (see +// Argus/src/lib/market-tx.js). Buying = verify it locally, append our own +// funding inputs and the outputs that give us the certificate and update +// the registry, sign our inputs, broadcast. The seller's signature only +// stays valid while their certificate UTXO is unspent, so the gateway prunes +// stale offers and the buyer's broadcast is the final arbiter. + +import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market"; + +const API = "https://silentmode.st"; +const $ = (id) => document.getElementById(id); +const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); +const fmtInt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ","); +const usd = (sats) => { const r = window.siriusPricing?.CHIPNET_SATS_PER_USD || 250000; const v = Number(sats) / r; return (window.siriusPricing?.formatUsd || ((n) => "$" + n.toFixed(2)))(Math.round(v * 100) / 100); }; +const shortAddr = (a) => (a ? a.replace(/^[^:]+:/, "").slice(0, 8) + "…" + a.slice(-5) : "—"); + +let listings = []; +let wallet = null; +let selected = null; + +async function restoreWallet() { + if (window.siriusWallet) return window.siriusWallet; + const S = window.siriusSession; + if (!S?.restore) return null; + try { + const s = await S.restore(); + if (!s?.mnemonic) return null; + const w = await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.CHIPNET_PREFIX, s.accountPath || undefined); + window.siriusWallet = w; + return w; + } catch { return null; } +} + +async function load() { + const status = $("status"); + try { + const r = await fetch(`${API}/api/market`, { cache: "no-store" }); + if (!r.ok) throw new Error("API " + r.status); + const j = await r.json(); + listings = (j.listings || []).map((l) => ({ ...l, label: l.name.split(".")[0], tld: l.tld || l.name.split(".").pop() })); + render(); + } catch (e) { status.className = "status err"; status.textContent = "Could not load listings: " + (e.message || e); } +} +function render() { + const q = ($("q").value || "").toLowerCase().trim(); + const sort = $("sort").value; + const tldSel = $("tld"); + const tlds = [...new Set(listings.map((l) => l.tld))].sort(); + const keep = tldSel.value; + tldSel.innerHTML = `` + tlds.map((t) => ``).join(""); + let rows = listings.filter((l) => (!q || l.name.includes(q)) && (!tldSel.value || l.tld === tldSel.value)); + rows.sort((a, b) => sort === "name" ? a.name.localeCompare(b.name) : sort === "new" ? Date.parse(b.created_at) - Date.parse(a.created_at) : Number(a.price_sats) - Number(b.price_sats)); + $("count").textContent = `${rows.length} of ${listings.length}`; + const status = $("status"); + if (!listings.length) { status.className = "status"; status.textContent = "Nothing is for sale right now. Owners list names from their dashboard."; $("list").innerHTML = ""; return; } + status.className = "status"; status.textContent = `${listings.length} name${listings.length === 1 ? "" : "s"} for sale. Prices are set by the sellers; you pay the price plus about 1,300 sat of chain dust and fee.`; + $("list").innerHTML = rows.length ? rows.map((l) => ` +
+
${esc(l.label)}.${esc(l.tld)} ${wallet && l.seller === wallet.address ? 'yours' : ""}
+
${fmtInt(l.price_sats)} sat≈ ${esc(usd(l.price_sats))}
+
+
seller ${esc(shortAddr(l.seller))}·listed ${esc(new Date(l.created_at).toLocaleDateString())}·view site →
+
`).join("") : `
Nothing matches that filter.
`; +} +["q", "sort", "tld"].forEach((id) => $(id).addEventListener("input", render)); +document.addEventListener("click", (e) => { const b = e.target.closest("[data-buy]"); if (b) openBuy(b.dataset.buy); }); + +// ---------- buy ---------- +function setMsg(t, cls = "") { $("buy-msg").textContent = t; $("buy-msg").className = "msg " + cls; } +function push(t) { const log = $("buy-steps"); log.className = "steps on"; const d = document.createElement("div"); d.textContent = t; log.appendChild(d); log.scrollTop = 1e6; } +function openBuy(name) { + selected = listings.find((l) => l.name === name); if (!selected) return; + $("buy-name").textContent = name; + const price = BigInt(selected.price_sats); + const check = BNS.verifyListing(selected, { category: selected.category }); + $("buy-kv").innerHTML = [ + ["Price", `${fmtInt(price)} sat (≈ ${esc(usd(price))})`], + ["Seller", esc(selected.seller)], + ["You receive", "the name's certificate, to your token address"], + ["Offer check", check.ok ? "seller signature valid" : "INVALID: " + esc(check.error)], + ["Extra", "≈ 1,600 sat dust + fee"], + ].map(([k, v]) => `
${k}
${v}
`).join(""); + $("buy-steps").className = "steps"; $("buy-steps").innerHTML = ""; setMsg(""); + const canBuy = !!wallet && check.ok && wallet.address !== selected.seller; + $("buy-go").disabled = !canBuy; $("buy-go").textContent = "Buy — sign & broadcast"; + $("buy-signin").hidden = !!wallet; + if (!wallet) setMsg("Sign in on the dashboard first — the purchase is signed with your wallet in this browser."); + $("buy").classList.add("open"); +} +const closeBuy = () => $("buy").classList.remove("open"); +$("buy-close").addEventListener("click", closeBuy); $("buy-cancel").addEventListener("click", closeBuy); +$("buy").addEventListener("click", (e) => { if (e.target === $("buy")) closeBuy(); }); +$("buy-go").addEventListener("click", async () => { + if (!selected || !wallet) return; + const btn = $("buy-go"); btn.disabled = true; btn.textContent = "Buying…"; setMsg(""); + let el = null; + try { + el = await BNS.connect(); push("connected to chipnet"); + const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses); + push(`wallet has ${utxos.filter((u) => !u.token).length} spendable coin(s)`); + const built = BNS.completeSale({ listing: selected, buyerAddress: wallet.address, buyerTokenAddress: wallet.tokenAddress, utxos, records: selected.records || {}, addressToLockingBytecode: BNS.addressToLockingBytecode }); + push(`built sale · price ${fmtInt(built.costs.priceSats)} sat · fee ${built.costs.chainFeeSats} sat`); + const signed = BNS.signInputs(built.transaction, built.sourceOutputs, (h) => wallet.keyFor(h), { skip: [built.sellerInputIndex] }); + push("signed your inputs (the seller's signature is already in place)"); + const txid = await BNS.broadcast(el, signed.hex); + push("broadcast: " + txid); + setMsg(`Done. ${selected.name} is yours — it appears in your dashboard within a minute.`, "ok"); + btn.textContent = "Bought ✓"; + setTimeout(load, 3000); + } catch (e) { + const raw = String(e.message || e); + setMsg(/not enough funds/i.test(raw) ? `Not enough coins: ${raw}. Fund ${wallet.address} first.` : "Failed: " + raw, "err"); + push("error: " + raw); btn.disabled = false; btn.textContent = "Buy — sign & broadcast"; + } finally { try { el?.close?.(); } catch {} } +}); + +(async function boot() { + wallet = await restoreWallet(); + await load(); +})(); diff --git a/js/pricing.js b/js/pricing.js index cac78c9..be71321 100644 --- a/js/pricing.js +++ b/js/pricing.js @@ -48,7 +48,13 @@ hidden, hiddenBy: row && row.hidden_by ? row.hidden_by : (hidden ? "owner" : null), frozen: policy === "frozen", + // cosign: anyone may register, but the TLD owner must co-sign the + // registration (indexers reject a REG under a cosign/hidden TLD that + // does not carry the TLD certificate). hidden implies the same rule + // on chain; in the UI hidden is owner-only, cosign is request-based. + cosign: policy === "cosign", policy, + category: row && row.category ? row.category : null, owner: row && row.owner ? row.owner : null, records: rec, }; @@ -122,6 +128,9 @@ const owner = isTldOwner(tld, addresses); if (p.frozen) return { ok: false, reason: "frozen", policy: p, owner }; if (p.hidden && !owner) return { ok: false, reason: "hidden", policy: p, owner }; + // Sellable, but the registration needs the owner's co-signature: the + // flow builds the request and parks it in the gateway's approval queue. + if (p.cosign && !owner) return { ok: true, reason: "cosign", policy: p, owner }; return { ok: true, reason: null, policy: p, owner }; } diff --git a/js/register-flow.js b/js/register-flow.js index 19882c4..927b531 100644 --- a/js/register-flow.js +++ b/js/register-flow.js @@ -7,7 +7,7 @@ // Injects its own modal HTML into and its own CSS into on load // so the host page needs nothing but a single script include. -import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917sig"; +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])); @@ -416,6 +416,14 @@ async function startFlow(name, opts = {}) { } const p = sp?.priceForName(label, tld); state.ownerMint = !!sell.owner; + // Co-sign rule (consensus): a REG under a TLD whose policy is `cosign` or + // `hidden` is only admitted if the transaction also carries the TLD's own + // certificate. Owner: add it from this wallet. Anyone else under a cosign + // TLD: build the transaction, sign our inputs, and park it in the gateway + // queue for the owner to co-sign and broadcast. + const pol = sell.policy || {}; + state.cosign = sell.owner && (pol.hidden || pol.cosign) && pol.category ? { tld, category: pol.category } : null; + state.cosignRequest = sell.reason === "cosign" && pol.category ? { tld, category: pol.category, owner: pol.owner } : null; // Owner registering under their own TLD: they would receive 90% of the // fee from themselves, so only the platform's share is charged. The // registrar lib already routes the whole fee to the platform when the @@ -610,6 +618,13 @@ async function stepExternalConfirm(session, label) { // TLD external path: keep the session, hand off to the TLD confirm/mint // steps which know to call registerTldWithExternalWallet. if (state.tld) { state.session = session; return stepConfirmTld(); } + if (state.cosign || state.cosignRequest) { + render(`

Use the built-in wallet for this TLD

+

Names under .${esc(state.cosignRequest?.tld || state.cosign?.tld)} need the TLD owner's co-signature in the same transaction. That flow is built for the browser wallet; external-wallet support lands in the next drop.

+
`); + $("back").onclick = async () => { try { await session.disconnect(); } catch {} stepWallet(); }; + return; + } const feeOverride = state.nameFeeSats != null ? { serviceFee: { ...(BNS.REGISTRAR?.serviceFee ?? {}), sats: state.nameFeeSats } } @@ -922,15 +937,17 @@ async function stepConfirm() {

Confirm ${esc(quote.displayName)}

One transaction. It mints the certificate to your address and pays the fees.

${state.ownerMint ? `
You own this TLD, so this is an owner registration: only the platform's 10% share of the name price is charged — the 90% owner share would just be paid to yourself.
` : ""} + ${state.cosign ? `
Private TLD: your .${esc(state.cosign.tld)} certificate is spent and re-issued to you in the same transaction — that is the co-signature the registry requires.
` : ""} + ${state.cosignRequest ? `
Needs the TLD owner's approval. .${esc(state.cosignRequest.tld)} is a co-sign TLD. You sign your part now; the owner co-signs and broadcasts from their dashboard. Nothing leaves your wallet until they do, and if they decline the request simply expires.
` : ""} ${rows}
The certificate will be minted to ${esc(state.wallet.address)} — your key, your name. You can set where it points immediately afterwards.
-
+ `); $("back").onclick = stepFund; - $("go").onclick = () => stepApprove({ feeSats: state.nameFeeSats ?? 0n, kind: "name", back: stepConfirm, next: stepRegister }); + $("go").onclick = () => stepApprove({ feeSats: state.nameFeeSats ?? 0n, kind: "name", back: stepConfirm, next: state.cosignRequest ? stepRequestCosign : stepRegister }); } catch (e) { render(`

Could not prepare

${esc(e.message)}

`); @@ -1139,6 +1156,7 @@ async function stepRegister() { wallet: state.wallet, name: state.name, records: {}, onProgress: (s) => say(words[s] ?? s), ...(state._feeOverride ?? {}), + ...(state.cosign ? { cosign: state.cosign } : {}), }); state.result = res; stepDone(); @@ -1151,6 +1169,95 @@ async function stepRegister() { } } +// ---------- co-sign request (third party under a cosign TLD) ---------- +// Build the full registration with the TLD owner's certificate as the last +// input, sign only our own inputs, hand the half-signed transaction to the +// gateway queue. The owner signs the certificate input and broadcasts. +const GW = "https://silentmode.st"; +async function stepRequestCosign() { + const cr = state.cosignRequest; + render(`

Requesting approval for ${esc(state.name)}

+

Building and signing your half of the transaction. Do not close this tab.

+
`); + const log = $("log"); + const say = (s) => { const d = document.createElement("div"); d.textContent = s; log.appendChild(d); log.scrollTop = log.scrollHeight; }; + const words = { + "checking-availability": "checking the name is still free", + "loading-coins": "loading your coins", + "preparing-wallet": "preparing your wallet (one-off setup transaction)", + "prepared": "wallet prepared", + "building": "building the registration with the owner's certificate", + "signing": "signing your inputs — in this browser", + }; + try { + say(`locating the .${cr.tld} certificate`); + const cres = await fetch(`${GW}/api/tld-cert/${encodeURIComponent(cr.tld)}`, { cache: "no-store" }); + const cert = await cres.json(); + if (!cres.ok) throw new Error(cert.error || `API ${cres.status}`); + const built = await BNS.buildCosignRequest(await client(), { + wallet: state.wallet, name: state.name, records: {}, tldCert: cert, + onProgress: (s) => say(words[s] ?? s), + ...(state._feeOverride ?? {}), + }); + say("submitting to the approval queue"); + const r = await fetch(`${GW}/api/cosign`, { method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: built.name, buyer: state.wallet.address, tx: built.tx, sourceOutputs: built.sourceOutputs, note: "" }) }); + const j = await r.json().catch(() => ({})); + if (!r.ok) throw new Error(j.error || `API ${r.status}`); + state.cosignId = j.id; + stepAwaitCosign(); + } catch (e) { + err(e); + const row = document.createElement("div"); row.className = "row end"; + row.innerHTML = ``; + sheet.appendChild(row); + $("back").onclick = stepConfirm; + } +} +async function stepAwaitCosign() { + const cr = state.cosignRequest; + render(` +

Waiting for the owner of .${esc(cr.tld)}

+

Your request ${esc(state.cosignId)} is in the queue. The TLD owner sees it in their dashboard and + co-signs and broadcasts it in one click. Your coins stay in your wallet until then — if you spend them elsewhere, the request simply becomes invalid.

+
Requests expire after 7 days. You can close this window; when the owner approves, ${esc(state.name)} appears in your dashboard.
+
Checking every 20 seconds…
+
+ `); + let stop = false; + $("done").onclick = () => { stop = true; close(); }; + $("withdraw").onclick = async () => { + try { + const ts = String(Date.now()); + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`BNS-COSIGN1\n${state.cosignId}\n${ts}`))); + const sig = btoa(String.fromCharCode(...state.wallet.signMessage(digest))); + await fetch(`${GW}/api/cosign/${encodeURIComponent(state.cosignId)}`, { method: "DELETE", headers: { "x-bns-ts": ts, "x-bns-sig": sig } }); + } catch {} + stop = true; close(); + }; + const tick = async () => { + if (stop || !document.body.contains($("poll"))) return; + try { + const r = await fetch(`${GW}/api/cosign/${encodeURIComponent(state.cosignId)}`, { cache: "no-store" }); + if (r.status === 404) { + const avail = await BNS.checkAvailability(await client(), state.name); + if (!avail.available) { + render(`

${esc(state.name)} is yours

+

The owner of .${esc(cr.tld)} co-signed and broadcast your registration. Set records and content from your dashboard.

+
Open dashboard →
`); + } else { + render(`

Request declined

The owner of .${esc(cr.tld)} removed your request. Nothing was spent.

+
`); + $("done").onclick = close; + } + return; + } + } catch {} + setTimeout(tick, 20000); + }; + setTimeout(tick, 20000); +} + // ---------- done + point somewhere ---------- function stepDone() { const r = state.result; diff --git a/js/site-footer.js b/js/site-footer.js index 267b972..6168ce8 100644 --- a/js/site-footer.js +++ b/js/site-footer.js @@ -33,6 +33,7 @@ diff --git a/market.html b/market.html new file mode 100644 index 0000000..af05243 --- /dev/null +++ b/market.html @@ -0,0 +1,130 @@ + + + + + +Market — names for sale on Sirius.X + + + + + + + + + + +
+
🛒
+

Names for sale

+

Every sale is a single Bitcoin Cash transaction. The seller signed an offer that pays them only when the + certificate moves to you — you add your coins, sign, broadcast. No escrow, no middleman, no way for either side to cheat.

+
+ +
+
+ + + + +
+
Loading listings…
+
+ +
Selling a name? Open it in your dashboard, choose Sell, set a price. The listing is a signed offer stored by the gateway; cancelling it moves your certificate once so the old offer can never be used.
+
+ + + + + + + + + + + + + + diff --git a/portal.html b/portal.html index dbb98a4..2968e50 100644 --- a/portal.html +++ b/portal.html @@ -222,7 +222,7 @@ .addrow{grid-template-columns:1fr 1fr} } - + @@ -231,6 +231,7 @@ Try it ✏️ Register 🌐 TLD + 🛒 Market Theseus 🔑 Sign in @@ -313,9 +314,35 @@ + + +
@@ -475,6 +502,12 @@
+
+

Pending approvals

+

Registrations under this TLD that wait for your co-signature. Approving signs your TLD certificate into the buyer's transaction and broadcasts it; the certificate returns to you in the same transaction. Nothing can be registered under a co-sign or private TLD without this step.

+
+
+

Names registered under this TLD

Everyone's registrations, from the public registry.

@@ -607,11 +640,11 @@ - - + + - - + + diff --git a/studio.html b/studio.html index 6dbc125..8ac0bb4 100644 --- a/studio.html +++ b/studio.html @@ -65,7 +65,7 @@ .muted{color:var(--mut);font-size:13px} code{font-family:var(--mono);background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-size:12px} - +
diff --git a/tld.html b/tld.html index 7408883..4834893 100644 --- a/tld.html +++ b/tld.html @@ -115,7 +115,7 @@ - + @@ -302,12 +302,12 @@ })(); - - + + - +