diff --git a/docs/DESIGN-signed-records-manifest.md b/docs/DESIGN-signed-records-manifest.md new file mode 100644 index 0000000..a6c6fc6 --- /dev/null +++ b/docs/DESIGN-signed-records-manifest.md @@ -0,0 +1,154 @@ +# Signed records manifest (BNS records v1) + +**Status:** shipped v1 — gateway `GET/POST /api/records/` and +verified `GET /api/dns/` live on `silentmode.st/navigate.st`; +owner-tracking in the indexer live; portal DNS editor + wallet-side +`signMessage` + `signRecordsManifest` shipped. Native DNS server +(UDP :53 → BCDN name → verified records) is a v2 follow-up. +**Related:** `DESIGN-tld-registry.md`, `PROTOCOL.md`, public explainer at +`silentmode.st/sirius-x/docs/#signed-records`. Mirrored to +`site-sirius-x/docs/DESIGN-signed-records-manifest.md` so it rides the +next push-split to the sirius forge. + +## Problem + +Every records edit today is a chain UPD: +- costs ~1,300 sat + service fee +- caps records at ~150 bytes of payload +- rules out DNS-style records (A/AAAA/MX/TXT/CNAME/NS) at any useful scale +- makes TXT-verification churn (ACME, DKIM, DMARC) actively expensive + +We want records to be: +- **free to edit** — no chain tx per change +- **unlimited size** — accommodate whole DNS record sets +- **trustless** — no operator can substitute their own records for the owner's +- **backwards compatible** — existing on-chain `h` / `s3` / `p` / `u` records keep working + +## Design + +The chain says *who owns the name*. A signed manifest says *what the owner's +records are*. Resolvers verify both. + +### The manifest — `_records.json` + +Lives at the Sia bucket the name's on-chain `records.s3` already points at, +under the fixed filename `_records.json`. Shape: + +```json +{ + "v": 1, + "name": "bitcoin.cash", + "seq": 42, + "updated_at": "2026-09-12T00:00:00Z", + "dns": { + "A": ["1.2.3.4"], + "AAAA": ["2001:db8::1"], + "MX": [{"pref": 10, "host": "mail.example.com"}], + "TXT": ["v=spf1 include:_spf.silentmode.st -all"], + "CNAME": null, + "NS": [] + }, + "meta": { "note": "optional free-form" }, + "sig": "H3n… (BCH message signature, base64)" +} +``` + +**Rules:** +- `v`: schema version, currently `1`. +- `name`: the fully-qualified name the manifest belongs to. Resolver rejects + a manifest whose `name` field doesn't match the URL it was fetched from. +- `seq`: monotonic integer. Resolvers cache the highest seen; blobs with an + older or equal `seq` are rejected as replays. +- `updated_at`: ISO-8601 UTC. Human-readable only — verification is on `seq`. +- `dns.*`: DNS record slots. `null` or `[]` means "not set". `A`/`AAAA`/`TXT` + are string arrays. `MX` is objects `{pref, host}`. `NS` is a string array + of host names. Additional slots (`CAA`, `SRV`) reserved for v2. +- `meta`: unstructured; ignored by DNS resolvers, useful for portal UI. +- `sig`: BCH-style message signature (same format `libauth.signMessage` + produces) over the canonical bytes of the envelope (see below). + +### Canonicalisation and signing + +The signable bytes are the JSON serialisation of the manifest **without the +`sig` field**, with: +- keys sorted lexicographically at every level +- no insignificant whitespace (`JSON.stringify` with no `space` arg) +- `\u`-escape every non-ASCII character +- `null` fields OMITTED (`"CNAME": null` becomes absent) + +The signature is `sign(sha256(canonical_bytes))` with the wallet key whose +address currently holds the name's NFT certificate. + +Verification, at the resolver: +1. Read the name's chain record. Follow `records.s3` to the Sia bucket. +2. Fetch `_records.json` from that bucket. +3. Extract `sig`, recompute `sha256(canonical_bytes)` over the rest. +4. Recover the signing pubkey → derive the CashAddress → compare to the + current NFT owner address from the chain. +5. Compare `seq` to the cached last-seen `seq` for this name. Reject if not + strictly greater. +6. If everything matches: cache the new `seq`, apply the DNS records. + +Any step failing means "no records" — the resolver falls back to whatever +was already on the chain (`h`, `s3` for content, etc.). + +### Endpoints (gateway) + +**GET `/api/records/`** — read-through of the signed manifest. +Returns 200 with the JSON if present, 404 if missing, 5xx on Sia errors. +Cacheable for a minute. Anyone can call this; it's public. + +**POST `/api/records/`** — write. Body IS the manifest JSON. +The gateway: +1. Verifies the signature matches the current on-chain NFT owner. +2. Verifies `seq` is strictly greater than the last-seen `seq` (from + `GET`ing the current manifest if any). +3. Uploads the JSON to the name's Sia bucket at `_records.json`. +4. Returns 200 with `{seq, bytes, sia_key}`. + +Rate-limit: 1 write / 5 s per name. Refuses if the on-chain record has no +`s3` pointer yet — the name has no storage location, no place to write. + +### Precedence and interaction with existing records + +The signed manifest **complements** the on-chain records — it doesn't replace +them: +- **Content** (`h`, `s3`, `p`, `u`, `ip`) still comes from chain. +- **DNS records** come from the manifest — chain has never carried them. + +A future extension (v2) could allow the manifest to override `h`/`s3`/etc. +for owners who want everything off-chain. Not shipped in v1 because the +existing content model already works. + +### Threat model + +- **Operator tampers with the manifest.** Signature verification catches it — + the resolver rejects and falls back to on-chain records. +- **Replay of an old manifest.** `seq` monotonicity catches it — resolvers + keep the highest-seen `seq`. +- **Owner's key compromised.** Same as chain-side compromise: attacker + controls the NFT and can sign whatever. Move the name to a new key. +- **Sia object deleted.** The name still resolves via chain records; DNS + records disappear until the manifest is republished. +- **Gateway hostility.** The gateway signs nothing itself — it only relays + and enforces `seq` monotonicity. A hostile gateway can refuse to accept + writes but cannot forge them; users can bypass it entirely by writing + directly to Sia with their own credentials (v2 flow). + +### What ships in v1 + +- The spec (this doc). +- Gateway `/api/records/` (GET + POST) with signature verification and + Sia read-through. +- Portal DNS editor that composes a manifest, asks the wallet to sign, and + POSTs to the gateway. +- Resolver support in Theseus + Ariadne staged behind a flag until the + portal-side has been in production for a week. + +### What ships in v2 + +- Direct-to-Sia write flow for users who hold their own S3 credentials. +- Owner-controlled manifest that overrides `h`/`s3`/etc. entirely. +- CAA/SRV/HTTPS records. +- Federated resolver caches for `seq` (mitigates the case where two writes + race and each thinks its `seq` is winning). diff --git a/docs/index.html b/docs/index.html index 62550b3..222d93f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -120,6 +120,7 @@ TLD registry Register a name Records & hosting + Signed off-chain records Host your name Resolver / gateway Pricing (mainnet) @@ -193,6 +194,89 @@ (Host-header semantics). Full rule is in Argus/src/lib/record-picker.js.

