ENS 与链上身份
什么是 ENS?
ENS(Ethereum Name Service) 是以太坊上的去中心化域名系统,作用类似于传统互联网的 DNS——将人类可读的名称(如 vitalik.eth)映射到机器可读的地址(如 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045)。
ENS 不仅支持地址解析,还能存储丰富的链上记录:
| 记录类型 | 说明 | 示例 |
|---|---|---|
| ETH 地址 | 以太坊钱包地址 | 0xd8dA...6045 |
| BTC 地址 | 比特币地址 | bc1q... |
| 内容哈希 | IPFS / Swarm 内容 | ipfs://Qm... |
| 头像 | 用户头像(URL / IPFS / NFT) | eip155:1/erc721:0x.../123 |
| 社交账号 | Twitter、GitHub 等 | @vitalik |
| 文本记录 | 自定义键值对 | com.discord, email |
DNS 由中心化机构(ICANN)管理,域名可被审查或收回;ENS 运行在以太坊智能合约上,一旦注册,只有所有者(持有 NFT 的地址)能控制域名,任何人无法审查或没收。ENS 域名本质上是一个 ERC-721 NFT。
什么是 ENS?前端要做什么? ENS 是以太坊上的去中心化「DNS」:
- 把人类可读的
vitalik.eth映射到0xd8dA...6045,本质是 ERC-721 NFT,谁持有 NFT 谁控制域名。 - 不只能解地址,还能存头像、Twitter、IPFS 内容哈希等记录。
- 前端常用场景:输入框允许输
xx.eth自动解析、地址栏显示vitalik.eth比0xd8...友好。
正向解析 vs 反向解析?
- 正向:
name → address,用getAddress('vitalik.eth')或 viemgetEnsAddress。 - 反向:
address → name,需要用户在 ENS App 里主动设置 Primary Name,否则反查为 null。 - 反查后还要再做一次正查校验「双向一致」,防伪造。
头像和链上身份怎么取?
- ENS Avatar 记录支持 URL、IPFS、
eip155:1/erc721:0x.../tokenId(NFT 头像)三种格式。 - 用
getEnsAvatar(name)自动处理,背后会去拉对应 NFT 的元数据。 - 想做泛「链上身份」可以叠加 Lens Protocol、Farcaster、SBT(不可转让 NFT 做凭证)。
Wagmi 怎么用?
直接 useEnsName({ address }) / useEnsAvatar({ name }) / useEnsAddress({ name }),自动缓存。
正向解析与反向解析
ENS 有两个核心解析方向:
- 正向解析(Forward Resolution):域名 -> 地址。用户输入
vitalik.eth,解析为链上地址,用于转账、合约交互 - 反向解析(Reverse Resolution):地址 -> 域名。将
0xd8dA...6045显示为vitalik.eth,提升用户体验
反向解析不是自动生效的。用户必须在 ENS App 中主动设置 Primary Name(主名称),才能通过地址反向查询到域名。未设置的地址查询结果为 null。
前端集成 ENS
使用 ethers.js
ethers.js 内置了 ENS 解析支持:
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://eth.llamarpc.com');
// 正向解析:域名 → 地址
const address = await provider.resolveName('vitalik.eth');
console.log(address); // 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
// 反向解析:地址 → 域名
const name = await provider.lookupAddress(
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
);
console.log(name); // vitalik.eth
// 获取头像
const resolver = await provider.getResolver('vitalik.eth');
const avatar = await resolver?.getAvatar();
console.log(avatar); // 头像 URL(已解析为可访问地址)
// 获取任意文本记录
const twitter = await resolver?.getText('com.twitter');
console.log(twitter); // VitalikButerin
使用 viem
viem 提供了类型安全的 ENS API:
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { normalize } from 'viem/ens';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
// 正向解析
const address = await client.getEnsAddress({
name: normalize('vitalik.eth'), // normalize 处理 Unicode 规范化
});
// 反向解析
const name = await client.getEnsName({
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
});
// 获取头像
const avatar = await client.getEnsAvatar({
name: normalize('vitalik.eth'),
});
// 获取文本记录
const twitter = await client.getEnsText({
name: normalize('vitalik.eth'),
key: 'com.twitter',
});
ENS 名称需要经过 UTS-46 Unicode 规范化处理。例如大写字母 Vitalik.eth 需要转为小写,某些 Unicode 字符需要标准化。viem 的 normalize() 和 ethers 内部都会自动处理这个问题。
使用 Wagmi Hooks
Wagmi 提供了开箱即用的 React Hooks:
import { useEnsName, useEnsAddress, useEnsAvatar } from 'wagmi';
function EnsProfile({ address }: { address: `0x${string}` }) {
// 反向解析:地址 → ENS 名称
const { data: ensName } = useEnsName({ address });
// 获取 ENS 头像
const { data: ensAvatar } = useEnsAvatar({
name: ensName ?? undefined,
});
return (
<div className="profile">
<img
src={ensAvatar ?? generateBlockie(address)}
alt="avatar"
/>
<span>{ensName ?? truncateAddress(address)}</span>
</div>
);
}
// 正向解析:ENS 名称 → 地址
function SendForm() {
const [recipient, setRecipient] = useState('');
const { data: resolvedAddress } = useEnsAddress({
name: recipient.includes('.') ? recipient : undefined,
});
const targetAddress = resolvedAddress ?? recipient;
// ...
}
ENS 头像解析
ENS 头像支持多种格式,前端需要统一处理:
| 格式 | 示例 | 说明 |
|---|---|---|
| HTTPS URL | https://example.com/avatar.png | 直接可用 |
| IPFS URI | ipfs://QmHash... | 需转为网关 URL |
| NFT URI | eip155:1/erc721:0xBC4C.../1234 | 需查询 NFT 元数据获取图片 |
NFT URI 的解析流程:
/**
* 解析 IPFS URI 为 HTTP 网关 URL
*/
function resolveIpfs(uri: string): string {
if (uri.startsWith('ipfs://')) {
const hash = uri.replace('ipfs://', '');
return `https://gateway.pinata.cloud/ipfs/${hash}`;
}
return uri;
}
/**
* 解析 ENS 头像(简化版)
* ethers.js 和 viem 内置了完整解析逻辑,一般不需要手动实现
*/
async function resolveEnsAvatar(avatarRecord: string): Promise<string> {
// HTTPS URL - 直接返回
if (avatarRecord.startsWith('http')) {
return avatarRecord;
}
// IPFS URI - 转网关
if (avatarRecord.startsWith('ipfs://')) {
return resolveIpfs(avatarRecord);
}
// NFT URI - 如 eip155:1/erc721:0xBC4C.../1234
if (avatarRecord.startsWith('eip155:')) {
const [, , contractInfo, tokenId] = avatarRecord.split(/[:/]/);
// 调用合约 tokenURI 获取元数据,再提取 image
// 实际项目中建议直接使用 ethers/viem 的内置解析
// ...
}
return avatarRecord;
}
ethers.js 的 resolver.getAvatar() 和 viem 的 getEnsAvatar() 已经内置了上述所有格式的解析逻辑,包括 NFT URI 的链上查询。生产项目中不建议手动实现,直接使用即可。
地址显示最佳实践
在 DApp 中展示钱包地址时,应遵循以下优先级:
- 优先显示 ENS 名称:如
vitalik.eth - 无 ENS 时截断地址:如
0x1234...5678 - 配合头像:ENS 头像 > Blockie / Jazzicon 生成
/**
* 截断地址显示
* 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 → 0xd8dA...6045
*/
function truncateAddress(address: string, chars = 4): string {
return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
}
/**
* 格式化地址显示:优先 ENS,退而截断
*/
function formatAddress(
address: string,
ensName?: string | null
): string {
if (ensName) return ensName;
return truncateAddress(address);
}
头像生成方案
当用户没有设置 ENS 头像时,常见的回退方案:
| 方案 | 库 | 特点 |
|---|---|---|
| Blockie | ethereum-blockies-base64 | 像素风方块头像,MetaMask 使用 |
| Jazzicon | @metamask/jazzicon | 彩色几何图形,基于地址哈希生成 |
| Gradient | 自定义 | 根据地址哈希生成渐变色 |
import { useEnsName, useEnsAvatar } from 'wagmi';
import makeBlockie from 'ethereum-blockies-base64';
function AddressAvatar({ address }: { address: `0x${string}` }) {
const { data: ensName } = useEnsName({ address });
const { data: ensAvatar } = useEnsAvatar({
name: ensName ?? undefined,
});
// 优先 ENS 头像,回退到 Blockie
const avatarSrc = ensAvatar ?? makeBlockie(address);
const displayName = ensName ?? truncateAddress(address);
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<img
src={avatarSrc}
alt={displayName}
style={{ width: 32, height: 32, borderRadius: '50%' }}
/>
<span title={address}>{displayName}</span>
</div>
);
}
链上身份聚合
ENS 只是链上身份的一部分。Web3 生态中还有多种身份协议:
| 协议 | 类型 | 说明 |
|---|---|---|
| ENS | 域名系统 | 地址到名称的映射,以太坊生态的基础身份层 |
| Lens Protocol | 去中心化社交图谱 | 基于 Polygon,将社交关系(关注、发帖)存储在链上 |
| Farcaster | 去中心化社交协议 | 链上身份 + 链下数据,Frames 可嵌入交互组件 |
| SBT | 灵魂绑定代币 | 不可转让的 NFT,用于表示学历、资质、身份凭证等 |
SBT(Soulbound Token)
SBT 由 Vitalik 在论文中提出,核心特征是不可转让——一旦发放到某个地址,就永远绑定该地址。
典型应用场景:
- 教育认证:完成某课程获得 SBT 证书
- DAO 成员资格:参与治理投票的凭证
- 信用评分:链上借贷历史形成的信用记录
- 出勤证明(POAP):参加活动的证明
链上声誉系统
链上声誉通过聚合多维度数据为地址建立信用画像:
对于前端开发者来说,链上身份的核心工作是展示层:如何优雅地将 ENS、头像、SBT 徽章、社交身份等信息聚合到用户 Profile 组件中。数据获取通常依赖 The Graph 子图查询或第三方 API(如 Airstack)。
常见面试问题
Q1: ENS 的正向解析和反向解析有什么区别?
答案:
- 正向解析:将 ENS 名称(如
vitalik.eth)解析为以太坊地址。用于转账场景,用户输入域名代替地址 - 反向解析:将以太坊地址解析为 ENS 名称。用于显示场景,让界面显示人类可读的名称而非长串地址
关键区别在于:正向解析只要域名所有者设置了地址记录即可生效;而反向解析需要地址所有者主动设置 Primary Name,且设置的名称必须正向解析回同一个地址(防止冒充)。
Q2: ENS 头像支持哪些格式?如何解析?
答案:
ENS 头像支持三种格式:
- HTTPS URL:直接可用,如
https://example.com/avatar.png - IPFS URI:需要通过 IPFS 网关转换,如
ipfs://QmHash->https://gateway.pinata.cloud/ipfs/QmHash - NFT URI:如
eip155:1/erc721:0xBC4C.../1234,需要调用 NFT 合约的tokenURI方法获取元数据,再从元数据中提取image字段
实际开发中,ethers.js 和 viem 内置了完整的头像解析逻辑,建议直接使用 resolver.getAvatar() 或 getEnsAvatar()。
Q3: DApp 中如何优雅展示用户地址?
答案:
遵循优先级策略:
- 优先显示 ENS 名称(如
vitalik.eth) - 无 ENS 时截断地址(如
0xd8dA...6045),保留前后各 4 位 - 头像使用 ENS Avatar,无头像时使用 Blockie 或 Jazzicon 生成确定性头像
- 鼠标悬停时显示完整地址(tooltip)
- 点击地址可复制到剪贴板
Q4: 什么是 SBT?与普通 NFT 有什么区别?
答案:
SBT(Soulbound Token)是不可转让的代币,核心区别:
| 特性 | 普通 NFT | SBT |
|---|---|---|
| 可转让 | 可以自由转让和交易 | 绑定地址,不可转让 |
| 用途 | 数字艺术、PFP、游戏道具 | 身份凭证、学历证明、信用记录 |
| 价值来源 | 稀缺性和市场供需 | 代表的真实世界身份或成就 |
| 合约实现 | 标准 ERC-721 | 重写 transferFrom 使其 revert |
Q5: Lens Protocol 和 Farcaster 有什么区别?
答案:
两者都是去中心化社交协议,但架构不同:
- Lens Protocol:完全链上,社交关系(关注、帖子、评论)都以 NFT 形式存储在 Polygon 上,数据完全由用户拥有,但 gas 成本和性能受限于链上
- Farcaster:混合架构,身份注册在以太坊上,但社交数据存储在链下(Hubs 网络),兼顾了去中心化和性能。Farcaster 的 Frames 功能允许在信息流中嵌入可交互组件
Q6: 为什么反向解析需要额外验证?
答案:
反向解析存在冒充风险。假设 Alice 拥有 alice.eth 并将其指向地址 A,而 Bob 的地址 B 也设置反向记录声称自己是 alice.eth。如果不验证,Bob 就能冒充 Alice。
正确的验证流程:
- 通过反向解析查询地址 B 的 Primary Name,得到
alice.eth - 再通过正向解析查询
alice.eth指向的地址,得到地址 A - 比较地址 A 和地址 B,不一致则说明是冒充,应忽略反向解析结果
ethers.js 的 lookupAddress() 和 viem 的 getEnsName() 内部已经包含了这个验证逻辑。
Q7: ENS 名称需要 normalize 处理吗?为什么?
答案:
是的。ENS 名称必须经过 UTS-46 Unicode 规范化处理。原因:
- Unicode 中存在视觉相同但编码不同的字符(如拉丁字母
a和西里尔字母а) - 大写需要转为小写(
Vitalik.eth->vitalik.eth) - 某些不可见字符或混合脚本需要被检测和拒绝
viem 提供 normalize() 函数,ethers.js 在解析时自动处理。不做规范化可能导致域名解析失败或被钓鱼攻击利用。
Q8: 前端如何聚合展示用户的链上身份?
答案:
一个完整的链上身份 Profile 组件需要聚合多个数据源:
// 组件设计思路
interface ChainIdentity {
address: string;
ensName?: string; // ENS 反向解析
ensAvatar?: string; // ENS 头像
ensRecords?: Record<string, string>; // 社交账号等文本记录
sbtTokens?: SBT[]; // 灵魂绑定代币列表
lensProfile?: LensProfile; // Lens 社交资料
}
数据获取策略:
- ENS 数据:通过 ethers/viem 直接查询链上合约
- SBT / NFT:通过 The Graph 子图或 Alchemy/Moralis NFT API
- 社交图谱:通过 Lens API 或 Farcaster Hub API
- 聚合查询:使用 Airstack 等聚合 API 一次获取多维度身份数据
组件应做好加载态和缺省态处理,因为大部分用户不会拥有所有类型的链上身份。