A TLD owner needed to run their namespace without the operator: set what every name under the TLD sells for, take the TLD off the public registry for a while, and still register names under it themselves. Both settings live on chain in the TLD's TUPD records (`price`, `hidden`) because changes are rare and every client already walks the TLD beacon. The gateway's /api/tlds now carries records, owner, price_usd and hidden_by per TLD; pricing.js quotes the owner price ahead of the length tiers and only trusts a TLD_BEACON-sourced list; the register flow refuses hidden and frozen TLDs for the public but lets the owner through at the platform share only (the 90% owner cut would be paid to themselves). The portal's TLDs tab loads real holdings, shows price and on/off state, and gives each TLD a one-click switch, a price/policy editor and an inline "register a name under .tld" form. Docs and the design table describe the two new records.
221 lines
9.3 KiB
JavaScript
221 lines
9.3 KiB
JavaScript
// Sirius.X pricing model — deterministic per-label cost so users can predict
|
|
// what a name or TLD will cost before they click Register. Public tiers only;
|
|
// 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.
|
|
|
|
(() => {
|
|
// Chipnet placeholder rate: mainnet swaps this for a real BCH/USD oracle.
|
|
// The numbers keep the demo transactions in the "few chipnet coins" range
|
|
// so a single faucet visit covers a name or a TLD.
|
|
const CHIPNET_SATS_PER_USD = 250_000;
|
|
|
|
const usdToSats = (usd) => BigInt(Math.max(0, Math.round(Number(usd) * CHIPNET_SATS_PER_USD)));
|
|
|
|
const cleanLabel = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
|
|
// ---------- 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"; }
|
|
else if (s.length === 3) { usd = 2.00; tier = "short-3"; }
|
|
else if (s.length <= 5) { usd = 1.00; tier = "standard"; }
|
|
else if (s.length <= 7) { usd = 0.50; tier = "long"; }
|
|
else if (s.length <= 16) { usd = 0.25; tier = "extended"; }
|
|
else { usd = 0.10; tier = "very-long"; }
|
|
// Modifiers.
|
|
if (/^\d+$/.test(s)) { usd = round2(usd * 0.5); tier += "+digits"; }
|
|
if (s.includes("-")) { usd = round2(usd * 0.7); tier += "+hyphen"; }
|
|
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"; }
|
|
else if (s.length === 3) { usd = 150.00; tier = "short-3"; }
|
|
else if (s.length <= 6) { usd = 100.00; tier = "standard"; }
|
|
else if (s.length <= 10) { usd = 50.00; tier = "long"; }
|
|
else { usd = 25.00; tier = "extended"; }
|
|
return { usd, tier, sats: usdToSats(usd) };
|
|
}
|
|
|
|
// Rounded USD → display string. Under $1 shows cents ($0.25); $1 and up
|
|
// shows whole dollars ($5) unless there's a fractional part.
|
|
function formatUsd(usd) {
|
|
const n = Number(usd || 0);
|
|
if (n >= 1 && n === Math.floor(n)) return `$${n}`;
|
|
return `$${n.toFixed(2)}`;
|
|
}
|
|
function formatSats(sats) {
|
|
return `${BigInt(sats || 0n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} sat`;
|
|
}
|
|
function round2(n) { return Math.round(Number(n) * 100) / 100; }
|
|
|
|
window.siriusPricing = {
|
|
priceForName,
|
|
priceForTld,
|
|
formatUsd,
|
|
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();
|
|
})();
|