+
+

Free edits — signed off-chain records

+

The on-chain records above (h, s3, ip, u, + tls, np, el) cost a chain transaction to change and are capped + at 200 bytes of OP_RETURN. For DNS-style records (A / AAAA / MX / TXT / CNAME / NS) and any records + you edit often, Sirius.X uses a companion mechanism: a signed off-chain manifest that costs + nothing per change and has no size cap.

+ +

The chain says who owns the name. A signed blob says what + the records are. Resolvers verify both. No third party is trusted for records: the manifest is + signed by the same key that holds the on-chain NFT certificate, so an operator (or Sia farmer, or + anyone else in the middle) can't rewrite records without breaking the signature.

+ + + + + + + + +
ConcernWhere it livesCost per change
Who owns the nameBitcoin Cash chain (NFT certificate)1 tx to transfer
Where the site livesChain record: s3, ip, u, h1 tx per change
DNS records, meta, TXT churn, etc.Sia at <s3-bucket>/_records.jsonfree
+ +

How the manifest is verified

+
    +
  1. Resolver reads the name's chain record, follows records.s3 to the Sia bucket, + fetches _records.json.
  2. +
  3. Signature check — recovers the signing pubkey from the manifest's sig field, + derives the CashAddress, compares to the current NFT-holder address from the chain. Mismatch = reject.
  4. +
  5. Replay check — the manifest carries a monotonic seq number. Resolvers cache the + highest seen and reject anything less-or-equal. An old signed blob can't be re-served.
  6. +
  7. Fall-back — if the manifest is missing, invalid, or its signature doesn't match, the resolver + silently falls back to the on-chain records. Signed records never override a name; they extend it.
  8. +
