#
ZilNames: Zilliqa Name Service Documentation
#
Introduction to ZilNames
ZilNames is Zilliqa's domain name service, offering human-readable names with the .zil
extension. Similar to Ethereum's ENS, ZilNames allows users to replace complex wallet addresses with memorable names, making transactions and identification within the Zilliqa ecosystem simpler and less error-prone.
Features include:
- Human-readable names (e.g.,
myname.zil
) - Reverse resolution (wallet address to name)
- Profile avatars
- Cross-application compatibility in the Zilliqa ecosystem
#
Contract Details
The main contract used for Zilnames resolution is the UniversalResolver. This is the most simplistic and also works out of the box with Viem.
The other advanced option is using the L2Resolver contract but this is more advanced and manual.
Contracts for the full set (only need to use these if you want to setup new addresses) Alternatively you can direct users to https://zilnames.com/
#
Testnet contracts
#
Mainnet contracts
#
ABIs
Here is links to the ABIs used for Zilnames
BaseRegistrar.abi
L2Resolver.abi
RegistrarController.abi
Registry.abi
ReverseRegistrar.abi
UniversalResolver.abi
#
OPTION 1 - ZilNames Integration using Viem
Viem.sh now supports ZilNames out-of-the-box thanks to the UniversalResolver contract. This is the simplest way to integrate ZilNames into your dApp.
First, define the Zilliqa chain configuration in your Viem setup:
import { defineChain } from 'viem'
export const zilliqa = defineChain({
id: 32769,
name: "Zilliqa",
iconUrl: "https://plunderswap.com/images/chains/33101.png",
nativeCurrency: { name: "Zilliqa", symbol: "ZIL", decimals: 18 },
rpcUrls: {
default: { http: ["https://api.zilliqa.com/"] },
},
blockExplorers: {
default: { name: "Otterscan", url: "https://otterscan.zilliqa.com" },
},
contracts: {
ensUniversalResolver: {
address: "0x2b6a7953c510392aE88c7C302a984460Daa8AF24",
},
ensRegistry: {
address: "0x2196b67Ca97bBcA07C01c7Bdf4f35209CC615389",
},
multicall3: {
address: "0x38899efb93d5106d3adb86662c557f237f6ecf57",
blockCreated: 5313022,
},
},
});
Once your client is configured with the Zilliqa chain, you can use Viem's ENS actions. Remember to normalize ENS names using normalize
from viem/ens
before passing them to these functions.
#
1. Forward Resolution (Name → Address)
Use getEnsAddress
to resolve a .zil
name to an address.
import { normalize } from 'viem/ens'
import { createPublicClient, http } from 'viem'
import { zilliqa } from './chains' // Assuming your chain definition is in chains.ts
const publicClient = createPublicClient({
chain: zilliqa,
transport: http(),
})
async function resolveZilNameToAddress(name: string) {
const ensAddress = await publicClient.getEnsAddress({
name: normalize(name), // e.g., normalize('example.zil')
});
return ensAddress;
}
For more details, see the Viem getEnsAddress documentation.
#
2. Reverse Resolution (Address → Name)
Use getEnsName
to resolve an address to its primary .zil
name.
import { createPublicClient, http } from 'viem'
import { zilliqa } from './chains'
const publicClient = createPublicClient({
chain: zilliqa,
transport: http(),
})
async function resolveAddressToZilName(address: `0x${string}`) {
const ensName = await publicClient.getEnsName({
address: address,
});
return ensName;
}
For more details, see the Viem getEnsName documentation.
#
3. Avatar Resolution
Use getEnsAvatar
to get the avatar URL for a .zil
name.
import { normalize } from 'viem/ens'
import { createPublicClient, http } from 'viem'
import { zilliqa } from './chains'
const publicClient = createPublicClient({
chain: zilliqa,
transport: http(),
})
async function getZilNameAvatar(name: string) {
const avatarUrl = await publicClient.getEnsAvatar({
name: normalize(name),
});
return avatarUrl;
}
For more details, see the Viem getEnsAvatar documentation.
#
4. Resolver Address
Use getEnsResolver
to get the resolver address for a .zil
name.
import { normalize } from 'viem/ens'
import { createPublicClient, http } from 'viem'
import { zilliqa } from './chains'
const publicClient = createPublicClient({
chain: zilliqa,
transport: http(),
})
async function getZilNameResolver(name: string) {
const resolverAddress = await publicClient.getEnsResolver({
name: normalize(name),
});
return resolverAddress;
}
For more details, see the Viem getEnsResolver documentation.
#
5. Text Records
Use getEnsText
to retrieve arbitrary text records associated with a .zil
name.
import { normalize } from 'viem/ens'
import { createPublicClient, http } from 'viem'
import { zilliqa } from './chains'
const publicClient = createPublicClient({
chain: zilliqa,
transport: http(),
})
async function getZilNameTextRecord(name: string, key: string) {
const textRecord = await publicClient.getEnsText({
name: normalize(name),
key: key,
});
return textRecord;
}
For more details, see the Viem getEnsText documentation.
#
OPTION 2 - ZilNames Integration using UniversalResolver Contract
This option demonstrates how to directly interact with the UniversalResolver
contract for more fine-grained control or when not using Viem's built-in ENS functions. You will need the ABI for the UniversalResolver
contract. (Linked above)
Refer to the ENS Universal Resolver Documentation for detailed information on the contract's functions and how to encode data.
The UniversalResolver
contract address on Zilliqa Mainnet is 0x2b6a7953c510392aE88c7C302a984460Daa8AF24
. You can find the ABI linked in the
Here's a conceptual Next.js example using wagmi
and viem
for contract interaction:
#
1. Setup
Ensure you have wagmi
and viem
installed. You'll also need the UniversalResolver.abi
.
// lib/universalResolver.ts
import { Address, encodeFunctionData, parseAbi, toHex } from 'viem';
import { namehash, normalize, packetToBytes } from 'viem/ens'; // For name normalization, hashing, and DNS encoding
// Assuming UniversalResolver.abi is available in your project
// You'll need to load the ABI content, e.g., by importing it as a JSON module or reading the file content.
// const UniversalResolverABI = await import('../abis/UniversalResolver.abi.json'); // Example if using JSON module
// For this example, we'll assume the ABI string is loaded into a variable.
// The actual ABI content should be sourced from the 'UniversalResolver.abi' file linked in the 'ABIs' section of this document.
const UNIVERSAL_RESOLVER_ABI_JSON_STRING = '[...]'; // Placeholder: Replace with actual JSON string content of UniversalResolver.abi
const universalResolverAddress = "0x2b6a7953c510392aE88c7C302a984460Daa8AF24" as Address;
const universalResolverAbi = parseAbi(UNIVERSAL_RESOLVER_ABI_JSON_STRING);
// Viem client (example setup)
// import { createPublicClient, http } from 'viem';
// import { zilliqa } from './chains'; // Your chain definition
// const client = createPublicClient({ chain: zilliqa, transport: http() });
// Helper function to interact with the UniversalResolver
async function callUniversalResolver(client: any, functionName: string, args: any[]) {
try {
const data = await client.readContract({
address: universalResolverAddress,
abi: universalResolverAbi,
functionName,
args,
});
return data;
} catch (error) {
console.error(`Error calling ${functionName}:`, error);
throw error; // Re-throw to allow caller to handle
}
}
export async function resolveNameWithUniversalResolver(client: any, name: string): Promise<[`0x${string}`, Address] | null> {
const normalizedName = normalize(name); // UTS-46 normalization
const dnsEncodedName = toHex(packetToBytes(normalizedName)); // DNS encode the name
// Construct the call data for the target resolver's `addr(bytes32)` function.
// This is what we want the UniversalResolver to call on the actual resolver for the name.
const node = namehash(normalizedName);
const addrFunctionSignature = 'addr(bytes32)'; // Standard ENS resolver function
// For simplicity, assuming a basic ABI for the target resolver's addr function.
// In a real scenario, you might need a more complete resolver ABI if dealing with various resolver types.
const targetResolverAddrAbi = parseAbi([`function ${addrFunctionSignature} view returns (address)`]);
const callDataToForward = encodeFunctionData({
abi: targetResolverAddrAbi,
functionName: 'addr',
args: [node]
});
try {
// Call the `resolve` function on the UniversalResolver
const result = await callUniversalResolver(client, 'resolve', [dnsEncodedName, callDataToForward]) as [`0x${string}`, Address];
// result[0] is the ABI-encoded address from the target resolver
// result[1] is the address of the target resolver
return result;
} catch (e) {
console.error(`Error in resolveNameWithUniversalResolver for ${name}:`, e);
return null;
}
}
export async function reverseResolveAddressWithUniversalResolver(client: any, address: Address): Promise<[`0x${string}`, Address] | null> {
// Construct the reverse node string, e.g., "d2135cfb216b74109775236e36d4b433f1df507b.addr.reverse"
const reverseNodeString = `${address.slice(2).toLowerCase()}.addr.reverse`;
const dnsEncodedReverseNode = toHex(packetToBytes(reverseNodeString)); // DNS encode the reverse node string
// Construct the call data for the target resolver's `name(bytes32)` function.
const node = namehash(reverseNodeString);
const nameFunctionSignature = 'name(bytes32)'; // Standard ENS resolver function
const targetResolverNameAbi = parseAbi([`function ${nameFunctionSignature} view returns (string)`]);
const callDataToForward = encodeFunctionData({
abi: targetResolverNameAbi,
functionName: 'name',
args: [node]
});
try {
// Call the `resolve` function on the UniversalResolver
const result = await callUniversalResolver(client, 'resolve', [dnsEncodedReverseNode, callDataToForward]) as [`0x${string}`, Address];
// result[0] is the ABI-encoded name string from the target resolver
// result[1] is the address of the target resolver
return result;
} catch (e) {
console.error(`Error in reverseResolveAddressWithUniversalResolver for ${address}:`, e);
return null;
}
}
// Note on UNIVERSAL_RESOLVER_ABI_JSON_STRING:
// This placeholder should be replaced with the actual JSON string content of the UniversalResolver.abi file.
// You can typically achieve this by importing the .abi file as a JSON module if your bundler supports it,
// or by reading the file content and parsing it.
// Example (conceptual, depends on your environment):
// import abiFileContent from '../abis/UniversalResolver.abi.json';
// const UNIVERSAL_RESOLVER_ABI_JSON_STRING = JSON.stringify(abiFileContent);
Important Note on callDataToForward
: The data
parameter (which we've named callDataToForward
for clarity) for the Universal Resolver's resolve
function is crucial. It is the ABI-encoded function call that you intend to execute on the actual name's resolver contract, not on the Universal Resolver itself. The Universal Resolver acts as a gateway, forwarding this call to the appropriate resolver for the given name.
- For forward resolution (name to address), this is typically an encoded call to
addr(bytes32 node)
oraddr(bytes32 node, uint256 coinType)
. - For reverse resolution (address to name), this is typically an encoded call to
name(bytes32 node)
. - The Universal Resolver's
resolve
function returns(bytes result, address resolverAddress)
. You will then need to decoderesult
using the ABI of the function you originally encoded intocallDataToForward
(e.g., decoderesult
as anaddress
if you calledaddr
, or as astring
if you calledname
).
#
2. React Component Example
// components/ZilNamesUniversalResolver.tsx
import { useState } from 'react';
import { usePublicClient } from 'wagmi';
import { zilliqa } from '@/lib/chains'; // Your chain definition
import { resolveNameWithUniversalResolver, reverseResolveAddressWithUniversalResolver } from '@/lib/universalResolver'; // Adjust path
import { Address, isAddress, decodeAbiParameters, parseAbiParameters } from 'viem';
export default function ZilNamesUniversalResolver() {
const publicClient = usePublicClient({ chainId: zilliqa.id }); // Ensure this client is configured for Zilliqa
const [nameInput, setNameInput] = useState('');
const [addressInput, setAddressInput] = useState('');
const [resolvedForwardAddress, setResolvedForwardAddress] = useState<Address | null>(null);
const [forwardResolverAddress, setForwardResolverAddress] = useState<Address | null>(null);
const [forwardError, setForwardError] = useState<string | null>(null);
const [resolvedReverseName, setResolvedReverseName] = useState<string | null>(null);
const [reverseResolverAddress, setReverseResolverAddress] = useState<Address | null>(null);
const [reverseError, setReverseError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const handleResolveName = async () => {
if (!publicClient || !nameInput) {
setForwardError("Please connect wallet and enter a name.");
return;
}
setIsLoading(true);
setResolvedForwardAddress(null);
setForwardResolverAddress(null);
setForwardError(null);
try {
const result = await resolveNameWithUniversalResolver(publicClient, nameInput);
if (result) {
const [encodedAddressBytes, resolverAddr] = result;
// Decode the address from the bytes returned by the target resolver
const decodedAddress = decodeAbiParameters(
parseAbiParameters('address'),
encodedAddressBytes
)[0];
setResolvedForwardAddress(decodedAddress);
setForwardResolverAddress(resolverAddr);
} else {
setForwardError("Could not resolve name or an error occurred.");
}
} catch (e: any) {
console.error("handleResolveName error:", e);
setForwardError(e.message || "Failed to resolve name.");
} finally {
setIsLoading(false);
}
};
const handleReverseResolveAddress = async () => {
if (!publicClient || !isAddress(addressInput)) {
setReverseError("Please connect wallet and enter a valid address.");
return;
}
setIsLoading(true);
setResolvedReverseName(null);
setReverseResolverAddress(null);
setReverseError(null);
try {
const result = await reverseResolveAddressWithUniversalResolver(publicClient, addressInput as Address);
if (result) {
const [encodedNameBytes, resolverAddr] = result;
// Decode the name string from the bytes returned by the target resolver
const decodedName = decodeAbiParameters(
parseAbiParameters('string'),
encodedNameBytes
)[0];
setResolvedReverseName(decodedName);
setReverseResolverAddress(resolverAddr);
} else {
setReverseError("Could not resolve address or an error occurred.");
}
} catch (e: any) {
console.error("handleReverseResolveAddress error:", e);
setReverseError(e.message || "Failed to resolve address to name.");
} finally {
setIsLoading(false);
}
};
return (
<div>
<h2>Resolve via UniversalResolver (Direct Call - Advanced)</h2>
<p className="text-sm text-gray-600 mb-2">
This section demonstrates direct calls to the UniversalResolver.
Ensure your client is connected to Zilliqa network.
</p>
{/* Name to Address */}
<div className="my-4 p-4 border rounded">
<h3 className="font-semibold">Name to Address (Forward Resolution)</h3>
<input
type="text"
value={nameInput}
onChange={(e) => setNameInput(e.target.value)}
placeholder="Enter .zil name (e.g., example.zil)"
className="border p-1 mr-2 my-1"
/>
<button onClick={handleResolveName} disabled={isLoading} className="bg-blue-500 text-white p-1 rounded disabled:opacity-50">
{isLoading ? 'Resolving...' : 'Resolve Name'}
</button>
{resolvedForwardAddress && (
<div className="mt-2 text-green-700">
<p>Resolved Address: {resolvedForwardAddress}</p>
<p className="text-xs">Resolver Used: {forwardResolverAddress}</p>
</div>
)}
{forwardError && <p className="mt-2 text-red-500">{forwardError}</p>}
</div>
{/* Address to Name */}
<div className="my-4 p-4 border rounded">
<h3 className="font-semibold">Address to Name (Reverse Resolution)</h3>
<input
type="text"
value={addressInput}
onChange={(e) => setAddressInput(e.target.value)}
placeholder="Enter 0x address"
className="border p-1 mr-2 my-1"
/>
<button onClick={handleReverseResolveAddress} disabled={isLoading} className="bg-blue-500 text-white p-1 rounded disabled:opacity-50">
{isLoading ? 'Resolving...' : 'Reverse Resolve Address'}
</button>
{resolvedReverseName && (
<div className="mt-2 text-green-700">
<p>Resolved Name: {resolvedReverseName}</p>
<p className="text-xs">Resolver Used: {reverseResolverAddress}</p>
</div>
)}
{reverseError && <p className="mt-2 text-red-500">{reverseError}</p>}
</div>
</div>
);
}
Key considerations for direct UniversalResolver interaction:
- DNS Encoding: Names need to be DNS-encoded into bytes. This is a non-trivial step. Viem handles this internally for its ENS functions, but for direct calls, you'll need to implement or find a library for this.
- Call Data Construction: The
data
argument for theresolve
function must be the ABI-encoded call to the specific function on the underlying resolver (e.g.,addr(bytes32)
for resolving an address, ortext(bytes32, string)
for a text record). - ABI: You need the ABI for
UniversalResolver.sol
. Theresolve
function returns(bytes, address)
where the first component is the result of the call to the underlying resolver, and the second is the address of that resolver. You'll need to decode thebytes
result using the ABI of the function you called on the underlying resolver.
For most use cases, OPTION 1 (using Viem's built-in ENS functions) is highly recommended due to its simplicity and abstraction over these complexities. Option 2 provides an avenue for advanced scenarios or environments where Viem's abstractions are not directly used.
#
OPTION 3 - ZilNames Manual Integration using L2Resolver
You can also use the L2Resolver using the following functions/examples.
#
1. Forward Resolution (Name → Address) using L2Resolver
To resolve a .zil
name to an address, you'll need to:
- Calculate the namehash of the name
- Call the
addr
function on the L2Resolver contract
import { namehash } from "viem/ens";
// Function to resolve a .zil name to an address
async function resolveNameToAddress(name, client) {
// Ensure name has .zil suffix
if (!name.endsWith('.zil') && !name.endsWith('.test.zil')) {
throw new Error('Invalid name format: Must end with .zil or .test.zil');
}
// Get appropriate contract address based on name
const isTestnet = name.endsWith('.test.zil');
const contractAddress = isTestnet
? "0x579C72c5377a5a4A8Ce6d43A1701F389c8FDFC8e"
: "0x5c0c7BFd25efCAE366fE62219fD5558305Ffc46F";
// Calculate namehash
const node = namehash(name);
// Call contract
try {
const address = await client.readContract({
address: contractAddress,
abi: L2ResolverAbi,
functionName: "addr",
args: [node],
});
return address;
} catch (error) {
console.error("Error resolving name:", error);
return null;
}
}
#
Namehash Function
While most libraries like viem provide a namehash implementation, here's how the namehash algorithm works:
import { keccak256 } from "viem";
import { toUtf8Bytes } from "viem/utils";
// Implementation of ENS namehash algorithm
function namehash(name) {
let node = '0x0000000000000000000000000000000000000000000000000000000000000000';
if (name) {
const labels = name.split('.');
for (let i = labels.length - 1; i >= 0; i--) {
const label = labels[i];
const labelHash = keccak256(toUtf8Bytes(label));
node = keccak256(new Uint8Array([...hexToBytes(node), ...hexToBytes(labelHash)]));
}
}
return node;
}
// Helper to convert hex string to byte array
function hexToBytes(hex) {
const bytes = [];
for (let c = 2; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16));
}
return bytes;
}
#
2. Reverse Resolution (Address → Name)
Reverse resolution requires converting the address to a special reverse node format:
import { encodePacked, keccak256, namehash } from "viem";
// Convert chainId to coin type for reverse resolution
function convertChainIdToCoinType(chainId) {
// Zilliqa mainnet
if (chainId === zilliqa.id) {
return "addr";
}
// Zilliqa testnet
else if (chainId === zilliqaTestnet.id) {
return "80002105";
}
// For other chains, apply BIP44 derivation
const cointype = (0x80000000 | chainId) >>> 0;
return cointype.toString(16).toLocaleUpperCase();
}
// Convert address to reverse node
function convertReverseNodeToBytes(address, chainId) {
const addressFormatted = address.toLowerCase();
const addressNode = keccak256(addressFormatted.substring(2));
const chainCoinType = convertChainIdToCoinType(chainId);
const baseReverseNode = namehash(`${chainCoinType.toLocaleUpperCase()}.reverse`);
const addressReverseNode = keccak256(
encodePacked(["bytes32", "bytes32"], [baseReverseNode, addressNode])
);
return addressReverseNode;
}
// Resolve address to name
async function resolveAddressToName(address, chainId, client) {
// Get appropriate contract based on chainId
const contractAddress = chainId === 33101
? "0x579C72c5377a5a4A8Ce6d43A1701F389c8FDFC8e"
: "0x5c0c7BFd25efCAE366fE62219fD5558305Ffc46F";
// Calculate reverse node
const addressReverseNode = convertReverseNodeToBytes(address, chainId);
// Call contract
try {
const name = await client.readContract({
address: contractAddress,
abi: L2ResolverAbi,
functionName: "name",
args: [addressReverseNode],
});
return name || null;
} catch (error) {
console.error("Error resolving address to name:", error);
return null;
}
}
#
3. Avatar Resolution
ZilNames also supports avatar images stored in the text records:
import { namehash } from "viem/ens";
// Function to get avatar for a .zil name
async function getAvatar(name, client) {
if (!name) return null;
// Get appropriate contract address based on name
const isTestnet = name.endsWith('.test.zil');
const contractAddress = isTestnet
? "0x579C72c5377a5a4A8Ce6d43A1701F389c8FDFC8e"
: "0x5c0c7BFd25efCAE366fE62219fD5558305Ffc46F";
try {
// Get the avatar text record
const avatar = await client.readContract({
address: contractAddress,
abi: L2ResolverAbi,
functionName: "text",
args: [namehash(name), "avatar"],
});
if (!avatar) return null;
// Handle IPFS URLs if needed
if (avatar.startsWith('ipfs://')) {
const ipfsGateway = "https://ipfs.io/ipfs/";
const ipfsPath = avatar.replace('ipfs://', '');
return `${ipfsGateway}${ipfsPath}`;
}
return avatar;
} catch (error) {
console.error("Error fetching avatar:", error);
return null;
}
}
#
React Integration Example
For React applications, here's how you might implement hooks to use ZilNames manually:
import { useQuery } from '@tanstack/react-query';
import { namehash } from 'viem/ens';
import { Address } from 'viem';
// Hook to resolve wallet address to .zil name
function useZilliqaEnsName({ address, chainId }) {
const { data, isLoading } = useQuery({
queryKey: ['zilname', address, chainId],
queryFn: async () => {
if (!address || !chainId) return null;
const client = getChainPublicClient(chainId);
const addressReverseNode = convertReverseNodeToBytes(address, chainId);
try {
const contractAddress = chainId === zilliqa.id
? "0x5c0c7BFd25efCAE366fE62219fD5558305Ffc46F"
: "0x579C72c5377a5a4A8Ce6d43A1701F389c8FDFC8e";
const zilname = await client.readContract({
abi: L2ResolverAbi,
address: contractAddress,
functionName: "name",
args: [addressReverseNode],
});
return zilname || null;
} catch (error) {
console.error('Error fetching Zilname:', error);
return null;
}
},
enabled: !!address && !!chainId,
});
return {
name: data,
isLoading,
};
}
// Component using the hook
function UserDisplay({ address, chainId }) {
const { name, isLoading } = useZilliqaEnsName({ address, chainId });
if (isLoading) return <div>Loading...</div>;
return (
<div>
{name ? name : `${address.slice(0, 6)}...${address.slice(-4)}`}
</div>
);
}
#
Advanced React Integration Example
#
1. Setting up the hooks
First, create three custom hooks to handle different aspects of ZilNames resolution:
useZilliqaEnsName.ts - for resolving addresses to names (reverse resolution):
import { Address, isAddress } from "viem";
import { useQuery } from "@tanstack/react-query";
import { convertReverseNodeToBytes } from "../utils/convertReverseNodeToBytes";
import { getChainPublicClient } from "../utils/getChainPublicClient";
import L2ResolverAbi from "../abis/L2ResolverAbi";
import { zilliqa, zilliqaTestnet } from "../../config/chains";
export type UseZilliqaEnsNameProps = {
address?: Address;
chainId?: number;
};
export type ZilliqaEnsNameData = {
name: string | null;
isLoading: boolean;
isFetching: boolean;
};
export default function useZilliqaEnsName({ address, chainId }: UseZilliqaEnsNameProps): ZilliqaEnsNameData {
const { data, isLoading, isFetching } = useQuery({
queryKey: ['zilname', address, chainId],
queryFn: async () => {
if (!address || !chainId) return null;
// Get the appropriate chain configuration
const chain = chainId === zilliqa.id ? zilliqa :
chainId === zilliqaTestnet.id ? zilliqaTestnet :
null;
if (!chain) {
console.log('Chain not supported:', chainId);
return null;
}
const client = getChainPublicClient(chain);
const addressReverseNode = convertReverseNodeToBytes(address, chainId);
try {
const contractAddress = chainId === zilliqa.id
? "0x5c0c7BFd25efCAE366fE62219fD5558305Ffc46F"
: "0x579C72c5377a5a4A8Ce6d43A1701F389c8FDFC8e";
const zilname = await client.readContract({
abi: L2ResolverAbi,
address: contractAddress,
functionName: "name",
args: [addressReverseNode],
}) as string;
if (zilname && zilname.length > 0) {
return zilname;
}
return null;
} catch (error) {
console.error('Error fetching Zilname:', error);
return null;
}
},
enabled: !!address && !!chainId && isAddress(address),
});
return {
name: data ?? null,
isLoading,
isFetching,
};
}
useZilliqaEnsAddress.ts - for resolving names to addresses (forward resolution):
import { Address } from "viem";
import { useQuery } from "@tanstack/react-query";
import { getChainPublicClient } from "../utils/getChainPublicClient";
import L2ResolverAbi from "../abis/L2ResolverAbi";
import { zilliqa, zilliqaTestnet } from "../../config/chains";
import { namehash } from "viem/ens";
export type UseZilliqaEnsAddressProps = {
name?: string;
chainId?: number;
};
export type ZilliqaEnsAddressData = {
address: string | null;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
};
export default function useZilliqaEnsAddress({ name, chainId }: UseZilliqaEnsAddressProps): ZilliqaEnsAddressData {
const { data, isLoading, isFetching, error } = useQuery({
queryKey: ['ziladdress', name, chainId],
queryFn: async () => {
if (!name || !chainId) return null;
// Get the appropriate chain configuration
const chain = chainId === zilliqa.id ? zilliqa :
chainId === zilliqaTestnet.id ? zilliqaTestnet :
null;
if (!chain) {
console.log('Chain not supported:', chainId);
return null;
}
const client = getChainPublicClient(chain);
try {
const contractAddress = chainId === zilliqa.id
? "0x5c0c7BFd25efCAE366fE62219fD5558305Ffc46F"
: "0x579C72c5377a5a4A8Ce6d43A1701F389c8FDFC8e";
// Use namehash to convert the name to the node format expected by the contract
const nameNode = namehash(name);
// Call the addr function of the L2 resolver contract
const address = await client.readContract({
abi: L2ResolverAbi,
address: contractAddress,
functionName: "addr",
args: [nameNode],
}) as Address;
if (address) {
return address;
}
return null;
} catch (error) {
console.error('Error fetching address:', error);
throw error; // Let the error be caught by the error handler
}
},
enabled: !!name && !!chainId && name.trim().length > 0,
});
return {
address: data ?? null,
isLoading,
isFetching,
error: error as Error | null,
};
}
useZilEnsAvatar.ts - for fetching avatar images:
import { useQuery } from "@tanstack/react-query";
import { getChainPublicClient } from "../utils/getChainPublicClient";
import { zilliqa, zilliqaTestnet } from "../../config/chains";
import L2ResolverAbi from "../abis/L2ResolverAbi";
import { Address } from "viem";
import useZilliqaEnsName from "./useZilliqaEnsName";
import { namehash } from "viem/ens";
export type UseZilEnsAvatarProps = {
address?: Address;
chainId?: number;
};
export type ZilEnsAvatarData = {
avatar: string | null;
isLoading: boolean;
isFetching: boolean;
};
export default function useZilEnsAvatar({ address, chainId }: UseZilEnsAvatarProps): ZilEnsAvatarData {
// First get the name associated with this address
const { name, isLoading: isNameLoading } = useZilliqaEnsName({ address, chainId });
const { data, isLoading, isFetching } = useQuery({
queryKey: ['zilavatar', name, chainId],
queryFn: async () => {
if (!name || !chainId) return null;
// Get the appropriate chain configuration
const chain = chainId === zilliqa.id ? zilliqa :
chainId === zilliqaTestnet.id ? zilliqaTestnet :
null;
if (!chain) {
console.log('Chain not supported:', chainId);
return null;
}
const client = getChainPublicClient(chain);
try {
const contractAddress = chainId === zilliqa.id
? "0x5c0c7BFd25efCAE366fE62219fD5558305Ffc46F"
: "0x579C72c5377a5a4A8Ce6d43A1701F389c8FDFC8e";
// Get the avatar text record using the name's namehash
const avatar = await client.readContract({
abi: L2ResolverAbi,
address: contractAddress,
functionName: "text",
args: [namehash(name), "avatar"],
}) as string;
if (!avatar) {
return null;
}
// Handle IPFS URLs
if (avatar.startsWith('ipfs://')) {
const ipfsGateway = "https://ipfs.io/ipfs/";
const ipfsPath = avatar.replace('ipfs://', '');
return `${ipfsGateway}${ipfsPath}`;
}
// Return the avatar URL directly if it's not IPFS
return avatar;
} catch (error) {
console.error('Error fetching avatar:', error);
return null;
}
},
enabled: !!name && !isNameLoading && !!chainId,
});
return {
avatar: data ?? null,
isLoading: isLoading || isNameLoading,
isFetching,
};
}
#
2. Utility functions
You'll need some utility functions referenced in the hooks:
convertReverseNodeToBytes.ts:
import { Address, encodePacked, keccak256, namehash } from "viem";
import { zilliqa, zilliqaTestnet } from "../../config/chains";
// Convert chainId to coin type for reverse resolution
export function convertChainIdToCoinType(chainId: number): string {
// Zilliqa mainnet
if (chainId === zilliqa.id) {
return "addr";
}
// Zilliqa testnet
else if (chainId === zilliqaTestnet.id) {
return "80002105";
}
// For other chains, apply BIP44 derivation
const cointype = (0x80000000 | chainId) >>> 0;
return cointype.toString(16).toLocaleUpperCase();
}
// Convert address to reverse node
export function convertReverseNodeToBytes(address: Address, chainId: number): `0x${string}` {
const addressFormatted = address.toLowerCase();
const addressNode = keccak256(addressFormatted.substring(2));
const chainCoinType = convertChainIdToCoinType(chainId);
const baseReverseNode = namehash(`${chainCoinType.toLocaleUpperCase()}.reverse`);
const addressReverseNode = keccak256(
encodePacked(["bytes32", "bytes32"], [baseReverseNode, addressNode])
);
return addressReverseNode;
}
getChainPublicClient.ts:
import { Chain, createPublicClient, http } from "viem";
export function getChainPublicClient(chain: Chain) {
return createPublicClient({
chain,
transport: http(),
});
}
#
3. Example implementation in a React component
Here's how you can use these hooks in a React component:
'use client';
import { useState } from 'react';
import { isAddress } from 'viem';
import { zilliqa } from '@/config/chains';
import useZilliqaEnsName from '@/lib/hooks/useZilliqaEnsName';
import useZilEnsAvatar from '@/lib/hooks/useZilEnsAvatar';
import useZilliqaEnsAddress from '@/lib/hooks/useZilliqaEnsAddress';
export default function ZilNamesResolver() {
// Name to Address state
const [nameInput, setNameInput] = useState('');
const [shouldResolveAddress, setShouldResolveAddress] = useState(false);
// Address to Name state
const [addressInput, setAddressInput] = useState('');
const [shouldResolveName, setShouldResolveName] = useState(false);
// ENS name to address resolution
const {
address: resolvedAddress,
isLoading: isLoadingAddress,
error: addressError
} = useZilliqaEnsAddress({
name: shouldResolveAddress ? nameInput.trim() : undefined,
chainId: zilliqa.id,
});
// Address to ENS name resolution
const {
name: resolvedName,
isLoading: isLoadingName
} = useZilliqaEnsName({
address: shouldResolveName && isAddress(addressInput.trim())
? addressInput.trim() as `0x${string}`
: undefined,
chainId: zilliqa.id,
});
// Get avatar for the resolved name
const {
avatar: nameAvatar,
isLoading: isLoadingAvatar
} = useZilEnsAvatar({
address: resolvedAddress as `0x${string}`,
chainId: zilliqa.id
});
// Handle name to address resolution
const handleResolveAddress = () => {
setShouldResolveAddress(true);
};
// Handle address to name resolution
const handleResolveName = () => {
setShouldResolveName(true);
};
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">ZilNames Resolver</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Name to Address Section */}
<div className="border p-4 rounded">
<h2 className="text-xl mb-4">Resolve Name to Address</h2>
<input
type="text"
value={nameInput}
onChange={(e) => setNameInput(e.target.value)}
placeholder="Enter a .zil name"
className="border p-2 w-full mb-2"
/>
<button
onClick={handleResolveAddress}
className="bg-blue-500 text-white px-4 py-2 rounded"
disabled={isLoadingAddress}
>
{isLoadingAddress ? 'Loading...' : 'Resolve'}
</button>
{resolvedAddress && (
<div className="mt-4">
<p><strong>Address:</strong> {resolvedAddress}</p>
{nameAvatar && <img src={nameAvatar} alt="Avatar" className="w-12 h-12 rounded-full mt-2" />}
</div>
)}
{addressError && <p className="text-red-500 mt-2">{addressError.message}</p>}
</div>
{/* Address to Name Section */}
<div className="border p-4 rounded">
<h2 className="text-xl mb-4">Resolve Address to Name</h2>
<input
type="text"
value={addressInput}
onChange={(e) => setAddressInput(e.target.value)}
placeholder="Enter an EVM 0x address"
className="border p-2 w-full mb-2"
/>
<button
onClick={handleResolveName}
className="bg-blue-500 text-white px-4 py-2 rounded"
disabled={isLoadingName}
>
{isLoadingName ? 'Loading...' : 'Resolve'}
</button>
{resolvedName && (
<div className="mt-4">
<p><strong>Name:</strong> {resolvedName}</p>
</div>
)}
</div>
</div>
</div>
);
}
This example demonstrates a complete workflow for resolving ZilNames, including both forward and reverse resolution, and avatar fetching.
#
Full L2Resolver Contract Interface
The L2Resolver contract implements the following key functions:
name(bytes32 node)
- Gets the name for a reverse recordaddr(bytes32 node)
- Gets the address for a forward recordtext(bytes32 node, string key)
- Gets a text record value (e.g., avatar)setPubkey(bytes32 node, bytes32 x, bytes32 y)
- Sets public keysetAddr(bytes32 node, address addr)
- Sets forward recordsetText(bytes32 node, string key, string value)
- Sets text record
Most applications will primarily use the resolution functions (name
and addr
), but the contract supports a full suite of ENS-compatible functionality.
#
Working with Different Chains
ZilNames operates on both Zilliqa mainnet (for .zil
domains) and Zilliqa testnet (for .test.zil
domains). To determine the appropriate chain:
function getChainForZilname(username) {
return username.endsWith('.test.zil')
? zilliqaTestnet
: zilliqa;
}
#
Conclusion
ZilNames provides a user-friendly naming system for the Zilliqa ecosystem. By integrating ZilNames into your application, you can offer users a more intuitive experience when interacting with blockchain addresses.
For more information, visit zilnames.com. For more information on ENS Contracts, visit docs.ens.domains/contracts.