Files
metamask/README.md
eskimo e90426ff43 Resolve Namebase decentralized domains in MetaMask
Namebase domains are Handshake names minted onto Ethereum as non-expiring
ERC-721s. MetaMask cannot resolve them on its own: it asks the ENS registry,
which has never heard of them. This snap knows where to look instead.

It talks to no server. Whatever a name resolves to is where the user's money
goes, so it is read from the chain through the user's own provider - which is
also why no network permission is needed. addr() falls back to whoever holds
the token, so a name keeps paying the right person after a sale.

The manifest declares chains only and carries no TLD list, so enabling minting
on a new TLD needs no change here. Two rules in src/index.ts filter instead:
stay out of TLDs another naming system is authoritative for, and ignore
anything without two labels. When in doubt the answer is null, never an
address.

Hashing mirrors the registry's dsld_node() and every test vector is generated
from it. Two requirements are pinned because breaking either is silent: UTS-46
must be transitional, and it must not be ENSIP-15. Either mistake yields a
valid-looking node with no records and a name that never resolves. The built
bundle is tested separately from the source because tr46 reaches for the Node
punycode builtin - without that polyfill the build is clean and every
non-ASCII name fails at runtime.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUVyawKbjSWw57md791LUu
2026-08-09 15:05:18 -04:00

112 lines
5.1 KiB
Markdown

# 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.