+ +

The manifest shape

+
{
+  "v":    1,
+  "name": "bitcoin.cash",
+  "seq":  42,
+  "updated_at": "2026-09-15T00:00:00Z",
+  "dns": {
+    "A":     ["1.2.3.4"],
+    "AAAA":  ["2001:db8::1"],
+    "MX":    [{"pref": 10, "host": "mail.example.com"}],
+    "TXT":   ["v=spf1 include:_spf.silentmode.st -all"],
+    "CNAME": null,
+    "NS":    []
+  },
+  "sig":  "<BCH message signature over sha256(canonical bytes)>"
+}
+

Canonicalisation: sorted keys, no whitespace, null + fields omitted. The signable bytes are the manifest with the sig field removed. Full + spec: DESIGN-signed-records-manifest.md + — signable byte order, seq-monotonicity rules, threat model, v2 roadmap.

+ +

Endpoints

+ + + + + + + +
MethodPathEffect
GET/api/records/<name>Fetches the raw signed manifest from the name's Sia bucket. Public, cacheable.
POST/api/records/<name>Accepts a signed manifest, verifies against the on-chain owner + seq monotonicity, uploads to Sia.
GET/api/dns/<name>Resolver read. Fetches the manifest, verifies the signature against the on-chain owner and rejects rollback of the seq, then returns only the verified dns block. Callers can trust the response without doing their own signature or chain lookups. 30-second cache hint.
+

The distinction between /api/records/<name> + and /api/dns/<name> is deliberate: the former is transparent (raw manifest as + uploaded, including the signature so clients can re-verify), the latter is opinionated (only the + verified DNS records, with the gateway having done the sig + seq work). Bridges and shims should + use /api/dns/; wallets and tools that want to see the full signed blob should use + /api/records/.

+

A name needs records.s3 set on chain first — + without a storage pointer, the manifest has nowhere to live. Portal and CLI both refuse to + publish a manifest until s3 is present.

+ +
+ Threat model at a glance. Operator tampers → sig check fails, resolver falls back to chain + records. Replay of an old blob → seq check fails. Owner's key compromised → same as chain-side + compromise; move the name to a new key. Sia object deleted → name still resolves via chain + records, DNS records disappear until republished. Hostile gateway can refuse writes but cannot + forge them; users can PUT directly to Sia with their own credentials to bypass. +
+
+

Host your name

The end-to-end recipe for putting a static site under a BCNR name. Chipnet @@ -358,6 +442,7 @@ process.exit(0);" owner collects a share (fee_bps, default 5%, cap 50%). Chain-enforced when the TLD's policy is covenant; honour-system otherwise. Name registration is yearly and priced separately.

+

The TLD owner also sets what names under it sell for and whether the TLD is on or off, straight from the portal: a price record (flat USD per name, replaces the length tiers for every buyer) and a hidden record (1 makes it a private TLD: off the public list, nobody but the owner can register names under it — the owner still can from the portal, paying only the platform share — until it is switched back on). Both ride the same signed TUPD, so every client reads them from the chain; the owner keeps 90% of each sale.

