EIP-9700是一份面向智能账户与NFT融合场景的以太坊改进提案,它将NFT的持有权与智能账户的控制权绑定在一起,使得NFT不再只是静态的收藏品,而可以成为一个具备自主交互能力的链上实体。Cosmology则是围绕Cosmos生态打造的一整套TypeScript开发工具链,包括链抽象、钱包连接、代码生成等能力。将一个已有的React应用迁移到EIP-9700并接入Cosmology,本质上是一次从传统EOA钱包模式向智能账户模式的架构升级。本文将从标准解读、架构调整、代码改造和联调测试四个层面,详细讲解整个迁移过程。

一、理解EIP-9700的核心变化与迁移前提
在动手改造代码之前,必须先弄清楚EIP-9700与传统ERC-721的根本差异。传统NFT合约中,ownerOf返回的是一个外部账户地址,NFT只是被动的资产;而在EIP-9700体系下,每一枚NFT本身关联一个智能合约账户,这个账户可以持有资产、发起交易、执行任意合约调用。换句话说,NFT从“被拥有的对象”变成了“能自己行动的主体”。
这个变化对前端的影响是深远的。首先,用户的身份不再单纯依赖MetaMask等浏览器钱包提供的EOA地址,而是可以通过智能账户的Session Key或代理账户进行签名。其次,NFT的展示逻辑需要额外查询其绑定账户的链上状态,例如该NFT账户当前持有的代币余额、正在参与的链上活动等。最后,交易的生命周期也变了,一笔交易可能先进入Bundler的内存池,再经过签名聚合,最终才上链,前端需要轮询或订阅UserOperation的哈希状态。
在迁移前建议做一次全面的技术盘点:梳理当前应用中所有依赖window.ethereum直接调用的地方,统计合约ABI中与所有权判断相关的方法,整理现有的状态管理结构。这些盘点结果将直接决定后续改造的工作量分布。
二、架构调整:引入Cosmology工具链与账户抽象层
Cosmology提供了一系列开箱即用的工具,其中对这次迁移最有价值的是钱包抽象模块和链类型定义生成器。传统React应用通常直接在组件中调用ethers.BrowserProvider,耦合度很高。迁移的第一步是把这些调用抽离成独立的Service层,再由Service层对接Cosmology的账户接口。
推荐的目录结构调整如下:
src/ ├── services/ │ ├── account.ts // 账户抽象服务,封装EIP-9700智能账户 │ ├── nft.ts // NFT合约交互服务 │ └── chain.ts // 链连接与RPC管理 ├── hooks/ │ ├── useAccount.ts // 账户状态Hook │ └── useNftAssets.ts // 资产查询Hook ├── components/ └── generated/ // Cosmology生成的类型定义
这种分层的好处在于,账户逻辑的变化被限制在Service层内,组件层完全无感知。当后续需要切换不同的智能账户实现(例如从SimpleAccount换成支持社交恢复的账户)时,只需替换account.ts中的实现,上层代码一行都不用改。
另一个关键调整是状态管理。智能账户模式下,应用需要同时维护三类状态:钱包连接状态、智能账户地址、以及UserOperation的执行状态。这三类状态的更新时机各不相同,建议使用状态机来管理,避免出现“账户已连接但智能账户尚未初始化”这类中间态导致的界面闪烁。
三、代码改造:从ethers直连到智能账户交互
下面看具体的代码变化。先回顾传统模式下React应用连接钱包的写法:
// 传统写法:直接依赖浏览器注入的window.ethereum
import { ethers } from "ethers";
async function connectWallet() {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
return { provider, signer, address };
}
这种写法在EIP-9700场景下问题很多:它假设用户一定是EOA,无法支持智能账户的批量签名,也没有处理UserOperation的异步上链流程。改造后的写法如下:
// 改造后:通过Cosmology账户抽象层创建EIP-9700智能账户
import { createSmartAccountClient } from "@/services/account";
export function useEip9700Account() {
const [account, setAccount] = useState(null);
const [status, setStatus] = useState("idle"); // idle | connecting | ready
const connect = useCallback(async (walletProvider) => {
setStatus("connecting");
try {
const client = await createSmartAccountClient({
provider: walletProvider,
// 每一枚EIP-9700 NFT对应一个独立的智能账户
entryPointAddress: "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
factoryAddress: process.env.REACT_APP_ACCOUNT_FACTORY,
});
const address = await client.getAccountAddress();
setAccount({ client, address });
setStatus("ready");
} catch (err) {
setStatus("idle");
throw err;
}
}, []);
return { account, status, connect };
}
注意这里的核心差异:我们不再直接拿signer去签名交易,而是先创建一个智能账户客户端,再通过它发送UserOperation。发送NFT相关操作时的代码也要相应调整:
// 通过智能账户铸造EIP-9700 NFT
async function mintCosmologyNft(account, tokenId) {
const userOp = await account.client.buildUserOperation({
target: process.env.REACT_APP_NFT_CONTRACT,
data: encodeFunctionData({
abi: eip9700Abi,
functionName: "mintWithAccount",
args: [tokenId],
}),
value: parseEther("0.01"),
});
// Bundler会返回UserOperation哈希,需要轮询确认上链
const userOpHash = await account.client.sendUserOperation(userOp);
const receipt = await account.client.waitForUserOperationReceipt(userOpHash);
return receipt;
}
这段代码中有两个容易被忽视的细节。第一,waitForUserOperationReceipt可能因为Bundler拥堵而超时,务必设置合理的超时时间并给用户展示中间状态。第二,EIP-9700的铸造方法通常会同时部署对应的智能账户,gas消耗比普通ERC-721铸造高出不少,前端应当预估gas并在界面上明确提示。
四、联调测试与性能优化的收尾工作
迁移完成后,测试环节需要覆盖几个过去不存在的场景:智能账户的创建是否幂等(重复调用不应重复部署)、NFT转账后账户控制权是否正确转移、以及当用户切换钱包时应用状态能否正确重置。建议在测试网络部署一套完整的Bundler和EntryPoint环境,用Cosmology提供的本地开发工具可以快速拉起这条链路。
性能方面,智能账户的地址计算涉及CREATE2预测,第一次进入页面时可能产生可感知的延迟。优化手段包括:将地址计算结果缓存到localStorage、在应用启动阶段预计算常用账户地址、以及把Cosmology生成的类型文件按需加载避免首屏包体积膨胀。实测中,做好这几项优化后,首屏交互时间通常可以控制在原有水平的1.2倍以内,代价完全可接受。
最后提醒一点,EIP-9700相关的生态仍在快速演进,Bundler和EntryPoint的接口可能随版本调整。建议在项目中锁定具体版本号,并通过Service层的抽象隔离这些外部依赖,这样即使底层标准更新,迁移成本也能控制在最小范围内。完成这些步骤后,你的React应用就具备了完整的EIP-9700加Cosmology能力,可以支撑宇宙学NFT这类需要NFT自主参与链上协作的复杂场景了。