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
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_calls 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ßefolds tostrasseand a final sigma folds to a medial one. Nontransitional UTS-46 - which istr46's default - keeps them apart and hashes to nodes the contract has never heard of. - UTS-46, not ENSIP-15.
@adraffy/ens-normalizeis 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
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
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.