diff --git a/index.html b/index.html index e9c1928..3701fee 100644 --- a/index.html +++ b/index.html @@ -195,11 +195,24 @@ // against the client-side TLDS list. Register buttons hand the label off // to register.html?q= for the full mint flow. (function () { - const TLDS = [ + // Fallback list, used until /api/tlds answers (js/pricing.js loads it). + // The live list drops TLDs their owners switched off and carries the + // owner-set prices, so it wins whenever it is available. + let TLDS = [ "bch","p2p","bit","nav","test","x","asm","neo","gt","sc", "sia","dex","cex","nt","dea","hub","os","vpn", - // .com is held off the public registry (2026-08-29) — skip. ]; + // .com is held off the public registry (2026-08-29) regardless of what + // the API says — the operator holds it; nobody buys names under it yet. + const NEVER_SELL = new Set(["com"]); + async function liveTlds() { + const sp = window.siriusPricing; + if (!sp) return TLDS; + await sp.tldReady(); + const live = sp.visibleTlds(); + if (live && live.length) TLDS = live.filter((t) => !NEVER_SELL.has(t) && sp.tldSellable(t).ok); + return TLDS; + } const $ = (id) => document.getElementById(id); const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); const clean = (v) => v.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 32); @@ -213,9 +226,10 @@ // pricing.js so landing / tld.html / the mint modal all agree. let price = ""; if (state === "ok" && window.siriusPricing) { - const label = full.split(".")[0]; - const p = window.siriusPricing.priceForName(label); - price = `${esc(window.siriusPricing.formatUsd(p.usd))}`; + const [label, tld] = full.split("."); + const p = window.siriusPricing.priceForName(label, tld); + const why = p.ownerSet ? `price set by the .${tld} owner` : p.tier; + price = `${esc(window.siriusPricing.formatUsd(p.usd))}`; } const btn = state === "ok" ? `` @@ -233,7 +247,10 @@ out.innerHTML = TLDS.map((t) => row(`${label}.${t}`, "checking")).join(""); let taken = new Set(); try { - const r = await fetch(`https://silentmode.st/api/labels/${encodeURIComponent(label)}`); + const [r] = await Promise.all([ + fetch(`https://silentmode.st/api/labels/${encodeURIComponent(label)}`), + liveTlds(), // owner prices + on/off state, same round-trip + ]); if (r.ok) { const j = await r.json(); for (const m of (j.matches || [])) taken.add(m.name); @@ -489,9 +506,9 @@ process.exit(0);" - + - + diff --git a/js/pricing.js b/js/pricing.js index 8a0baed..cac78c9 100644 --- a/js/pricing.js +++ b/js/pricing.js @@ -3,6 +3,12 @@ // operator-level discounts and coupon overrides land in a future revision // (the plan is oracle-fed rates + coupon-code redemption at mint time). // +// Two layers decide what a NAME costs: +// 1. The TLD owner's on-chain policy (`price`, `hidden`, `policy`) — read +// from the gateway's /api/tlds snapshot of the TLD beacon. A TLD owner +// who sets `price: 7` sells every name under that TLD for $7, flat. +// 2. Otherwise the length tiers below. +// // Exposed as window.siriusPricing so both the register-flow modal and the // inline search widgets on landing/tld.html use one source of truth. @@ -16,12 +22,127 @@ const cleanLabel = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9-]/g, ""); - // Tier picker — LENGTH-first, then downshift for "less brandable" shapes - // (all-digits, hyphenated). Kept as multiplicative modifiers so tiers - // remain the primary story and mods just soften an edge case. - function priceForName(label) { + // ---------- TLD owner policies ---------- + // tld -> { price, hidden, frozen, policy, owner, records } + // Filled from /api/tlds (see loadTldPolicies). Unknown TLDs fall through + // to tier pricing and are treated as sellable, so a gateway blip never + // blocks a purchase; the chain is the authority at mint time anyway. + // include_hidden=1 so the policy map knows about switched-off (private) + // TLDs too — their owners register names under them from the portal, and + // the register flow must be able to price and gate them. The public + // listing (visibleTlds) still drops anything hidden. + const TLD_API = "https://navigate.st/api/tlds?include_hidden=1"; + const TLD_POLICIES = new Map(); + let tldList = null; // raw /api/tlds rows, once loaded (hidden ones excluded) + let tldLoad = null; // memoised in-flight fetch + + function normalisePolicy(row) { + const rec = row && row.records && typeof row.records === "object" ? row.records : {}; + const priceRaw = row && row.price_usd != null ? row.price_usd : rec.price; + const price = typeof priceRaw === "number" && Number.isFinite(priceRaw) && priceRaw >= 0 ? priceRaw : null; + const hidden = !!(row && row.hidden) || rec.hidden === 1 || rec.hidden === true; + const policy = typeof rec.policy === "string" ? rec.policy : "open"; + return { + tld: row.tld, + price, + hidden, + hiddenBy: row && row.hidden_by ? row.hidden_by : (hidden ? "owner" : null), + frozen: policy === "frozen", + policy, + owner: row && row.owner ? row.owner : null, + records: rec, + }; + } + + function setTldPolicies(rows) { + TLD_POLICIES.clear(); + for (const r of rows || []) { + const row = typeof r === "string" ? { tld: r } : r; + const tld = cleanLabel(row && row.tld); + if (!tld) continue; + TLD_POLICIES.set(tld, normalisePolicy({ ...row, tld })); + } + } + + // Fetch the TLD list once per page. Resolves to the array of visible TLD + // rows (or null on failure) — never rejects, callers just fall back. + function loadTldPolicies({ url = TLD_API, force = false } = {}) { + if (tldLoad && !force) return tldLoad; + tldLoad = (async () => { + try { + const r = await fetch(url, { cache: "no-store" }); + if (!r.ok) throw new Error("api " + r.status); + const j = await r.json(); + const rows = Array.isArray(j.tlds) ? j.tlds : []; + setTldPolicies(rows); + // Only a beacon-sourced list is a sellable list. A gateway still + // serving the legacy `tlds.bch`-derived union includes every suffix + // that ever appeared on a name (".de", ".silentmode", …), which is + // not what the registry sells — keep the page's own fallback then. + tldList = j.source === "TLD_BEACON" ? rows.filter((t) => !t.hidden) : null; + return tldList; + } catch { + return null; + } + })(); + return tldLoad; + } + + // Resolves once the list is loaded, or after `ms` — search UIs await this + // so the first paint already shows owner prices without ever hanging. + function tldReady(ms = 2500) { + return Promise.race([ + loadTldPolicies(), + new Promise((res) => setTimeout(() => res(null), ms)), + ]); + } + + function tldPolicy(tld) { + return TLD_POLICIES.get(cleanLabel(tld)) || null; + } + + // Does one of `addresses` (the signed-in wallet's) hold this TLD's NFT? + // `owner` comes from the gateway's beacon snapshot (mint output, updated + // on every TUPD carrier), so a wallet that owns the TLD matches here. + function isTldOwner(tld, addresses) { + const p = tldPolicy(tld); + if (!p || !p.owner || !addresses) return false; + const list = Array.isArray(addresses) ? addresses : [addresses]; + return list.some((a) => typeof a === "string" && a === p.owner); + } + + // Can names be sold under this TLD right now? + // hidden → owner switched the TLD off: private TLD. The public cannot + // buy, but the OWNER still can — pass their wallet addresses + // and a hidden TLD they hold answers ok with owner: true. + // frozen → owner set policy "frozen" (no new registrations, owner included) + function tldSellable(tld, addresses) { + const p = tldPolicy(tld); + if (!p) return { ok: true, reason: null, owner: false }; + 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 }; + return { ok: true, reason: null, policy: p, owner }; + } + + // Visible TLD labels in listing order, or null if no beacon-sourced list + // is available (callers keep their own fallback list then). + function visibleTlds() { + return tldList ? tldList.map((t) => t.tld) : null; + } + + // ---------- name pricing ---------- + // Owner-set flat price first; else LENGTH-first tiers, then downshift for + // "less brandable" shapes (all-digits, hyphenated). Kept as multiplicative + // modifiers so tiers remain the primary story and mods just soften an + // edge case. + function priceForName(label, tld) { const s = cleanLabel(label); if (!s) return { usd: 0, tier: "invalid" }; + const p = tld ? tldPolicy(tld) : null; + if (p && p.price != null) { + return { usd: p.price, tier: "tld-set", sats: usdToSats(p.price), tld: p.tld, ownerSet: true }; + } let usd, tier; if (s.length === 1) { usd = 5.00; tier = "premium-1"; } else if (s.length === 2) { usd = 3.00; tier = "short-2"; } @@ -36,9 +157,24 @@ return { usd, tier, sats: usdToSats(usd) }; } + // Per-label overrides — TLDs the operator wants priced outside the tiers. + // Cheap namespaces (`.test`, `.dev`, `.local`) exist for experimentation, + // school assignments and demo throwaways: length-tier pricing prices them + // like premium TLDs, which isn't the point. Add labels here to opt them + // out of the tier and (optionally) mark them renewal-free. + // + // Shape: label -> { usd, tier, noRenewal? } + const SPECIAL_TLDS = { + "test": { usd: 0.01, tier: "sandbox", noRenewal: true }, + }; + function priceForTld(label) { const s = cleanLabel(label); if (!s) return { usd: 0, tier: "invalid" }; + if (Object.prototype.hasOwnProperty.call(SPECIAL_TLDS, s)) { + const o = SPECIAL_TLDS[s]; + return { usd: o.usd, tier: o.tier, sats: usdToSats(o.usd), noRenewal: !!o.noRenewal }; + } let usd, tier; if (s.length === 1) { usd = 500.00; tier = "premium-1"; } else if (s.length === 2) { usd = 250.00; tier = "short-2"; } @@ -68,5 +204,18 @@ formatSats, usdToSats, CHIPNET_SATS_PER_USD, + // TLD owner policy layer + loadTldPolicies, + setTldPolicies, + tldReady, + tldPolicy, + tldSellable, + isTldOwner, + visibleTlds, + TLD_API, }; + + // Warm the policy cache as soon as the script lands; every consumer + // awaits tldReady() before it paints a price. + loadTldPolicies(); })(); diff --git a/js/register-flow.js b/js/register-flow.js index 8ed76aa..834ba4f 100644 --- a/js/register-flow.js +++ b/js/register-flow.js @@ -353,14 +353,42 @@ async function startFlow(name, opts = {}) { state.name = name; state.tld = null; state.signInOnly = false; - // Tier-based service fee (siriusPricing owns the tiers so landing search - // badges, tld cards and this mint agree). Caller can override with + // Service fee: the TLD owner's flat price when they set one, else the + // length tier (siriusPricing owns both so landing search badges, tld + // cards and this mint agree). Caller can override with // opts.serviceFeeSats when a coupon or manual price is in play. - const label = String(name).split(".")[0]; - const p = window.siriusPricing?.priceForName(label); + const [label, tldRaw] = String(name).split("."); + const tld = tldRaw || "bch"; + const sp = window.siriusPricing; + if (sp?.tldReady) await sp.tldReady(); + // A TLD its owner switched off is private: the public cannot buy under + // it, but the owner still can (from the portal's TLDs tab). Frozen means + // nobody, owner included. Check against the signed-in wallet's addresses + // so the owner's own purchase goes through. + const mine = (window.siriusWallet?.watchedAddresses) + || (window.siriusWallet?.address ? [window.siriusWallet.address] : []) + || []; + const sell = sp?.tldSellable ? sp.tldSellable(tld, mine) : { ok: true, owner: false }; + if (!sell.ok) { + open(); + const why = sell.reason === "frozen" + ? `The owner of .${esc(tld)} has frozen it — no new names are being registered under it right now.` + : `.${esc(tld)} is a private TLD — its owner has switched it off for the public. Only the owner can register names under it right now. Check back later or pick another TLD.`; + render(`

