跳到主要内容

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
ENS vs DNS

DNS 由中心化机构(ICANN)管理,域名可被审查或收回;ENS 运行在以太坊智能合约上,一旦注册,只有所有者(持有 NFT 的地址)能控制域名,任何人无法审查或没收。ENS 域名本质上是一个 ERC-721 NFT。


面试速答版

什么是 ENS?前端要做什么? ENS 是以太坊上的去中心化「DNS」:

  • 把人类可读的 vitalik.eth 映射到 0xd8dA...6045,本质是 ERC-721 NFT,谁持有 NFT 谁控制域名。
  • 不只能解地址,还能存头像、Twitter、IPFS 内容哈希等记录。
  • 前端常用场景:输入框允许输 xx.eth 自动解析、地址栏显示 vitalik.eth0xd8... 友好。

正向解析 vs 反向解析?

  • 正向name → address,用 getAddress('vitalik.eth') 或 viem getEnsAddress
  • 反向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 解析支持:

ens-ethers.ts
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:

ens-viem.ts
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',
});
normalize 的作用

ENS 名称需要经过 UTS-46 Unicode 规范化处理。例如大写字母 Vitalik.eth 需要转为小写,某些 Unicode 字符需要标准化。viem 的 normalize() 和 ethers 内部都会自动处理这个问题。

使用 Wagmi Hooks

Wagmi 提供了开箱即用的 React Hooks:

EnsProfile.tsx
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 URLhttps://example.com/avatar.png直接可用
IPFS URIipfs://QmHash...需转为网关 URL
NFT URIeip155:1/erc721:0xBC4C.../1234需查询 NFT 元数据获取图片

NFT URI 的解析流程:

resolve-avatar.ts
/**
* 解析 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 中展示钱包地址时,应遵循以下优先级:

  1. 优先显示 ENS 名称:如 vitalik.eth
  2. 无 ENS 时截断地址:如 0x1234...5678
  3. 配合头像:ENS 头像 > Blockie / Jazzicon 生成
address-display.ts
/**
* 截断地址显示
* 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 头像时,常见的回退方案:

方案特点
Blockieethereum-blockies-base64像素风方块头像,MetaMask 使用
Jazzicon@metamask/jazzicon彩色几何图形,基于地址哈希生成
Gradient自定义根据地址哈希生成渐变色
AddressAvatar.tsx
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 头像支持三种格式:

  1. HTTPS URL:直接可用,如 https://example.com/avatar.png
  2. IPFS URI:需要通过 IPFS 网关转换,如 ipfs://QmHash -> https://gateway.pinata.cloud/ipfs/QmHash
  3. NFT URI:如 eip155:1/erc721:0xBC4C.../1234,需要调用 NFT 合约的 tokenURI 方法获取元数据,再从元数据中提取 image 字段

实际开发中,ethers.js 和 viem 内置了完整的头像解析逻辑,建议直接使用 resolver.getAvatar()getEnsAvatar()

Q3: DApp 中如何优雅展示用户地址?

答案

遵循优先级策略:

  1. 优先显示 ENS 名称(如 vitalik.eth
  2. 无 ENS 时截断地址(如 0xd8dA...6045),保留前后各 4 位
  3. 头像使用 ENS Avatar,无头像时使用 Blockie 或 Jazzicon 生成确定性头像
  4. 鼠标悬停时显示完整地址(tooltip)
  5. 点击地址可复制到剪贴板

Q4: 什么是 SBT?与普通 NFT 有什么区别?

答案

SBT(Soulbound Token)是不可转让的代币,核心区别:

特性普通 NFTSBT
可转让可以自由转让和交易绑定地址,不可转让
用途数字艺术、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。

正确的验证流程:

  1. 通过反向解析查询地址 B 的 Primary Name,得到 alice.eth
  2. 再通过正向解析查询 alice.eth 指向的地址,得到地址 A
  3. 比较地址 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 社交资料
}

数据获取策略:

  1. ENS 数据:通过 ethers/viem 直接查询链上合约
  2. SBT / NFT:通过 The Graph 子图或 Alchemy/Moralis NFT API
  3. 社交图谱:通过 Lens API 或 Farcaster Hub API
  4. 聚合查询:使用 Airstack 等聚合 API 一次获取多维度身份数据

组件应做好加载态和缺省态处理,因为大部分用户不会拥有所有类型的链上身份。


相关链接