import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import vm from 'node:vm'; /** * Exercises the BUILT BUNDLE, not the source. That distinction is the entire reason this file * exists: tr46 reaches for the Node `punycode` builtin, which is what turns a stored xn-- label * back into the emoji a user typed. If that polyfill is missing from snap.config.ts the bundle * still compiles, still passes `mm-snap build`, and still "evaluates successfully" - it just * silently fails to resolve every non-ASCII name. Only running the bundle catches that. * * Run `npm run build` before this. The provider is mocked, so no network and no real chain reads; * what is asserted is the exact calldata the snap would put on the wire. */ const BUNDLE = fileURLToPath(new URL('../dist/bundle.js', import.meta.url)); type Call = { to: string; data: string }; /** * The bundle is CommonJS while this package is type:module, so it cannot be require()d or * import()ed. Evaluating it in a fresh vm context sidesteps that and is closer to what the Snaps * runtime does anyway - and gives every test a clean module instance, so the snap's internal * resolution cache never leaks between cases. */ function evaluateBundle(ethereum: unknown) { const module_ = { exports: {} as Record }; const sandbox = { module: module_, exports: module_.exports, ethereum, console, TextEncoder, TextDecoder, URL, crypto: globalThis.crypto, setTimeout, clearTimeout, }; vm.runInNewContext(readFileSync(BUNDLE, 'utf8'), vm.createContext(sandbox), { filename: 'bundle.js', }); return module_.exports; } /** Stands in for MetaMask's injected provider, recording calldata and replying with ABI words. */ function mockProvider(replies: Record) { const calls: Call[] = []; const ethereum = { async request({ method, params }: { method: string; params: unknown[] }) { assert.equal(method, 'eth_call'); const [tx] = params as [Call, string]; calls.push({ to: tx.to.toLowerCase(), data: tx.data.toLowerCase() }); return replies[tx.data.toLowerCase()] ?? null; }, }; return { ethereum, calls }; } const word = (addr: string) => '0x' + '0'.repeat(24) + addr.replace(/^0x/u, '').toLowerCase(); const REGISTRY = '0x667ab1d9f98817ffb28cd61b911f921181c669b3'; const RESOLVER = '0xbc621963e531b0980aa754bd86bdde611b82be9c'; const HOLDER = '0xf1f2ddeb9a90f42499ac68109d0b10bed33a1cd7'; // dsld_node("e.xp") and dsld_node("xn--qei.xp"), from the production registry. const NODE_E = '1c757df1d88d9dce1e45729d70449b42c6f99a0e0475155e676e68cb91783b55'; const NODE_HEART = '61c4c02197848e39f678d802cfea098b6cf874c1682022fab3e7df523c028059'; function loadSnap(replies: Record) { const { ethereum, calls } = mockProvider(replies); return { snap: evaluateBundle(ethereum), calls }; } function repliesFor(nodeHex: string) { return { [`0x0178b8bf${nodeHex}`]: word(RESOLVER), // resolver(bytes32) [`0x3b3b57de${nodeHex}`]: word(HOLDER), // addr(bytes32) }; } test('resolves an ASCII name through the bundle', async () => { const { snap, calls } = loadSnap(repliesFor(NODE_E)); const result = await snap.onNameLookup({ chainId: 'eip155:1', domain: 'e.xp' }); // Compared field-by-field, not with deepEqual: the result is built inside the vm context, so its // prototype is that realm's Object and a strict structural compare rejects it as non-identical. assert.deepEqual(JSON.parse(JSON.stringify(result)), { resolvedAddresses: [{ resolvedAddress: HOLDER, protocol: 'Namebase', domainName: 'e.xp' }], }); assert.equal(calls[0].to, REGISTRY, 'first call goes to the registry'); assert.equal(calls[0].data, `0x0178b8bf${NODE_E}`, 'resolver(bytes32) with the right node'); assert.equal(calls[1].to, RESOLVER, 'second call goes to the resolver the registry named'); }); /** The polyfill test. A typed, emoji-qualified name must produce the stored label's node. */ test('resolves a typed emoji name - proves the punycode polyfill is present', async () => { const { snap, calls } = loadSnap(repliesFor(NODE_HEART)); const result = await snap.onNameLookup({ chainId: 'eip155:1', domain: '❤️.xp' }); assert.equal(result?.resolvedAddresses[0].resolvedAddress, HOLDER); assert.equal( calls[0].data, `0x0178b8bf${NODE_HEART}`, 'U+FE0F must be stripped and the label punycode-folded, or this is a different node', ); }); test('an unminted name resolves to nothing rather than the zero address', async () => { const zero = '0x' + '0'.repeat(64); const { snap } = loadSnap({ [`0x0178b8bf${NODE_E}`]: word(RESOLVER), [`0x3b3b57de${NODE_E}`]: zero, }); assert.equal(await snap.onNameLookup({ chainId: 'eip155:1', domain: 'e.xp' }), null); }); test('a provider error is "unknown", not a bad name', async () => { const snap = evaluateBundle({ async request() { throw new Error('rpc exploded'); }, }); assert.equal(await snap.onNameLookup({ chainId: 'eip155:1', domain: 'e.xp' }), null); }); test('other chains are refused even though the manifest already restricts them', async () => { const { snap, calls } = loadSnap(repliesFor(NODE_E)); assert.equal(await snap.onNameLookup({ chainId: 'eip155:10', domain: 'e.xp' }), null); assert.equal(calls.length, 0, 'and it must not have called out at all'); }); test('a reverse lookup (address, no domain) is declined', async () => { const { snap } = loadSnap({}); assert.equal(await snap.onNameLookup({ chainId: 'eip155:1', address: HOLDER }), null); }); /** * The manifest carries no matchers.tlds, so MetaMask routes EVERY domain here and these rules are * the whole filter. */ test('.eth is refused without touching the chain', async () => { const { snap, calls } = loadSnap({}); for (const name of ['vitalik.eth', 'VITALIK.ETH', 'a.b.eth', 'vitalik.eth.']) { assert.equal(await snap.onNameLookup({ chainId: 'eip155:1', domain: name }), null, name); } assert.equal(calls.length, 0, 'and it must not have called out at all'); }); test('a bare TLD and malformed names are refused before any chain read', async () => { const { snap, calls } = loadSnap({}); for (const name of ['xp', 'xp.', '', ' ', '.xp', '.e.xp', 'a..xp']) { assert.equal(await snap.onNameLookup({ chainId: 'eip155:1', domain: name }), null, JSON.stringify(name)); } assert.equal(calls.length, 0); }); test('ordinary names still go through now that the TLD list is gone', async () => { const { snap, calls } = loadSnap(repliesFor(NODE_E)); const result = await snap.onNameLookup({ chainId: 'eip155:1', domain: 'e.xp' }); assert.equal(result?.resolvedAddresses[0].resolvedAddress, HOLDER); assert.equal(calls.length, 2, 'registry then resolver'); }); /** * A resolver that answers with the zero address is worse than one that stays quiet: anything sent * there is destroyed. When we do not know, we answer null and let MetaMask's own resolvers reply. */ test('never resolves to the zero address, and never falls back to ENS', async () => { const zero = '0x' + '0'.repeat(64); const noResolver = loadSnap({ [`0x0178b8bf${NODE_E}`]: zero }); assert.equal(await noResolver.snap.onNameLookup({ chainId: 'eip155:1', domain: 'e.xp' }), null, 'a zero resolver is not a resolution'); const noAddr = loadSnap({ [`0x0178b8bf${NODE_E}`]: word(RESOLVER), [`0x3b3b57de${NODE_E}`]: zero }); assert.equal(await noAddr.snap.onNameLookup({ chainId: 'eip155:1', domain: 'e.xp' }), null, 'a zero address is not a resolution'); const empty = loadSnap({}); assert.equal(await empty.snap.onNameLookup({ chainId: 'eip155:1', domain: 'e.xp' }), null, 'an empty reply is not a resolution'); }); /** * onNameLookup fires per keystroke, so every prefix that happens to look like a name leaves a cache * entry and nothing expires them on its own. Unbounded, that grows for as long as the snap lives. */ test('the resolution cache is bounded and still serves recent names', async () => { const replies: Record = {}; const { snap, calls } = loadSnap(replies); // Far more distinct names than the cache holds. Each misses, so each is one registry call. for (let i = 0; i < 400; i++) { await snap.onNameLookup({ chainId: 'eip155:1', domain: `n${i}.xp` }); } const afterFill = calls.length; assert.equal(afterFill, 400, 'one registry call per distinct name, all misses'); // The most recent name is still cached: asking again must not hit the chain. await snap.onNameLookup({ chainId: 'eip155:1', domain: 'n399.xp' }); assert.equal(calls.length, afterFill, 'a recent name is served from cache'); // The oldest was evicted rather than retained forever, so it costs a lookup again. await snap.onNameLookup({ chainId: 'eip155:1', domain: 'n0.xp' }); assert.equal(calls.length, afterFill + 1, 'the oldest entry was evicted'); }); test('trailing dots share one cache entry', async () => { const { snap, calls } = loadSnap(repliesFor(NODE_E)); await snap.onNameLookup({ chainId: 'eip155:1', domain: 'e.xp' }); const after = calls.length; await snap.onNameLookup({ chainId: 'eip155:1', domain: 'e.xp.' }); assert.equal(calls.length, after, '"e.xp." must not pay for the same lookup twice'); });