qa
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesedApp QA — Pre-Ship Audit
dApp QA — 预发布审核
This skill is for review, not building. Give it to a fresh agent after the dApp is built. The reviewer should:
- Read the source code (,
app/,components/)contracts/ - Open the app in a browser and click through every flow
- Check every item below — report PASS/FAIL, don't fix
本技能仅用于审核,而非构建。请在dApp构建完成后交给全新的Agent使用。审核人员需要:
- 阅读源代码(、
app/、components/目录)contracts/ - 在浏览器中打开应用,遍历所有流程
- 检查以下每一项——仅记录通过/不通过,无需修复
🚨 Critical: Wallet Flow — Button Not Text
🚨 关键项:钱包流程——显示按钮而非文本
Open the app with NO wallet connected.
- ❌ FAIL: Text saying "Connect your wallet to play" / "Please connect to continue" / any paragraph telling the user to connect
- ✅ PASS: A big, obvious Connect Wallet button is the primary UI element
This is the most common AI agent mistake. Every stock LLM writes a instead of rendering .
<p>Please connect your wallet</p><RainbowKitCustomConnectButton />在未连接钱包的情况下打开应用。
- ❌ 不通过: 显示文本如“连接钱包以开始使用”/“请连接钱包以继续”/任何告知用户连接钱包的段落
- ✅ 通过: 一个显眼的大尺寸「连接钱包」按钮作为主要UI元素
**这是AI Agent最常犯的错误。**所有基础LLM都会生成标签,而非渲染组件。
<p>Please connect your wallet</p><RainbowKitCustomConnectButton />🚨 Critical: Four-State Button Flow
🚨 关键项:四状态按钮流程
The app must show exactly ONE primary button at a time, progressing through:
1. Not connected → Connect Wallet button
2. Wrong network → Switch to [Chain] button
3. Needs approval → Approve button
4. Ready → Action button (Stake/Deposit/Swap)Check specifically:
- ❌ FAIL: Approve and Action buttons both visible simultaneously
- ❌ FAIL: No network check — app tries to work on wrong chain and fails silently
- ❌ FAIL: User can click Approve, sign in wallet, come back, and click Approve again while tx is pending
- ✅ PASS: One button at a time. Approve button shows spinner, stays disabled until block confirms onchain. Then switches to the action button.
In the code: the button's prop must be tied to from . Verify it uses (waits for block confirmation), NOT raw wagmi (resolves on wallet signature):
disabledisPendinguseScaffoldWriteContractuseScaffoldWriteContractuseWriteContractgrep -rn "useWriteContract" packages/nextjs/Any match outside scaffold-eth internals → bug.
应用必须始终仅显示一个主按钮,按以下流程切换:
1. 未连接 → 连接钱包按钮
2. 网络错误 → 切换至[对应链]按钮
3. 需要授权 → 授权按钮
4. 准备完成 → 操作按钮(质押/存入/兑换)需重点检查:
- ❌ 不通过: 授权按钮和操作按钮同时可见
- ❌ 不通过: 未做网络检查——应用在错误链上尝试运行且无提示失败
- ❌ 不通过: 用户点击授权、在钱包中签名后返回,可再次点击授权按钮(即使交易处于待处理状态)
- ✅ 通过: 同一时间仅显示一个按钮。授权按钮显示加载动画,在链上区块确认前保持禁用状态,之后切换为操作按钮。
代码检查: 按钮的属性必须与返回的绑定。需验证使用的是(等待区块确认),而非原生wagmi的(仅在钱包签名后即完成):
disableduseScaffoldWriteContractisPendinguseScaffoldWriteContractuseWriteContractgrep -rn "useWriteContract" packages/nextjs/任何非Scaffold-ETH内部代码的匹配结果均为Bug。
🚨 Critical: SE2 Branding Removal
🚨 关键项:移除SE2品牌标识
AI agents treat the scaffold as sacred and leave all default branding in place.
- Footer: Remove BuidlGuidl links, "Built with 🏗️ SE2", "Fork me" link, support links. Replace with project's own repo link or clean it out
- Tab title: Must be the app name, NOT "Scaffold-ETH 2" or "SE-2 App" or "App Name | Scaffold-ETH 2"
- README: Must describe THIS project. Not the SE2 template README. Remove "Built with Scaffold-ETH 2" sections and SE2 doc links
- Favicon: Must not be the SE2 default
AI Agent会默认保留模板的所有品牌标识。
- 页脚: 移除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默认图标
Important: Contract Address Display
重要项:合约地址展示
- ❌ FAIL: The deployed contract address appears nowhere on the page
- ✅ PASS: Contract address displayed using component (blockie, ENS, copy, explorer link)
<Address/>
Agents display the connected wallet address but forget to show the contract the user is interacting with.
- ❌ 不通过: 页面上未显示已部署的合约地址
- ✅ 通过: 使用组件展示合约地址(包含区块标识、ENS、复制功能、浏览器链接)
<Address/>
AI Agent通常会显示已连接的钱包地址,但会忘记展示用户交互的合约地址。
Important: USD Values
重要项:USD价值显示
- ❌ FAIL: Token amounts shown as "1,000 TOKEN" or "0.5 ETH" with no dollar value
- ✅ PASS: "0.5 ETH (~$1,250)" with USD conversion
Agents never add USD values unprompted. Check every place a token or ETH amount is displayed, including inputs.
- ❌ 不通过: 仅显示“1,000 TOKEN”或“0.5 ETH”等代币数量,未附带美元价值
- ✅ 通过: 显示如“0.5 ETH (~$1,250)”的格式,附带USD换算值
AI Agent不会主动添加USD价值显示。需检查所有显示代币/ETH数量的位置,包括输入框。
Important: OG Image Must Be Absolute URL
重要项:OG图片必须为绝对URL
- ❌ FAIL: — relative path, breaks unfurling everywhere
images: ["/thumbnail.jpg"] - ✅ PASS: — absolute production URL
images: ["https://yourdomain.com/thumbnail.jpg"]
Quick check:
grep -n "og:image\|images:" packages/nextjs/app/layout.tsx- ❌ 不通过: —— 相对路径会导致所有场景下的链接展开失败
images: ["/thumbnail.jpg"] - ✅ 通过: —— 使用生产环境的绝对URL
images: ["https://yourdomain.com/thumbnail.jpg"]
快速检查命令:
grep -n "og:image\|images:" packages/nextjs/app/layout.tsxImportant: RPC & Polling Config
重要项:RPC与轮询配置
Open :
packages/nextjs/scaffold.config.ts- ❌ FAIL: (default — makes the UI feel broken, 30 second update lag)
pollingInterval: 30000 - ✅ PASS:
pollingInterval: 3000 - ❌ FAIL: Using default Alchemy API key that ships with SE2
- ✅ PASS: uses
rpcOverridesvariablesprocess.env.NEXT_PUBLIC_*
打开文件:
packages/nextjs/scaffold.config.ts- ❌ 不通过: (默认值——会导致UI响应卡顿,存在30秒更新延迟)
pollingInterval: 30000 - ✅ 通过:
pollingInterval: 3000 - ❌ 不通过: 使用SE2模板默认的Alchemy API密钥
- ✅ 通过: 使用
rpcOverrides环境变量process.env.NEXT_PUBLIC_*
Important: Phantom Wallet in RainbowKit
重要项:RainbowKit中添加Phantom钱包
Phantom is NOT in the SE2 default wallet list. A lot of users have Phantom — if it's missing, they can't connect.
- ❌ FAIL: Phantom wallet not in the RainbowKit wallet list
- ✅ PASS: is in
phantomWalletwagmiConnectors.tsx
Phantom钱包不在SE2默认钱包列表中。大量用户使用Phantom钱包——若缺失该选项,用户将无法连接。
- ❌ 不通过: RainbowKit钱包列表中无Phantom钱包
- ✅ 通过: 中包含
wagmiConnectors.tsxphantomWallet
Important: Mobile Deep Linking
重要项:移动端深度链接
RainbowKit v2 / WalletConnect v2 does NOT auto-deep-link to the wallet app. It relies on push notifications instead, which are slow and unreliable. You must implement deep linking yourself.
On mobile, when a user taps a button that needs a signature, it must open their wallet app. Test this: open the app on a phone, connect a wallet via WalletConnect, tap an action button — does the wallet app open with the transaction ready to sign?
- ❌ FAIL: Nothing happens, user has to manually switch to their wallet app
- ❌ FAIL: Deep link fires BEFORE the transaction — user arrives at wallet with nothing to sign
- ❌ FAIL: called before
window.location.href = "rainbow://"— navigates away and the TX never fireswriteContractAsync() - ❌ FAIL: It opens the wrong wallet (e.g. opens MetaMask when user connected with Rainbow)
- ❌ FAIL: Deep links inside a wallet's in-app browser (unnecessary — you're already in the wallet)
- ✅ PASS: Every transaction button fires the TX first, then deep links to the correct wallet app after a delay
RainbowKit v2 / WalletConnect v2不会自动跳转到钱包App,它依赖推送通知实现跳转,这种方式缓慢且不可靠。你必须自行实现深度链接。
在移动端,当用户点击需要签名的按钮时,必须直接打开他们的钱包App。测试方法:在手机上打开应用,通过WalletConnect连接钱包,点击操作按钮——钱包App是否会打开并显示待签名的交易?
- ❌ 不通过: 无任何反应,用户需手动切换至钱包App
- ❌ 不通过: 深度链接在交易发起前触发——用户打开钱包后无待签名交易
- ❌ 不通过: 在调用前执行
writeContractAsync()——页面跳转后交易未发起window.location.href = "rainbow://" - ❌ 不通过: 打开错误的钱包(例如用户连接的是Rainbow钱包,却打开了MetaMask)
- ❌ 不通过: 在钱包内置浏览器中触发深度链接(无此必要——已处于钱包环境内)
- ✅ 通过: 所有交易按钮先发起交易,延迟后再深度链接至对应钱包App
How to implement it
实现方法
Pattern: helper. Fire the write call first (sends the TX request over WalletConnect), then deep link after a delay to switch the user to their wallet:
writeAndOpentypescript
const writeAndOpen = useCallback(
<T,>(writeFn: () => Promise<T>): Promise<T> => {
const promise = writeFn(); // Fire TX — does gas estimation + WC relay
setTimeout(openWallet, 2000); // Switch to wallet AFTER request is relayed
return promise;
},
[openWallet],
);
// Usage — wraps every write call:
await writeAndOpen(() => gameWrite({ functionName: "click", args: [...] }));Why 2 seconds? must estimate gas, encode calldata, and relay the signing request through WalletConnect's servers. 300ms is too fast — the wallet won't have received the request yet.
writeContractAsyncDetecting the wallet: from wagmi says , NOT or . You must check multiple sources:
connector.id"walletConnect""rainbow""metamask"typescript
const openWallet = useCallback(() => {
if (typeof window === "undefined") return;
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (!isMobile || window.ethereum) return; // Skip if desktop or in-app browser
// Check connector, wagmi storage, AND WalletConnect session data
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]);Key rules:
- Fire TX first, deep link second. Never before the write call
window.location.href - Skip deep link if exists — means you're already in the wallet's in-app browser
window.ethereum - Check WalletConnect session data in localStorage — alone won't tell you which wallet
connector.id - Use simple scheme URLs like — not
rainbow://which reloads the pagerainbow://dapp/... - Wrap EVERY write call — approve, action, claim, batch — not just the main one
推荐模式:辅助函数。先发起写交易调用(通过WalletConnect发送交易请求),延迟后再触发深度链接将用户切换至钱包:
writeAndOpentypescript
const writeAndOpen = useCallback(
<T,>(writeFn: () => Promise<T>): Promise<T> => {
const promise = writeFn(); // 发起交易——执行gas估算 + WC中继
setTimeout(openWallet, 2000); // 请求中继完成后切换至钱包
return promise;
},
[openWallet],
);
// 使用方式——包裹所有写交易调用:
await writeAndOpen(() => gameWrite({ functionName: "click", args: [...] }));为什么设置2秒延迟? 需要完成gas估算、编码调用数据,并通过WalletConnect服务器中继签名请求。300ms太快——钱包此时尚未收到请求。
writeContractAsync钱包检测: wagmi的返回的是,而非或。你需要检查多个数据源:
connector.id"walletConnect""rainbow""metamask"typescript
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会话数据——仅靠无法判断具体钱包
connector.id - 使用简单的Scheme URL,如——不要使用
rainbow://这类会导致页面重载的链接rainbow://dapp/... - 包裹所有写交易调用——包括授权、操作、领取、批量操作等,而非仅主操作按钮
Audit Summary
审核总结
Report each as PASS or FAIL:
请记录以下各项的通过/不通过状态:
Ship-Blocking
阻塞发布项
- Wallet connection shows a BUTTON, not text
- Wrong network shows a Switch button
- One button at a time (Connect → Network → Approve → Action)
- Approve button disabled with spinner through block confirmation
- SE2 footer branding removed
- SE2 tab title removed
- SE2 README replaced
- 钱包连接显示按钮而非文本
- 网络错误时显示切换链按钮
- 按钮按流程切换(连接→网络→授权→操作),同一时间仅显示一个
- 授权按钮在区块确认过程中显示加载动画并保持禁用
- 移除SE2页脚品牌标识
- 移除SE2标签页标题
- 替换SE2默认README
Should Fix
建议修复项
- Contract address displayed with
<Address/> - USD values next to all token/ETH amounts
- OG image is absolute production URL
- pollingInterval is 3000
- RPC overrides set (not default SE2 key)
- Favicon updated from SE2 default
- Phantom wallet in RainbowKit wallet list
- Mobile: ALL transaction buttons deep link to wallet (fire TX first, then )
setTimeout(openWallet, 2000) - Mobile: wallet detection checks WC session data, not just
connector.id - Mobile: no deep link when exists (in-app browser)
window.ethereum
- 使用组件展示合约地址
<Address/> - 所有代币/ETH数量旁显示USD价值
- OG图片使用生产环境绝对URL
- pollingInterval设置为3000
- 配置RPC覆盖(不使用SE2默认密钥)
- 更新网站图标(替换SE2默认图标)
- RainbowKit钱包列表中包含Phantom钱包
- 移动端:所有交易按钮实现钱包深度链接(先发起交易,再执行)
setTimeout(openWallet, 2000) - 移动端:钱包检测需检查WC会话数据,而非仅依赖
connector.id - 移动端:存在时(内置浏览器)不触发深度链接
window.ethereum