# Gotchas, Do's & Don'ts

The practical wisdom for building ChainRender apps well. Read this before you spend gas — most of these
save you from a redeploy, a dropped grade, or a confusing render.

## The big ones (read these first)

### ✅ DO grade and preview locally before you deploy
The SDK's `grade()` equals what the chain computes (drift-locked). Grade, preview, and validate **locally, for
free**, before registering anything. Deploying to "see if it works" wastes gas — you can see everything first.
→ **[Build, Grade & Style Before Deploy](../guides/test-before-deploy.md)**

### ❌ DON'T redeploy to change how it looks
Colors, spacing, fonts, and chart dimensions are **render-time parameters**. Changing them is free and needs no
redeploy — pass a presentation context. A redeploy is only for changing what the app *is* (text, structure,
actions), not how it *looks*. If you're about to redeploy for a cosmetic tweak, stop. → **[Theme Without
Redeploying](../guides/theme-without-redeploy.md)**

### ✅ DO understand what's baked vs. what's render-time
- **Baked into the hash (change = new version):** text/content, structure, roles, declared actions, resolver
  targets. These are the app's *identity*.
- **Render-time (change = free):** theme, palette, chart dimensions, spacing/fonts. These are *presentation*.

Getting this wrong is the #1 source of "why did my hash change?" confusion. A one-character text change is a new
app; a color change is not.

### ❌ DON'T expect a resolver to return HTML
A resolver returns a **plain string**; the interpreter escapes it. Don't try to return markup — it will be escaped,
not rendered. This is a safety feature (a resolver can never inject markup), not a limitation to work around.

## Trust & grade gotchas

- **✅ DO watch your grade as you add features.** Adding an off-chain image or a `CLIENT_FETCH` drops you from A to
  C. The breakdown tells you exactly what did it. Decide the trade-off *before* users see it.
- **❌ DON'T embed off-chain assets if you want Grade A.** An external image = a live dependency = Grade C. Use the
  on-chain media resolvers (generative art, NFT media, SDF scenes) to stay A. → **[Visual & Media](../concepts/visual-media.md)**
