diff --git a/.claude/skills/add-chain/SKILL.md b/.claude/skills/add-chain/SKILL.md new file mode 100644 index 0000000..13c00a4 --- /dev/null +++ b/.claude/skills/add-chain/SKILL.md @@ -0,0 +1,332 @@ +--- +name: add-chain +description: Add a new EVM chain and/or tokens to the lintent demo app (lintent.org / cat-swapper) by editing src/lib/config.ts. Use when the user wants to add a chain (display name + RPC URL), support a new network, or add select tokens for demoing the service. Handles viem-supported and non-viem chains, custom RPCs, native + ERC-20 tokens, and runs RPC/codehash/Polymer verification. Does not commit or open a PR. +--- + +# Add a chain / tokens to lintent + +You add a new EVM chain and/or tokens to the lintent demo app. **Everything lives in +one file: `src/lib/config.ts`.** This skill carries every pointer you need — **do not +explore the repo**. Be fast and focused. + +## Hard rules + +- Edit **only `src/lib/config.ts`**. Do **not** edit `src/lib/utils/web3-onboard.ts` — + wallet onboarding auto-derives chains from `chainMap` (reads `chain.name` and + `chain.rpcUrls.default.http[0]`), which is exactly why the chain object must carry + the user's display name + custom RPC (see Step 3). +- **No commit, no PR.** Leave the working tree edited; the user reviews. +- **Ask the user questions ONCE, up front** (Step 0), only for missing inputs. After + that, run to completion with **no mid-flow questions** — anything unresolved gets a + safe default plus a warning in the final summary for the user to modify. +- **One blocking gate only:** `bun run check` must pass. Every external check + (codehash / Polymer / RouteMesh) is **non-blocking** — collect results and warn at + the end. +- **Never print, echo, or commit secrets.** For Polymer / RouteMesh API keys, reference + via shell variables only and prefer the local `/polymer/health` route so the key never + touches your shell (Step 5). Also: the **RPC URL you write into `config.ts` is bundled + into the browser client** (onboarding reads it from `chainMap`) **and committed** — it + must be a public, demo-safe endpoint with **no embedded API key/secret**. + +## Step 0 — Gather inputs (ask once, only if missing) + +Typical input is a chain (or chains) plus ≥1 token, but support all three shapes: +**chain + tokens**, **tokens-only** (added to a chain already in the repo), or +**chain-only**. If anything needed below is missing, ask for it all in a **single +batched** question, then proceed. + +Required, per new chain: + +- **display name** (e.g. "Optimism", "Base Sepolia") — **never infer this.** +- **≥1 RPC URL** — HTTPS, ideally browser/CORS-enabled. ⚠️ It is bundled into the client + and committed, so it must be **public/demo-safe with no API key in the URL**. +- **mainnet or testnet.** + +Required, per token: the **chain** it belongs to (new or existing), and either an +**address** (ERC-20) or the literal **`native`** (the chain's gas token). + +Optional (accept now to avoid asking later; otherwise defaulted + warned): + +- `nativeCurrency { name, symbol, decimals }` — only used if the chain is **not** in + viem. Default `{ name: "Ether", symbol: "ETH", decimals: 18 }`. +- a token **display alias** (used as the selector `name`) if you want to override the + fetched symbol or resolve a same-chain name collision. + +## Step 1 — Resolve each chain + +1. **chainId** (inferred, authoritative): + + ```bash + cast chain-id --rpc-url "" + ``` + + If the user also supplied a chainId and it differs → record a warning (don't abort). + +2. **Duplicate-chain guard:** if this chainId already matches an existing `chainMap` + entry (compare against the chain objects' `.id`), **do not create a second chain + entry.** Default to "add tokens to the existing key" mode — reuse that chainKey for + the tokens and skip edit sites 1–4, 6–7. (If the user instead wants to change that + chain's display name or RPC, edit its **existing** chain object + `clients` entry in + place rather than adding a new key.) + +3. **viem support detection:** + + ```bash + bun -e "import * as c from 'viem/chains'; const m=Object.entries(c).filter(([,v])=>v&&v.id===).map(([k])=>k); console.log(JSON.stringify(m))" + ``` + + - Non-empty → the chain is in viem. Pick the **canonical** export: drop any + `*Preconf` variants; for `id === 1` use `mainnet` (the repo aliases it as + `ethereum`). Note all candidates in the summary if more than one. + - `[]` → not in viem → you'll `defineChain` (Step 3, non-viem case). + +4. **mainnet/testnet bucket:** if the chain is in viem, read the export's `.testnet` + flag; **warn** if it conflicts with the user's stated mainnet/testnet. For non-viem + chains, use the user's answer (and you **must** set `testnet: true` explicitly in + `defineChain` for testnets — otherwise `!chainMap[c].testnet` is `!undefined === +true` and the app treats it as mainnet). + +5. **chainKey** (the camelCase repo key, also the TS union member): derive it from the + display name — lowercase the first word, capitalize subsequent words, strip + non-alphanumerics (e.g. "Base Sepolia" → `baseSepolia`, "Optimism" → `optimism`). + It must be a valid JS identifier and unique. Use it **byte-identically** at all 7 + edit sites. You'll present it at the end for the user to rename. + +## Step 2 — Resolve each token + +- **Native** (`native`): entry is + `{ address: ADDRESS_ZERO, name: "", chain: "", decimals: }`. + Do **not** make RPC metadata calls for native — `ADDRESS_ZERO` has no `decimals()`. +- **ERC-20:** fetch metadata over the chain's RPC: + ```bash + cast call "decimals()(uint8)" --rpc-url "" + cast call "symbol()(string)" --rpc-url "" + ``` + The repo's `name` field is a short **selector symbol** (`usdc`, `weth`, …), not the + ERC-20 `name()`. Convention is lowercase, but it isn't strict (e.g. `vbUSDC` exists) — + default to `symbol.toLowerCase()` or the user's alias, matching nearby entries. +- **Naming rules (UI-aware):** + - **Unique within a chain** — `OutputTokenModal` selects by `name + chain`. On a + same-chain collision, alias (e.g. `usdc` vs `usdc.e`). + - **Share the symbol across chains** — `InputTokenModal` groups input tokens by + `name` across chains, so the same asset should keep the same `name` on every chain + (do **not** auto-alias across chains). +- **On failure** (call reverts, returns `bytes32`, or RPC unreachable): use the user's + alias if provided, else a placeholder name derived from the address, and record a + warning. **Do not ask mid-flow.** + +## Step 3 — Apply the 7 edits to `src/lib/config.ts` + +Edit sites (current line anchors; re-open the file to place edits precisely): + +**Site 1 — chain object (top, near L1–23).** + +_viem case_ — wrap the export so the display name + custom RPC reach `chainMap` +(prepend the custom RPC as primary, viem defaults as fallback): + +```ts +import { as Viem } from "viem/chains"; +export const = { + ...Viem, + name: "", + rpcUrls: { + ...Viem.rpcUrls, + default: { + ...Viem.rpcUrls.default, + http: ["", ...Viem.rpcUrls.default.http], + }, + }, +} as const; +``` + +(Add `` to the existing `from "viem/chains"` import block. If the user gave no +custom RPC and accepts viem's display name, a bare `import { as }` +is acceptable — but default to wrapping, since the user supplies name + rpc.) + +_non-viem case_ — define it (mirror the existing `pharos` block): + +```ts +export const = defineChain({ + id: , + name: "", + nativeCurrency: { name: "", symbol: "", decimals: }, + rpcUrls: { default: { http: [""] } }, + testnet: , +}); +``` + +**Site 2 — `chainMap` (L64–77):** add `,`. + +**Site 3 — `chainList(mainnet)` (L80–84):** push `""` into the **mainnet** +array (the `mainnet === true` branch) or the **testnet** array (the `else` branch). +⚠️ This returns `string[]` and is **not** type-checked — forgetting it compiles fine +but the chain won't appear in the UI. The invariant check (Step 4) guards this. + +**Site 4 — `POLYMER_ORACLE` (L43–57):** add +`: ""` using the deterministic per-bucket address: + +- mainnet → `0x0000003E06000007A224AeE90052fA6bb46d43C9` +- testnet → `0xC401b53377b8A71A7cEB820e6a4dC53832343a90` + +**Site 5 — `coinList(mainnet)` (L95–282):** add one object per token to the correct +branch. ERC-20 uses a backtick hex literal; native uses the bare `ADDRESS_ZERO` +constant (no quotes, no backticks): + +```ts +// ERC-20: +{ address: `0x`, name: "", chain: "", decimals: }, +// native gas token: +{ address: ADDRESS_ZERO, name: "", chain: "", decimals: }, +``` + +**Site 6 — `clients` (L389–460):** add a client. Because the custom RPC is already +first in the wrapped chain object, just map over its RPC list: + +```ts +: createPublicClient({ + chain: , + transport: fallback(.rpcUrls.default.http.map((v) => http(v))), +}), +``` + +⚠️ Template off `base`/`megaeth`. **Do NOT copy the `polygon` entry** — it has a +`chain: base` copy-paste bug (L415). + +**Site 7 — `polymerChainIds` (L309–322):** add `: .id,` to keep it +in sync with `chainMap`. It's an `as const` object (not a `Record`) and is +currently not read at runtime, so a missing key neither fails compile nor breaks the +demo — add it for consistency, but it's the lowest-stakes site. + +(Skip `wormholeChainIds` and `WORMHOLE_ORACLE` — Wormhole is commented out in +`getOracle`, L378–381.) + +## Step 4 — Invariant check + compile gate (blocking) + +1. **Invariant check** (deterministic; catches the non-type-checked omissions). List + every occurrence of your chainKey and confirm by eye that it appears in **chainMap, + chainList (correct mainnet/testnet branch), POLYMER_ORACLE, coinList, clients, + polymerChainIds**, and that every new token's `chain` is a real chainKey: + ```bash + grep -n "" src/lib/config.ts + ``` + (`chainList` is the easy miss — it isn't type-checked at all — followed by `coinList` + and `clients`.) +2. **`bun run check` — fix until it has no errors referencing your edits.** This is the + only blocking gate (optionally also `bun run lint`). + ⚠️ This repo can have **pre-existing, unrelated** errors — notably, if `.env` lacks + `PRIVATE_ROUTEMESH_API_KEY` (it's in `.env.example`), `outputFilledVerify.ts` fails to + typecheck regardless of any chain change. So **run `bun run check` once before you + edit** to capture the baseline error set, then treat as blocking only NEW errors that + reference `src/lib/config.ts` or your new `` / token entries. (Don't + `git stash` mid-edit to baseline — it can clobber the user's other working changes.) + +## Step 5 — External verification (non-blocking; collect for the summary) + +Run these, record match/mismatch/absent/unsupported, but **never abort** on them. + +**Infra codehash compare** — proves the protocol contracts are actually deployed on the +new chain (these are deterministic addresses reused across chains). Compare each address +on the new chain vs the reference chain (`base` mainnet / `baseSepolia` testnet): + +```bash +NEW_RPC="" +REF_RPC="https://base-rpc.publicnode.com" # testnet: https://base-sepolia-rpc.publicnode.com +for a in \ + 0x0000000000eC36B683C2E6AC89e9A75989C22a2e \ + 0x00000000000000171ede64904551eeDF3C6C9788 \ + 0x0000000000cd5f7fDEc90a03a31F79E5Fbc6A9Cf \ + 0x000025c3226C00B2Cdc200005a1600509f4e00C0 \ + 0xb912b4c38ab54b94D45Ac001484dEBcbb519Bc2B \ + 0x1fccC0807F25A58eB531a0B5b4bf3dCE88808Ed7 \ + 0x0000003E06000007A224AeE90052fA6bb46d43C9 ; do + printf '%s new=%s ref=%s\n' "$a" "$(cast codehash $a --rpc-url "$NEW_RPC" 2>&1)" "$(cast codehash $a --rpc-url "$REF_RPC" 2>&1)" +done +``` + +(Addresses in order: COIN_FILLER, COMPACT, INPUT_SETTLER_COMPACT_LIFI, +INPUT_SETTLER_ESCROW_LIFI, MULTICHAIN_INPUT_SETTLER_ESCROW, MULTICHAIN_INPUT_SETTLER_COMPACT, +POLYMER_ORACLE-mainnet. For a testnet chain, use the testnet oracle +`0xC401b53377b8A71A7cEB820e6a4dC53832343a90` instead of the last one and `baseSepolia` +as REF.) `new == ref` → match. Codehash `0x0000…0000` or +`0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470` (empty code) → +**absent** (that path won't work on this chain). + +**Polymer support.** Preferred — if `bun run dev` is running, hit the **local route** so +the key stays server-side and never touches your shell: + +```bash +curl -s -X POST http://localhost:5173/polymer/health \ + -H 'Content-Type: application/json' -d '{"chainIds":[],"mainnet":true}' # mainnet:false for testnet +``` + +Fallback — direct API, reading the key from `.env`. ⚠️ This passes the key as a process +argument (visible in `ps`); only on a trusted local machine, and never log/commit it: + +```bash +set -a; . ./.env 2>/dev/null; set +a +URL="https://api.polymer.zone/v1/"; KEY="$PRIVATE_POLYMER_MAINNET_ZONE_API_KEY" # testnet: api.testnet.polymer.zone/v1/ + PRIVATE_POLYMER_TESTNET_ZONE_API_KEY +curl -s -X POST "$URL" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"info_health","params":[{"chain_ids":[]}]}' +``` + +`status[""] == "healthy"` (local route: `failed:false`) → supported. An +`unsupported chain ID(s)` error or `"unhealthy"` → warn (Polymer cross-chain won't work yet). + +**RouteMesh** (only if the key exists; it's in `.env.example` but usually not in `.env`): + +```bash +set -a; . ./.env 2>/dev/null; set +a +if [ -n "$PRIVATE_ROUTEMESH_API_KEY" ]; then + curl -s -X POST "https://lb.routeme.sh/rpc//$PRIVATE_ROUTEMESH_API_KEY" \ + -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' +else echo "RouteMesh: skipped (PRIVATE_ROUTEMESH_API_KEY not set locally)"; fi +``` + +A result matching `` (hex) → supported. Error/empty → warn. + +**CORS:** note that the RPC must allow browser origins, or in-app and wallet reads will +fail in the running app even though `cast`/Node calls succeed. + +## Step 6 — Final summary (your deliverable) + +Output, concisely: + +1. **What was added** — chain(s) + token(s), and the **derived `chainKey`** clearly + marked **"rename here if you want a different key"**, plus any **defaulted** values + (nativeCurrency, token aliases) flagged for modification. +2. **Verification table** — one row per check with ✅ / ⚠️ / ❌: + chainId reachable & matches · `bun run check` · infra codehashes (per address: + match/absent) · Polymer health · RouteMesh · CORS note. +3. **Caveats** — explicitly state that codehash/Polymer/RouteMesh prove _infra presence + / external-service support_, **not** that the full issue → fill → finalise demo path + works end-to-end; for cross-chain demos LI.FI's order server (`order.li.fi`) must + also support the chain. +4. **Usage instructions:** + - `bun run dev` and open the app. + - Toggle mainnet/testnet to match the chain's bucket. + - Select the new chain and token(s) in the input/output token modals; confirm the + wallet can switch to the chain. Note: a **native gas token** named `eth` is filtered + out of the **input**-token selector by design (`InputTokenModal`), so for input-side + demos add at least one ERC-20. +5. Remind the user nothing was committed. + +## Reference appendix + +**Infra constants** (from `config.ts` L25–57; reused across chains): + +``` +ADDRESS_ZERO 0x0000000000000000000000000000000000000000 +COIN_FILLER 0x0000000000eC36B683C2E6AC89e9A75989C22a2e +COMPACT 0x00000000000000171ede64904551eeDF3C6C9788 +INPUT_SETTLER_COMPACT_LIFI 0x0000000000cd5f7fDEc90a03a31F79E5Fbc6A9Cf +INPUT_SETTLER_ESCROW_LIFI 0x000025c3226C00B2Cdc200005a1600509f4e00C0 +MULTICHAIN_INPUT_SETTLER_ESCROW 0xb912b4c38ab54b94D45Ac001484dEBcbb519Bc2B +MULTICHAIN_INPUT_SETTLER_COMPACT 0x1fccC0807F25A58eB531a0B5b4bf3dCE88808Ed7 +POLYMER_ORACLE (mainnet bucket) 0x0000003E06000007A224AeE90052fA6bb46d43C9 +POLYMER_ORACLE (testnet bucket) 0xC401b53377b8A71A7cEB820e6a4dC53832343a90 +``` + +**Reference chains / RPCs:** mainnet → `base`, `https://base-rpc.publicnode.com`; +testnet → `baseSepolia`, `https://base-sepolia-rpc.publicnode.com`. + +**Deps** (already present): `cast` (foundry), `bun`, `viem`, `curl`. diff --git a/.gitignore b/.gitignore index 63871d7..43ffa11 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,8 @@ node_modules .wrangler /.svelte-kit /build -.claude +.claude/* +!.claude/skills/ # OS .DS_Store