${esc(name)} is not available

${why}

+
`); + $("back").onclick = close; + return; + } + const p = sp?.priceForName(label, tld); + state.ownerMint = !!sell.owner; + // 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 + // TLD owner is the buyer (no self-payment output). + const ownerRate = (sats) => (sats == null ? null : BigInt(sats) / 10n); state.nameFeeSats = opts.serviceFeeSats != null ? BigInt(opts.serviceFeeSats) - : (p ? p.sats : null); + : (p ? (sell.owner ? ownerRate(p.sats) : p.sats) : null); open(); if (await adoptSessionWalletAsync()) stepFund(); else stepWallet(); @@ -844,6 +872,7 @@ async function stepConfirm() { render(`

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.
` : ""} ${rows}
The certificate will be minted to ${esc(state.wallet.address)} — your key, your name. diff --git a/portal.html b/portal.html index 9ec87be..eb929d8 100644 --- a/portal.html +++ b/portal.html @@ -54,6 +54,33 @@ letter-spacing:inherit;text-transform:inherit;transition:color .12s} .font-picker button:hover{color:var(--ink)} .font-picker button.active{background:var(--acid);color:var(--bg);font-weight:600} + /* Profile-section tabs — horizontal menu inside the signed-in section. + Sticky under the topnav so context stays visible on scroll. Panes are + just
siblings; JS toggles a `hidden` class. */ + .profile-tabs{position:sticky;top:52px;z-index:15;display:flex;gap:6px; + padding:12px 0;margin:0 0 1.4rem;background:rgba(5,8,16,.86); + backdrop-filter:blur(10px);border-bottom:1px solid var(--line); + flex-wrap:wrap;font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase} + .profile-tabs button{background:transparent;border:1px solid transparent;color:var(--mut); + padding:8px 14px;border-radius:999px;cursor:pointer;font-family:inherit; + font-size:inherit;letter-spacing:inherit;text-transform:inherit; + display:inline-flex;align-items:center;gap:8px;transition:all .12s} + .profile-tabs button:hover{color:var(--ink);background:rgba(255,255,255,.03);border-color:var(--line)} + .profile-tabs button.active{background:var(--acid);color:var(--bg);font-weight:600;border-color:var(--acid)} + .profile-tabs button .count{font-family:var(--mono);font-size:11px;padding:1px 7px;border-radius:999px; + background:rgba(255,255,255,.08);color:var(--ink);letter-spacing:.02em;text-transform:none;font-weight:500} + .profile-tabs button.active .count{background:rgba(5,8,16,.24);color:var(--bg)} + .pane{animation:paneIn .18s ease-out} + .pane[hidden]{display:none} + @keyframes paneIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}} + /* Overview stat grid */ + .stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-top:1rem} + .stat{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 20px} + .stat .lbl{font-family:var(--mono);font-size:10.5px;letter-spacing:.24em;text-transform:uppercase;color:var(--dim);margin-bottom:8px; + display:flex;align-items:center;gap:6px} + .stat .lbl:before{content:"";width:5px;height:5px;background:var(--acid);border-radius:50%} + .stat .val{font-family:var(--serif);font-size:32px;font-weight:600;color:var(--ink);letter-spacing:-.01em;line-height:1} + .stat .sub{color:var(--mut);font-size:12.5px;margin-top:4px} a{color:var(--acid)} .wrap{max-width:820px;margin:0 auto;padding:0 1.2rem} .topnav{position:sticky;top:0;z-index:20;display:flex;gap:2px;align-items:center;flex-wrap:wrap; @@ -104,7 +131,7 @@ .authbox h3{margin:0 0 .3rem;font-size:1rem} .authbox p{margin:0;color:var(--mut);font-size:13px} .authbox .card > .cta{margin-top:14px} - .name-row{display:grid;grid-template-columns:1fr auto auto auto;gap:10px;align-items:center; + .name-row{display:grid;grid-template-columns:1fr auto auto auto auto;gap:10px;align-items:center; padding:14px 16px;background:var(--panel2);border:1px solid var(--line);border-radius:12px;margin-top:8px} .name-row .n{font-family:ui-monospace,monospace;font-size:15px} .name-row .rec{color:var(--mut);font-family:ui-monospace,monospace;font-size:12px;grid-column:1/-1;margin-top:6px;word-break:break-all} @@ -171,41 +198,114 @@
@@ -245,6 +345,101 @@ + + + + + +
- + - + diff --git a/tld.html b/tld.html index fec83c2..1460e00 100644 --- a/tld.html +++ b/tld.html @@ -301,8 +301,8 @@ })(); - - + +