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
This commit is contained in:
111
README.md
Normal file
111
README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user