ENS Domain Resolution System Development
ENS resolution is not just calling provider.resolveName(). For production systems, you need to account for caching, wildcard support, CCIP-Read (off-chain resolvers), and correct multi-chain address handling. Especially when resolution occurs not in a browser but in backend services or smart contracts.
Full Resolution Chain
A standard ENS lookup looks like:
- Name normalization by UTS-46 (lowercase + IDNA2008)
- Namehash computation
- Query to ENS Registry:
registry.resolver(namehash) - If resolver supports wildcard (
supportsInterface(ENSIP-10)) — use extended resolution - Query to Resolver:
resolver.addr(namehash, coinType) - If resolver returns
OffchainLookup(EIP-3668) — perform CCIP-Read - Final address
Most libraries (ethers.js, viem) handle steps 1-5. CCIP-Read is supported in ethers v6 and viem natively.
Custom Resolution Service
For high-load applications, it's worth implementing a custom resolution service with cache:
import { createPublicClient, http, normalize } from "viem";
import { mainnet } from "viem/chains";
import NodeCache from "node-cache";
class ENSResolutionService {
private client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
private cache = new NodeCache({ stdTTL: 300 }); // 5 minutes TTL
async resolveName(name: string): Promise<string | null> {
const normalized = normalize(name);
const cacheKey = `addr:${normalized}`;
const cached = this.cache.get<string>(cacheKey);
if (cached !== undefined) return cached;
try {
const address = await this.client.getEnsAddress({ name: normalized });
this.cache.set(cacheKey, address ?? null);
return address;
} catch (e) {
return null;
}
}
async lookupAddress(address: `0x${string}`): Promise<string | null> {
const cacheKey = `name:${address.toLowerCase()}`;
const cached = this.cache.get<string>(cacheKey);
if (cached !== undefined) return cached;
const name = await this.client.getEnsName({ address });
this.cache.set(cacheKey, name ?? null);
return name;
}
// Batch resolution for list of addresses
async batchLookup(addresses: `0x${string}`[]): Promise<Map<string, string | null>> {
const results = new Map<string, string | null>();
const uncached: `0x${string}`[] = [];
for (const addr of addresses) {
const cached = this.cache.get<string>(`name:${addr.toLowerCase()}`);
if (cached !== undefined) {
results.set(addr, cached);
} else {
uncached.push(addr);
}
}
// Parallel resolution for uncached
await Promise.allSettled(
uncached.map(async (addr) => {
const name = await this.lookupAddress(addr);
results.set(addr, name);
})
);
return results;
}
}
Multi-chain Addresses (ENSIP-9)
ENS stores addresses for different networks via coinType (SLIP-44):
const COIN_TYPES = { ETH: 60, BTC: 0, SOL: 501, MATIC: 966, ARB: 9001 };
async function getMultiChainAddresses(name: string) {
const resolver = await provider.getResolver(name);
if (!resolver) return null;
return {
eth: await resolver.getAddress(COIN_TYPES.ETH),
btc: await resolver.getAddress(COIN_TYPES.BTC),
sol: await resolver.getAddress(COIN_TYPES.SOL),
};
}
Development timeline for a custom resolution service with caching and multi-chain support — 3-5 working days. Integration into existing backend — 1-2 days additional.