- **✅ DO declare exactly the capabilities you use.** Over-declaring (claiming a power you don't use) is refused by
  the enforcer (rail R2 — dishonest envelope). Under-declaring (reaching a power you didn't declare) is also
  refused. Declare precisely what you reach.
- **❌ DON'T try to hide a value transfer in an action.** Moving ETH requires an explicit `VALUE` declaration. An
  action that moves value without it is refused (R4) — you can't sneak it in.

## Action & signing gotchas

- **✅ DO rely on construct-for-sign.** A button's calldata is built from its declaration — it can *only* be the
  declared selector + args. Trust this; don't hand-build calldata.
- **❌ DON'T put arbitrary calldata in a button.** There's no path for it — actions are declared, not raw. A
  `{to, selector}` not in the declared call set is refused (R5).

## Rendering & data gotchas

- **❌ DON'T assume a tree that compiles is the tree you wrote.** `compile()` silently drops unknown fields, enum
  values that aren't in the dictionary (`role: "note"`), a11y fields on images and `args` on batch members. **✅ DO**
  run `checkNode(tree)` first — it reports every one with its path. → **[Node Fields](../reference/node-fields.md)**
- **❌ DON'T guess a resolver arg's encoding.** Use the kind's descriptor: `abi.encode` of its args, or raw bytes when
  it's *packed* (`ensName` = the 20 address bytes). A wrong encoding renders the fallback, not an error.

- **✅ DO design for fail-closed.** A resolver that reverts falls back to "unavailable"; a chart with no data shows
  an axes-only frame. Design your UI so the fallback still reads sensibly — don't assume the data is always there.
- **❌ DON'T expect to enumerate "everything" in one call.** Reading a *specific* value or a *bounded* range is a
  direct on-chain call; listing *all* of something at scale needs an indexer. Plan for that boundary. → **[Live
  Data & Events](../guides/live-data-and-events.md)**
- **✅ DO paginate large outputs.** Big tables/atlases support chunked reads that concatenate byte-identically. Use
  them rather than trying to render everything in one call.
- **❌ DON'T use a font size below 12px.** The legibility floor rejects it. (And it's too small to read anyway.)

## Theming gotchas

- **✅ DO reference tokens, not hardcoded values.** `var(--cr-space-md)`, not `12px`. Token-driven styles re-flow
  when the theme changes; hardcoded ones don't.
- **❌ DON'T ship a low-contrast theme.** The contrast invariant (WCAG AA) rejects it at build time. Fix the token
  pair, don't bypass the check. → **[Themes in Depth](../reference/theming-in-depth.md)**

## Deployment & naming gotchas

- **✅ DO verify after deploy.** Render your deployed hash against multiple RPCs and diff — byte-identical confirms
  it's live and honest. → **[Verifying an App](../contracts/verifying.md)**
- **❌ DON'T treat the stake as a fee.** The subname/submission stake is a **refundable** anti-spam deposit — you
  get it back after the lock window. It's not a payment. → **[Submitting + Staking](../guides/submitting.md)**
- **❌ DON'T look for your submission by your EOA if you use a smart wallet.** The registrar records the
  submitter as `msg.sender`. If you submit through a **smart-contract wallet / account abstraction**
  (ERC-4337/7579), that's your **smart-account address**, not the EOA you signed with — so searching the
  `Registered` event logs filtered by your EOA returns nothing, even though the submit succeeded. Classic
  "I submitted but can't find it under my account." **✅ DO** look it up by the **label** (the registrar
  enumerates submissions by label, account-agnostic), or filter by your **smart-account** address. The
  catalog's *Verified only* filter is immune because it enumerates by label; a future *"my apps"* view must
  match the smart account, not the EOA.
- **✅ DO use versioning for updates.** Don't try to "change" a deployed app — publish a new version (timelocked,
  observable). The old version stays resolvable forever. → **[Update a Deployed App](../guides/update-app.md)**
- **❌ DON'T assume your RPC is private.** The rendered output is trustless, but the RPC sees which app/address you
  query. Use a private/light-client RPC if metadata privacy matters. → **[No Hosting](../concepts/no-hosting.md)**

## The meta-principle

When something surprises you, the answer is almost always one of these two:
1. **"Is this identity or presentation?"** — if the hash changed unexpectedly, you changed identity; if a visual
   change didn't stick, you changed something that needs a render-time context or a re-seed.
2. **"Does this preserve fail-closed + the grade?"** — if a feature dropped your grade or broke a render, check what
   dependency it introduced.

## Related
- **[Troubleshooting](./troubleshooting.md)** — symptom → cause → fix.
- **[Principles & Ethos](./ethos.md)** — the values these do's and don'ts flow from.


## Rich-media & on-chain library gotchas

### three.js must be the r160 UMD build (global `THREE`), not ESM
On-chain three has to be the **UMD build (`three.min.js`, r160)** that sets `window.THREE`. Modern three (r161+) is
ESM-only with no global -- loaded as a plain script it produces `THREE is not defined`, and loaded via an
importmap+blob it's blocked in a strict-CSP sandboxed iframe. Sketches use the **global `THREE`**, never `import`.

### No CDN in the deploy/render path
External scripts (p5/three/gzip from a CDN) are blocked in the sandboxed iframe under a strict CSP. Everything loads
**on-chain, as plain scripts, decompressed with the browser's native `DecompressionStream`**. If you see a
"CDN failed / CSP blocking" message, the app was likely deployed before the on-chain-library fix (a zero `libHash`)
-- re-deploy it to capture the on-chain library.

### Discovery is newest-wins
If you re-upload a library (e.g. swapping ESM three for UMD three), discovery picks the **newest** matching entry by
name -- so the new upload supersedes the old. Confirm on-chain (enumerate the DependencyRegistry) before re-deploying
apps that should use the new one.
