--- name: qa description: Pre-ship audit checklist for Ethereum dApps built with Scaffold-ETH 2. Give this to a separate reviewer agent (or fresh context) AFTER the build is complete. Use this skill whenever you are finalizing a dApp built with Scaffold-ETH 2. --- # dApp QA β€” Pre-Ship Audit For Scaffold-ETH 2 Builds ## What You Probably Got Wrong **"The app deployed, so we are done."** For SE2 builds, shipping includes UX correctness, metadata, RPC reliability, contract verification, and branding cleanup. **"The flow is obvious."** If Connect, Network, Approve, and Action are not strictly one-at-a-time with proper pending states, users will make duplicate or failing transactions. **"SE2 defaults are fine in production."** Default README/footer/title/favicon and default RPC fallbacks are template scaffolding, not production decisions. **"Pass means no console errors."** QA pass/fail here is behavioral and user-facing: real wallet flow, mobile deep-link behavior, readable errors, and trust signals must be validated. Give this 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 --- ## 🚨 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 `

Please connect your wallet

` instead of rendering ``. --- ## 🚨 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:** Main onchain CTA renders instead of a "Switch to [Chain]" button when the connected wallet is on the wrong network. SE-2's header `WrongNetworkDropdown` is **not sufficient** β€” the action button itself must become the switch CTA, or the user clicks Sign/Stake/Deposit on the wrong chain and eats a silent wagmi error. - ❌ **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. - βœ… **PASS:** Action button's render path branches on `useChainId() === targetNetwork.id` (or equivalent); mismatch renders a `useSwitchChain`-driven "Switch to [Chain]" button in the **same slot** as the primary CTA. **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. **Watch out: two gaps, both allow double-approve.** `isPending` from wagmi drops to `false` when the wallet returns the tx hash β€” not when the tx confirms. `writeContractAsync` is still awaiting confirmation. During that window `isPending = false` AND `approveCooldown = false` β†’ button re-enables mid-flight. Fix requires TWO states: - `approvalSubmitting` β€” set at top of handler, cleared in `finally {}` (covers clickβ†’hash gap) - `approveCooldown` β€” set after `await` resolves, cleared after 4s + refetch (covers confirmβ†’cache gap) ```tsx const [approvalSubmitting, setApprovalSubmitting] = useState(false); const [approveCooldown, setApproveCooldown] = useState(false); const handleApprove = async () => { if (approvalSubmitting || approveCooldown) return; setApprovalSubmitting(true); try { await approveWrite({ functionName: "approve", args: [spender, amount] }); setApproveCooldown(true); setTimeout(() => { setApproveCooldown(false); refetchAllowance(); }, 4000); } catch (e) { notifyError("Approval failed"); } finally { setApprovalSubmitting(false); // must be finally β€” releases on rejection too } }; ``` **The fix:** Remove `loading` from the button class, add an inline `loading-spinner` span inside the button alongside the text: ```tsx // βœ… PASS β€” small spinner inside the button, text visible next to it ``` **Check for this in code:** ```bash grep -rn '"loading"' packages/nextjs/app/ ``` Any `"loading"` string in a button's className β†’ **FAIL**. - ❌ **FAIL:** `className={... isPending ? "loading" : ""}` on a button - βœ… **PASS:** `` inside the button --- ## Important: SE2 Pill-Shaped Inputs (`--radius-field`) SE2 DaisyUI theme defaults to `--radius-field: 9999rem`, which creates pill-shaped textareas/selects and often clips content. - ❌ **FAIL:** `--radius-field: 9999rem` remains in `packages/nextjs/styles/globals.css` - βœ… **PASS:** `--radius-field` is changed to `0.5rem` (or similar) in both light and dark theme blocks Fix in theme (not per component): ```css /* In BOTH @plugin "daisyui/theme" blocks */ --radius-field: 0.5rem; ``` Do not patch this by sprinkling `rounded-*` utility classes per input; fix it once at theme level. --- ## SE2 References - Docs: https://docs.scaffoldeth.io/ - UI Components: https://ui.scaffoldeth.io/ - SpeedRun Ethereum: https://speedrunethereum.com/ --- ## Audit Summary Report each as PASS or FAIL: ### Ship-Blocking - [ ] Wallet connection shows a BUTTON, not text - [ ] Wrong network shows a Switch button **in the primary CTA slot** (not only in the header dropdown) - [ ] One button at a time (Connect β†’ Network β†’ Approve β†’ Action) - [ ] Approve button locked through full cycle: `approvalSubmitting` (clickβ†’hash), `approveCooldown` (confirmβ†’cache refresh) β€” both states required, both on the `disabled` prop - [ ] Contracts verified on block explorer (Etherscan/Basescan/Arbiscan) β€” source code readable by anyone - [ ] SE2 footer branding removed - [ ] SE2 tab title removed - [ ] SE2 README replaced ### Should Fix - [ ] Contract address displayed with `
` - [ ] Every address input uses `` β€” no raw `` for addresses - [ ] USD values next to all token/ETH amounts - [ ] OG image is absolute production URL - [ ] pollingInterval is 3000 - [ ] RPC overrides set (not default SE2 key) AND env var confirmed set on hosting platform - [ ] Favicon updated from SE2 default - [ ] `--radius-field` in `globals.css` changed from `9999rem` to `0.5rem` (or similar) β€” no pill-shaped textareas - [ ] Every contract error mapped to a human-readable message β€” no silent catch blocks, no raw hex selectors - [ ] No hardcoded dark backgrounds β€” page wrapper uses `bg-base-200 text-base-content` (or `data-theme="dark"` forced + `` removed) - [ ] Button loaders use inline `` β€” NOT `className="... loading"` on the button itself - [ ] 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)