# Namebase Domains - MetaMask Snap Type `alice.xp` into MetaMask's send field and have it resolve to the address its holder is paid at. Namebase decentralized domains are Handshake names minted onto Ethereum as non-expiring ERC-721s. MetaMask cannot resolve them on its own: it asks **the** ENS registry at `0x0000...2e1e`, which has never heard of them. This snap knows where to look instead. ## It talks to no server Whatever this snap returns is where the user's money goes. Resolution is two `eth_call`s through the user's own MetaMask provider: ``` registry.resolver(namehash(name)) -> the resolver for that name resolver.addr(node) -> the address to pay ``` Nothing of Namebase's is in that path, so nobody at Namebase can redirect a payment and resolution keeps working whatever happens to our infrastructure. The snap needs no network permission for the same reason. `addr()` falls back to whoever currently holds the token, so a name keeps paying the right person after a sale with no transaction from anyone. ## There is no TLD allowlist The manifest declares chains only, so every domain in the send field reaches the handler. Two rules in `src/index.ts` decide what to answer for: - **`DENIED_TLDS`** - just `.eth`. Names are keyed by namehash alone, so nothing about a node says which naming system ought to own it. Where another system is already authoritative for a TLD, this snap stays out of it rather than offering a second answer in the send field. Kept deliberately small: a Handshake TLD may legitimately share a string with some other naming system, and refusing those would break names we are supposed to serve. - **At least two labels.** A bare TLD is not something anyone sends funds to. Both run before any chain read. Everything else is left to registry policy, which will not sell a mint under a TLD it does not host. Keeping the list here rather than in `matchers.tlds` means enabling minting on a new TLD needs no change to this snap at all. **When we do not know, we answer `null`** and let MetaMask's own resolvers reply. A resolver that answers with the zero address is worse than one that stays quiet: anything sent there is destroyed. ## Hashing has to match the registry exactly The node is an ERC-721 token id. Getting it wrong does not fail loudly - it computes a different, perfectly valid-looking node with no records, and the name simply reports as unresolvable. `src/namehash.ts` mirrors `dsld_node()` in the Namebase registry, and every expected value in `test/namehash.test.ts` is generated from it rather than computed here. Two requirements are pinned by tests, because breaking either is silent: - **UTS-46 must be transitional.** `straße` folds to `strasse` and a final sigma folds to a medial one. Nontransitional UTS-46 - which is `tr46`'s default - keeps them apart and hashes to nodes the contract has never heard of. - **UTS-46, not ENSIP-15.** `@adraffy/ens-normalize` is the obvious library to reach for here and is the wrong one: it normalizes differently and preserves the emoji variation selectors UTS-46 strips. These tokens were minted under UTS-46. A user types the name as it is *displayed* - emoji-qualified, unicode - while the registry stores punycode. `namehash('❤️.xp')`, `namehash('❤.xp')` and `namehash('xn--qei.xp')` all reach one node. ## Build and test ```bash npm install npm run build # mm-snap build -> dist/bundle.js npm test # 23 tests, source and bundle ``` `test/bundle.test.ts` runs the **built bundle** in a vm with a mocked provider. That is not redundant with the source tests: `tr46` reaches for the Node `punycode` builtin, and without the polyfill in `snap.config.ts` the bundle still compiles, still passes `mm-snap build`, and still reports "evaluated successfully" - it just fails to resolve every non-ASCII name at runtime. ## Local development ```bash npm run serve # the snap, on http://localhost:8080 npm run site # the install page, on http://localhost:8000 ``` Open http://localhost:8000 in MetaMask Flask and click Install, then type a `.xp` name into the send field. Local snaps require Flask; the stable extension installs allowlisted snaps only. Manifest changes (name, icon, permissions) need a remove-and-reinstall in Flask to take effect; code changes only need `npm run build` and a reconnect. ## Deployed contracts | | | | --- | --- | | NamebaseRegistry | `0x667AB1d9F98817ffb28cD61b911F921181C669b3` | | NamebaseResolver | `0xbc621963e531b0980aA754bD86bDDE611b82bE9c` | | NamebaseMinter | `0x890C43092AfF84Dc07074c28bA158543c6d50cAE` | | chain | Ethereum mainnet (`eip155:1`) | The registry address is also what each TLD publishes on Handshake as its HIP-5 record (`NS 0x667ab1d9...._eth.`), which is what makes it effectively permanent - changing it would mean every TLD owner republishing an on-chain record. ## Scope **Domain to address only.** Reverse lookup needs an address-to-name index, which neither the registry nor the minter keeps. **Not DNS.** This resolves payment addresses, not websites. Reaching a minted site in a browser needs a HIP-5-aware DNS resolver, which is a separate problem.