名称: qa 描述: 针对基于Scaffold-ETH 2构建的以太坊dApp的预发布审计检查清单。在构建完成后,将此交给单独的审核代理(或新上下文)。仅覆盖AI代理实际发布的错误——通过与标准LLMs的基线测试验证。
dApp QA — 预发布审计
此技能用于审核,而非构建。 在dApp构建完成后,交给新代理。审核员应:
- 阅读源代码(
app/、components/、contracts/) - 在浏览器中打开应用并点击每个流程
- 检查以下每个项目——报告PASS/FAIL,不要修复
🚨 关键:钱包流程——按钮而非文本
打开应用,未连接钱包。
- ❌ FAIL: 显示文本如“连接你的钱包来玩” / “请连接以继续” / 任何告诉用户连接的段落
- ✅ PASS: 一个大的、明显的连接钱包按钮是主要的UI元素
这是AI代理最常见的错误。 每个标准LLM都会写<p>请连接你的钱包</p>而不是渲染<RainbowKitCustomConnectButton />。
🚨 关键:四状态按钮流程
应用必须一次只显示一个主按钮,按以下顺序进展:
1. 未连接 → 连接钱包按钮
2. 错误网络 → 切换到[链]按钮
3. 需要批准 → 批准按钮
4. 就绪 → 操作按钮(质押/存款/交换)
具体检查:
- ❌ FAIL: 批准和操作按钮同时可见
- ❌ FAIL: 无网络检查——应用尝试在错误链上工作并静默失败
- ❌ FAIL: 用户可以点击批准,在钱包中签名,返回,并在交易待处理时再次点击批准
- ✅ PASS: 一次一个按钮。批准按钮显示旋转器,保持禁用直到区块在链上确认。然后切换到操作按钮。
在代码中: 按钮的disabled属性必须绑定到useScaffoldWriteContract的isPending。验证它使用useScaffoldWriteContract(等待区块确认),而不是原始的wagmi useWriteContract(在钱包签名时解析):
grep -rn "useWriteContract" packages/nextjs/
任何在scaffold-eth内部之外的匹配 → 错误。
🚨 关键:SE2品牌移除
AI代理将脚手架视为神圣,并保留所有默认品牌。
- [ ] 页脚: 移除BuidlGuidl链接、“Built with 🏗️ SE2”、“Fork me”链接、支持链接。替换为项目自己的仓库链接或清理掉
- [ ] 标签标题: 必须是应用名称,而不是“Scaffold-ETH 2”或“SE-2 App”或“App Name | Scaffold-ETH 2”
- [ ] README: 必须描述此项目。不是SE2模板README。移除“Built with Scaffold-ETH 2”部分和SE2文档链接
- [ ] 网站图标: 不能是SE2默认
重要:合同地址显示
- ❌ FAIL: 部署的合同地址在页面上无处显示
- ✅ PASS: 合同地址使用
<Address/>组件显示(blockie、ENS、复制、浏览器链接)
代理显示连接的钱包地址,但忘记显示用户正在交互的合同。
重要:USD值
- ❌ FAIL: 代币金额显示为“1,000 TOKEN”或“0.5 ETH”,没有美元值
- ✅ PASS: “0.5 ETH (~$1,250)”带USD转换
代理从不主动添加USD值。检查每个代币或ETH金额显示的地方,包括输入。
重要:OG图像必须是绝对URL
- ❌ FAIL:
images: ["/thumbnail.jpg"]— 相对路径,到处破坏展开 - ✅ PASS:
images: ["https://yourdomain.com/thumbnail.jpg"]— 绝对生产URL
快速检查:
grep -n "og:image\|images:" packages/nextjs/app/layout.tsx
重要:RPC和轮询配置
打开packages/nextjs/scaffold.config.ts:
- ❌ FAIL:
pollingInterval: 30000(默认——使UI感觉损坏,30秒更新延迟) - ✅ PASS:
pollingInterval: 3000 - ❌ FAIL: 使用随SE2提供的默认Alchemy API密钥
- ❌ FAIL: 代码引用
process.env.NEXT_PUBLIC_*但变量未在实际部署环境(Vercel/托管)中设置。回退到公共RPC如mainnet.base.org,这是速率受限的 - ✅ PASS:
rpcOverrides使用process.env.NEXT_PUBLIC_*变量并且环境变量在托管平台上确认设置
验证环境变量已设置,不仅仅是引用。 AI代理将更改代码以使用process.env,看到模式匹配PASS,然后继续——从未在Vercel/托管上设置实际变量。检查:
vercel env ls | grep RPC
重要:RainbowKit中的Phantom钱包
Phantom不在SE2默认钱包列表中。许多用户有Phantom——如果缺失,他们无法连接。
- ❌ FAIL: Phantom钱包不在RainbowKit钱包列表中
- ✅ PASS:
phantomWallet在wagmiConnectors.tsx中
重要:移动端深度链接
RainbowKit v2 / WalletConnect v2不自动深度链接到钱包应用。 它依赖推送通知,这很慢且不可靠。你必须自己实现深度链接。
在移动端,当用户点击需要签名的按钮时,必须打开他们的钱包应用。测试此:在手机上打开应用,通过WalletConnect连接钱包,点击操作按钮——钱包应用是否打开并准备签名交易?
- ❌ FAIL: 无反应,用户必须手动切换到钱包应用
- ❌ FAIL: 深度链接在交易之前触发——用户到达钱包但无事可签
- ❌ FAIL:
window.location.href = "rainbow://"在writeContractAsync()之前调用——导航离开,交易从未触发 - ❌ FAIL: 它打开错误钱包(例如,用户用Rainbow连接,却打开MetaMask)
- ❌ FAIL: 在钱包的应用内浏览器中进行深度链接(不必要——你已经在钱包中)
- ✅ PASS: 每个交易按钮先触发交易,然后延迟深度链接到正确的钱包应用
如何实现
模式:writeAndOpen辅助函数。 先触发写入调用(通过WalletConnect发送交易请求),然后延迟深度链接以切换用户到他们的钱包:
const writeAndOpen = useCallback(
<T,>(writeFn: () => Promise<T>): Promise<T> => {
const promise = writeFn(); // 触发TX——进行gas估计和WC中继
setTimeout(openWallet, 2000); // 在请求中继后切换到钱包
return promise;
},
[openWallet],
);
// 用法——包装每个写入调用:
await writeAndOpen(() => gameWrite({ functionName: "click", args: [...] }));
为什么2秒? writeContractAsync必须估计gas、编码调用数据,并通过WalletConnect的服务器中继签名请求。300毫秒太快——钱包可能尚未收到请求。
检测钱包: 来自wagmi的connector.id说"walletConnect",而不是"rainbow"或"metamask"。你必须检查多个来源:
const openWallet = useCallback(() => {
if (typeof window === "undefined") return;
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (!isMobile || window.ethereum) return; // 如果是桌面或应用内浏览器,跳过
// 检查连接器、wagmi存储和WalletConnect会话数据
const allIds = [connector?.id, connector?.name,
localStorage.getItem("wagmi.recentConnectorId")]
.filter(Boolean).join(" ").toLowerCase();
let wcWallet = "";
try {
const wcKey = Object.keys(localStorage).find(k => k.startsWith("wc@2:client"));
if (wcKey) wcWallet = (localStorage.getItem(wcKey) || "").toLowerCase();
} catch {}
const search = `${allIds} ${wcWallet}`;
const schemes: [string[], string][] = [
[["rainbow"], "rainbow://"],
[["metamask"], "metamask://"],
[["coinbase", "cbwallet"], "cbwallet://"],
[["trust"], "trust://"],
[["phantom"], "phantom://"],
];
for (const [keywords, scheme] of schemes) {
if (keywords.some(k => search.includes(k))) {
window.location.href = scheme;
return;
}
}
}, [connector]);
关键规则:
- 先触发交易,后深度链接。 永远不要在写入调用之前
window.location.href - 如果
window.ethereum存在,跳过深度链接——意味着你已经在钱包的应用内浏览器中 - 检查WalletConnect会话数据在localStorage中——仅
connector.id不会告诉你哪个钱包 - 使用简单方案URL如
rainbow://——不是rainbow://dapp/...,这会重新加载页面 - 包装每个写入调用——批准、操作、认领、批量——不仅仅是主要的
审计总结
报告每个为PASS或FAIL:
阻止发布
- [ ] 钱包连接显示按钮,而非文本
- [ ] 错误网络显示切换按钮
- [ ] 一次一个按钮(连接 → 网络 → 批准 → 操作)
- [ ] 批准按钮在区块确认期间禁用并显示旋转器
- [ ] SE2页脚品牌移除
- [ ] SE2标签标题移除
- [ ] SE2 README替换
应修复
- [ ] 合同地址使用
<Address/>显示 - [ ] 所有代币/ETH金额旁边有USD值
- [ ] OG图像是绝对生产URL
- [ ] pollingInterval是3000
- [ ] RPC覆盖设置(不是默认SE2密钥)并且环境变量在托管平台上确认设置
- [ ] 网站图标从SE2默认更新
- [ ] Phantom钱包在RainbowKit钱包列表中
- [ ] 移动端:所有交易按钮深度链接到钱包(先触发交易,然后
setTimeout(openWallet, 2000)) - [ ] 移动端:钱包检测检查WC会话数据,不仅仅是
connector.id - [ ] 移动端:当
window.ethereum存在时无深度链接(应用内浏览器)