qa

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

dApp 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:
  1. Read the source code (
    app/
    ,
    components/
    ,
    contracts/
    )
  2. Open the app in a browser and click through every flow
  3. Check every item below — report PASS/FAIL, don't fix

本技能仅用于审核,而非构建。请在dApp构建完成后交给全新的Agent使用。审核人员需要:
  1. 阅读源代码(
    app/
    components/
    contracts/
    目录)
  2. 在浏览器中打开应用,遍历所有流程
  3. 检查以下每一项——仅记录通过/不通过,无需修复

🚨 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
<p>Please connect your wallet</p>
instead of rendering
<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
disabled
prop must be tied to
isPending
from
useScaffoldWriteContract
. Verify it uses
useScaffoldWriteContract
(waits for block confirmation), NOT raw wagmi
useWriteContract
(resolves on wallet signature):
grep -rn "useWriteContract" packages/nextjs/
Any match outside scaffold-eth internals → bug.

应用必须始终仅显示一个主按钮,按以下流程切换:
1. 未连接 → 连接钱包按钮
2. 网络错误 → 切换至[对应链]按钮
3. 需要授权 → 授权按钮
4. 准备完成 → 操作按钮(质押/存入/兑换)
需重点检查:
  • 不通过: 授权按钮和操作按钮同时可见
  • 不通过: 未做网络检查——应用在错误链上尝试运行且无提示失败
  • 不通过: 用户点击授权、在钱包中签名后返回,可再次点击授权按钮(即使交易处于待处理状态)
  • 通过: 同一时间仅显示一个按钮。授权按钮显示加载动画,在链上区块确认前保持禁用状态,之后切换为操作按钮。
代码检查: 按钮的
disabled
属性必须与
useScaffoldWriteContract
返回的
isPending
绑定。需验证使用的是
useScaffoldWriteContract
(等待区块确认),而非原生wagmi的
useWriteContract
(仅在钱包签名后即完成):
grep -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
    <Address/>
    component (blockie, ENS, copy, explorer link)
Agents display the connected wallet address but forget to show the contract the user is interacting with.

  • 不通过: 页面上未显示已部署的合约地址
  • 通过: 使用
    <Address/>
    组件展示合约地址(包含区块标识、ENS、复制功能、浏览器链接)
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:
    images: ["/thumbnail.jpg"]
    — relative path, breaks unfurling everywhere
  • PASS:
    images: ["https://yourdomain.com/thumbnail.jpg"]
    — absolute production URL
Quick check:
grep -n "og:image\|images:" packages/nextjs/app/layout.tsx

  • 不通过:
    images: ["/thumbnail.jpg"]
    —— 相对路径会导致所有场景下的链接展开失败
  • 通过:
    images: ["https://yourdomain.com/thumbnail.jpg"]
    —— 使用生产环境的绝对URL
快速检查命令:
grep -n "og:image\|images:" packages/nextjs/app/layout.tsx

Important: RPC & Polling Config

重要项:RPC与轮询配置

Open
packages/nextjs/scaffold.config.ts
:
  • FAIL:
    pollingInterval: 30000
    (default — makes the UI feel broken, 30 second update lag)
  • PASS:
    pollingInterval: 3000
  • FAIL: Using default Alchemy API key that ships with SE2
  • PASS:
    rpcOverrides
    uses
    process.env.NEXT_PUBLIC_*
    variables

打开
packages/nextjs/scaffold.config.ts
文件:
  • 不通过:
    pollingInterval: 30000
    (默认值——会导致UI响应卡顿,存在30秒更新延迟)
  • 通过:
    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:
    phantomWallet
    is in
    wagmiConnectors.tsx

Phantom钱包不在SE2默认钱包列表中。大量用户使用Phantom钱包——若缺失该选项,用户将无法连接。
  • 不通过: RainbowKit钱包列表中无Phantom钱包
  • 通过:
    wagmiConnectors.tsx
    中包含
    phantomWallet

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:
    window.location.href = "rainbow://"
    called before
    writeContractAsync()
    — navigates away and the TX never fires
  • 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:
writeAndOpen
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:
typescript
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?
writeContractAsync
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.
Detecting the wallet:
connector.id
from wagmi says
"walletConnect"
, NOT
"rainbow"
or
"metamask"
. You must check multiple sources:
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:
  1. Fire TX first, deep link second. Never
    window.location.href
    before the write call
  2. Skip deep link if
    window.ethereum
    exists
    — means you're already in the wallet's in-app browser
  3. Check WalletConnect session data in localStorage —
    connector.id
    alone won't tell you which wallet
  4. Use simple scheme URLs like
    rainbow://
    — not
    rainbow://dapp/...
    which reloads the page
  5. Wrap EVERY write call — approve, action, claim, batch — not just the main one

推荐模式:
writeAndOpen
辅助函数
。先发起写交易调用(通过WalletConnect发送交易请求),延迟后再触发深度链接将用户切换至钱包:
typescript
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秒延迟?
writeContractAsync
需要完成gas估算、编码调用数据,并通过WalletConnect服务器中继签名请求。300ms太快——钱包此时尚未收到请求。
钱包检测: 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]);
核心规则:
  1. 先发起交易,再触发深度链接。绝对不要在写交易调用前执行
    window.location.href
  2. window.ethereum
    存在时跳过深度链接
    ——表示当前处于钱包内置浏览器
  3. 检查WalletConnect会话数据——仅靠
    connector.id
    无法判断具体钱包
  4. 使用简单的Scheme URL,如
    rainbow://
    ——不要使用
    rainbow://dapp/...
    这类会导致页面重载的链接
  5. 包裹所有写交易调用——包括授权、操作、领取、批量操作等,而非仅主操作按钮

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
    window.ethereum
    exists (in-app browser)
  • 使用
    <Address/>
    组件展示合约地址
  • 所有代币/ETH数量旁显示USD价值
  • OG图片使用生产环境绝对URL
  • pollingInterval设置为3000
  • 配置RPC覆盖(不使用SE2默认密钥)
  • 更新网站图标(替换SE2默认图标)
  • RainbowKit钱包列表中包含Phantom钱包
  • 移动端:所有交易按钮实现钱包深度链接(先发起交易,再执行
    setTimeout(openWallet, 2000)
  • 移动端:钱包检测需检查WC会话数据,而非仅依赖
    connector.id
  • 移动端:
    window.ethereum
    存在时(内置浏览器)不触发深度链接