成就徽章NFT是目前Web3应用里非常主流的游戏化手段:用户完成新手引导、连续签到、参与社区投票等行为后,合约自动或半自动地为其铸造一枚不可转让的徽章,前端再将这些徽章渲染成个人主页上的荣誉墙。相比传统后端数据库里的一张积分表,链上徽章具备可验证、可组合的优势,第三方应用也能直接读取用户的历史成就。本文以React应用为载体,完整讲解EIP8720 Badges这类成就徽章标准的接入思路与落地代码。

一、EIP8720成就徽章的核心设计思路
EIP8720这类徽章提案的核心是定义了一套统一的徽章合约接口。它和普通的ERC721最大的区别在于两点:第一,徽章通常不可转让(或仅限管理员回收),这避免了成就被买卖的尴尬;第二,它强调徽章类型的批量管理,一个合约里可以定义几十上百种徽章,每种徽章有独立的元数据、解锁条件和发放上限。
从接口层面看,一个典型的徽章合约会暴露这样几个方法:用于定义新徽章类型的createBadgeType、用于发放的mint或claim、用于查询的balanceOf和badgesOfOwner。以Solidity为例,简化后的合约骨架大致如下:
// 前端视角:典型的EIP8720徽章合约ABI(节选) const BADGE_ABI = [ "function createBadgeType(uint256 typeId, string memory uri) external onlyOwner", "function mint(address to, uint256 typeId) external returns (uint256)", "function claim(uint256 typeId, bytes memory signature) external", "function badgesOfOwner(address owner) external view returns (uint256[] memory)", "function tokenURI(uint256 tokenId) external view returns (string memory)", "event Minted(address indexed to, uint256 indexed typeId, uint256 tokenId)" ];
这里的claim方法接受一个签名参数,这是徽章体系里非常常见的“服务端签名授权”模式。因为解锁条件(比如用户完成了某个任务)往往由后端判定,如果任何人都能直接调用mint,成就体系就形同虚设。所以典型流程是:后端验证用户行为后,用私钥对“用户地址+徽章类型ID+截止时间”做一个签名,用户拿到签名后自己去链上claim,Gas由用户承担,安全性由签名保证。
二、React前端的连接与查询实现
前端部分推荐使用ethers.js v6配合wagmi,两者搭配可以让钱包连接和合约调用都保持类型安全。首先初始化Provider和合约实例:
import { BrowserProvider, Contract } from "ethers";
export async function getBadgeContract() {
// MetaMask等注入式钱包
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
return new Contract(
"0xYourBadgeContractAddress",
BADGE_ABI,
signer
);
}
// 查询某个地址持有的全部徽章
export async function fetchUserBadges(address) {
const provider = new BrowserProvider(window.ethereum);
const contract = new Contract(
"0xYourBadgeContractAddress",
BADGE_ABI,
provider
);
const tokenIds = await contract.badgesOfOwner(address);
const badges = await Promise.all(
tokenIds.map(async (id) => {
const uri = await contract.tokenURI(id);
return { tokenId: id, uri };
})
);
return badges;
}有一个容易被忽视的细节:tokenURI返回的通常是IPFS链接或Base64编码的JSON。如果是IPFS链接,需要配置一个网关地址做转换,比如https://ipfs.io/ipfs/你的CID;如果是Base64,直接用atob或fetch解析即可。建议把元数据解析逻辑封装成独立的工具函数,不要散落在组件里。
React状态管理方面,徽章列表属于典型的服务端状态,建议直接交给TanStack Query管理,而不是塞进Redux。Query自带的缓存失效与轮询机制,恰好适合“等待链上确认后刷新徽章列表”这种场景:
import { useQuery, useQueryClient } from "@tanstack/react-query";
export function useUserBadges(address) {
return useQuery({
queryKey: ["badges", address],
queryFn: () => fetchUserBadges(address),
enabled: Boolean(address),
// 铸造确认后主动刷新
refetchInterval: 30_000,
});
}
export function BadgeWall({ address }) {
const { data: badges = [], isLoading } = useUserBadges(address);
if (isLoading) return <p>正在加载徽章...</p>;
return (
<div className="badge-grid">
{badges.map((b) => (
<BadgeCard key={b.tokenId.toString()} badge={b} />
))}
</div>
);
}三、触发铸造:懒铸造与服务端签名的完整链路
前面提到,实际项目中很少让前端直接调用管理员权限的mint,主流做法是“后端判定条件+签名,前端持签名claim”。整条链路的时序是:用户在React应用内完成任务,前端把任务凭证提交给自己的API,API校验通过后返回签名,前端再调用合约的claim方法。
// 第一步:请求后端签名
async function requestClaimSignature(typeId, userAddress) {
const res = await fetch("/api/badge/sign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ typeId, address: userAddress }),
});
if (!res.ok) throw new Error("任务校验未通过");
const { signature, deadline } = await res.json();
return { signature, deadline };
}
// 第二步:持签名上链claim
export async function claimBadge(typeId) {
const contract = await getBadgeContract();
const userAddress = await contract.signer.getAddress();
const { signature, deadline } = await requestClaimSignature(typeId, userAddress);
const tx = await contract.claim(typeId, signature);
// 等待打包确认
const receipt = await tx.wait();
console.log("徽章铸造成功,区块高度:", receipt.blockNumber);
return receipt;
}这套方案里最容易出错的环节是签名内容的前后端一致性。合约里用keccak256(abi.encode(user, typeId, deadline))做消息摘要,再套一层EIP712域名分隔,后端签出来的字节必须逐字节一致,否则链上ecrecover还原出的地址对不上,交易会直接revert。调试时可以先用ethers的verifyMessage在前端本地验一次签名,能大幅缩短排查时间。
关于Gas成本,如果应用的用户量较大且徽章发放频繁,可以考虑OpenZeppelin的ERC1155批量方案,一次交易发放多种徽章,把单枚徽章的边际成本压到很低;如果是低频高价值的核心成就,ERC721单发反而更清晰。两类合约的迁移成本不高,前端只需要把ABI和badgesOfOwner的返回结构适配一下。
四、常见问题与优化建议
第一个高频问题是链上数据与本地状态不同步。用户claim成功后,钱包里已经有徽章了,但页面因为查询缓存还没刷新,看起来像“铸造失败”。解决方式是在交易确认回调里调用queryClient.invalidateQueries,同时监听合约的Minted事件做兜底刷新。
第二个问题是钱包切换网络后的合约地址错配。徽章合约部署在测试网和主网时地址不同,前端必须根据chainId动态选择合约地址,硬编码单个地址几乎必然出事故。可以在项目里维护一个按chainId索引的地址映射,配合wagmi的链切换Hook自动响应。
第三个问题在于用户体验层面:钱包签名弹窗对普通用户是有心智负担的。建议在触发claim之前,用自定义的UI明确告知“你将免费领取XX徽章”或“本次操作需要支付少量Gas”,并在交易pending期间展示明确的进度状态。成就系统的目标是激励,而不是把用户挡在Web3的门槛外。
整体来看,React应用接入成就徽章NFT的技术复杂度并不高,关键工作量集中在解锁条件的后端判定、签名链路的安全设计,以及徽章展示层的交互打磨上。先把一条最小闭环(单一徽章类型、签名claim、列表展示)跑通,再逐步扩展徽章种类和视觉体系,是比较稳妥的演进路径。