Two gaps the owner panel left open. First, a hidden TLD was only a UI gate: anyone could still broadcast a REG under it and every indexer admitted it. Second, there was no way to sell a name without trusting the other side. Co-sign rule (consensus, applied in lockstep by bns.js and resolver-web.js): a REG under a TLD whose records at that height say policy "cosign" or hidden 1 is indexed only if the transaction carries the TLD's own certificate. The certificate can only be spent by the owner's key and is re-issued to them in the same transaction, so it is a co-signature nobody can forge and nothing is consumed. The TLD map now keeps the TUPD timeline so policy is evaluated at the REG height. Owners register under their private TLDs with the certificate added from their own wallet; third parties under a "cosign" TLD build the full transaction, sign their inputs and queue it at /api/cosign, where the owner approves it from the dashboard (signCosignRequest refuses to sign unless the certificate returns to the same locking script). Marketplace: a listing is the seller's certificate input plus a price output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a bulletin board (/api/market, verified against the on-chain owner and pruned when the certificate moves). The buyer completes it in one transaction, so the seller is paid exactly when the name moves. Cancelling also spends the certificate once so the offer is void. Site: market.html, Sell sub-tab and Pending approvals in the portal, Market link in nav and footer, six dictionaries extended, cache tags bumped. Verified on chipnet: cosigned.sc registered by a throwaway wallet through the queue with the .sc certificate back at the owner; aloevera.test listed and delisted through the API. Ariadne's resolver-web.js copy and the mobile Bns.java port still need the co-sign rule; until then they admit REGs this index rejects.
230 lines
9.9 KiB
JavaScript
230 lines
9.9 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",
|
|
// 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,
|
|
};
|
|
}
|
|
|
|
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 };
|
|
// 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 };
|
|
}
|
|
|
|
// 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();
|
|
})();
|