Repository navigation
feat(names): put the Veil Names rules behind an HTTP route - #353
Open
Miracle656 wants to merge 5 commits into
Open
Miracle656 wants to merge 5 commits into
Miracle656 wants to merge 5 commits into
Conversation
Length pricing, shortest dearest: 3 characters 10 USDC, 4 characters 5 USDC, 5 and over a flat 2 USDC. The validation is the load-bearing half. A name resolves over SEP-2 to an address somebody sends funds to, so two strings a human reads as the same name must not be two names: case is folded, and non-ASCII is refused rather than folded, because "jack" with a Cyrillic a is a payment redirect and not a typo. One- and two-character names are withheld rather than priced — no contract here is upgradeable, so a name sold is sold permanently, and holding the scarcest 1,332 of them costs nothing. Reserved names are refused with hyphens stripped too: without that, s-u-p-p-o-r-t is a 13-character name at the cheapest tier that reads as support in every screen that renders it. Pure module — no database, no payment, no issuance. The claim flow, the KYC gate and the mint call into this. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Replaces the per-length table. The curve is deliberately not monotonic: the dearest band is the middle one, because real first names are six to ten characters and that is where the asking will be. It prices demand, not scarcity, which is the opposite of the usual registry shape — so the tests pin the inversion and the comment says not to "fix" it. One- and two-character names are priced by the 1-5 band but stay unsellable: 1,332 of them exist, nothing here is upgradeable, and withholding is reversible where selling is not. MIN_SELLABLE_LENGTH is the one line that opens them. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
The Veil Names design tells the user "3 to 20 characters: lowercase letters, numbers and underscores" in the field's own help text, so those are the rules a client will already have promised by the time anything reaches this module. Max length drops 32 to 20 and the separator moves from hyphen to underscore. Underscore is now the ONLY separator. Allowing both would make jack_ng and jack-ng two different names nobody can tell apart read aloud, which is the same confusable problem the charset rule exists to prevent. The reserved check strips underscores for the same reason it stripped hyphens: s_u_p_p_o_r_t poses as support just as well in a screen that renders it, and the design promises that names posing as Veil or its team are reserved. Prices are unchanged and still the ones given in chat, which disagree with the design's own table. Flagged, not silently reconciled. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Names are rented by the year, so the prices in pricing.ts become annual rates and priceRegistration is the one place a multi-year total is computed — a caller multiplying the rate itself is a caller that can get the term wrong. Lifecycle in term.ts. Resolution stops at the instant of expiry with no grace, as asked: a name that still resolves after its term is a name whose holder believes it is working. The part that was my call is the 90-day recovery window after expiry. Rental creates a hazard permanent ownership did not — a lapsed name taken by somebody else keeps working in every contact list that has it and starts paying a stranger. During the window the name is already dead to senders, but only the previous holder may re-register, which buys two things at once: someone who forgot a 3 USDC renewal gets their name back instead of losing it to whoever was watching, and every stale contact fails for a full quarter before a repoint is even possible. Renewal is open to anyone, also my call. It only ever extends a name for its current holder and cannot move or re-point it, so the worst a stranger can do is pay your bill — and refusing that would mean a name lapsing because its holder lost their phone in the wrong week. A year is a flat 365 days, not a calendar year: the contract, the server and the client must compute one expiry, and "a year from 29 February" has no single answer. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
`src/names/pricing.ts` and `src/names/term.ts` have held the naming rules —
what a name may be, what it costs, how a term and its expiry work — with no
route to reach them. The wallet's /names page calls
`GET {WRAITH_URL}/names/{name}` and got a 404 from a server that already knew
the answer. The modules also sat on a branch mixed with unrelated retention,
offramp and metrics work; this lands just the four names commits.
Three routes:
- `GET /names/price-table` — the bands, the term bounds, the recovery window.
Answerable from the rules alone.
- `GET /names/{name}` — `reserved` and the refusal reasons, again from the
rules. Whether a *valid* name is taken needs the registry contract.
- `GET /names/{name}/quote?years=N` — the total. `priceRegistration` validates
the term rather than trusting it, and is the only place a total is computed:
a caller that multiplies a per-year price by its own term is a caller that
can get the term wrong.
The split that matters is between what these rules can answer and what needs a
ledger. With no registry deployed, a valid name returns **503 with
`status: "unknown"`, never a 404**. A 404 reads as "nobody has this name",
which is the false "available" that would let the page sell something nobody
can deliver — and "available" is the one answer that costs somebody money if
it is wrong. The wallet already renders any non-OK reply as *Not live yet* and
refuses to offer a claim, so this is handled end to end on both sides.
`VEIL_NAMES_CONTRACT_ID` has deliberately no default, because a placeholder id
would make every answer sound authoritative about a contract that does not
exist.
Three CI guards caught real omissions rather than noise, and all three are now
satisfied: `openapiCoverage` (every mounted route must appear in the spec, so
all three paths are registered and `openapi.json`, `docs/openapi.json` and
`clients/react-query/src/schema.d.ts` are regenerated), `envValidation` (every
env var read by source must be in `.env.example`), and `dockerCompose` (every
key `.env.example` documents must be declared in `docker-compose.yml`).
`npx tsc --noEmit` clean; full suite 90 suites / 1409 tests passed.
Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
src/names/pricing.tsandsrc/names/term.tshave held the naming rules —what a name may be, what it costs, how a term and its expiry work — with no
route to reach them. The wallet's
/namespage callsGET {WRAITH_URL}/names/{name}and got a 404 from a server that already knewthe answer.
Those modules also sat on
feat/veil-names-pricingmixed with unrelatedretention, offramp, metrics and CI work. This branch cherry-picks only the four
names commits onto
main, so none of that rides along.Routes
GET /names/price-tableGET /names/{name}reservedand the refusal reasonsGET /names/{name}/quote?years=NThe split that matters
What these rules can answer, versus what needs a ledger.
With no registry deployed, a valid name returns 503 with
status: "unknown", never a 404. A 404 reads as nobody has this name —the false "available" that would let the page sell something nobody can
deliver. "Available" is the one answer that costs somebody money if it is
wrong.
The wallet already renders any non-OK reply as Not live yet and refuses to
offer a claim, so this is handled end to end on both sides.
VEIL_NAMES_CONTRACT_IDdeliberately has no default — a placeholder id wouldmake every answer sound authoritative about a contract that does not exist.
Three CI guards caught real omissions
All three now satisfied, and each one was a genuine gap rather than noise:
openapiCoverage— every mounted route must appear in the spec. Allthree paths registered;
openapi.json,docs/openapi.jsonandclients/react-query/src/schema.d.tsregenerated.envValidation— every env var read by source must be in.env.example.dockerCompose— every key.env.exampledocuments must be declared indocker-compose.yml.Verification
npx tsc --noEmit— cleansrc/api/names.ts— 100% statements, 100% functions, 90.9% branchesRelated
The registry contract landed in Miracle656/veil#1328 and is now deployed to
testnet at
CBR5Z7JN6IGNQNN3IJQMKFMVRESNGQCD2WO2BZPBRTYYRFR2GRHLZXVM,where register, resolve, payment transfer and the taken/reserved refusals are
all verified on-chain.
https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB