diff --git a/.tix/issues.jsonl b/.tix/issues.jsonl index 8748960..00282b9 100644 --- a/.tix/issues.jsonl +++ b/.tix/issues.jsonl @@ -40,3 +40,4 @@ {"type":"comment","id":"01KQ00SHT9A0933RQ0AN9ZBVGQ","issue_id":"RG-bd4d82","author":"claude","body":"PR: https://github.com/fielding/ripguard/pull/8 — behavior-preserving registry introduction. Call-site refactor lands in RG-9430aa.","created_at":1777043556169} {"type":"issue","id":"RG-9430aa","title":"Refactor create + vaults pages to useChainId() lookups","body":"","status":"in_progress","priority":2,"assignee":"","created_at":1777031317780,"updated_at":1777083286869,"tags":["multichain","evm"]} {"type":"issue","id":"RG-42e939","title":"Per-chain USDC decimals (BNB=18, others=6)","body":"","status":"in_progress","priority":2,"assignee":"","created_at":1777031317799,"updated_at":1777083286894,"tags":["multichain","evm"]} +{"type":"issue","id":"RG-b53952","title":"Update wagmi config for multi-chain array + wrong-chain guard on /create","body":"","status":"in_progress","priority":2,"assignee":"","created_at":1777031317760,"updated_at":1777083687610,"tags":["multichain","evm"]} diff --git a/packages/app/src/app/create/page.tsx b/packages/app/src/app/create/page.tsx index fb6f24d..3561ba1 100644 --- a/packages/app/src/app/create/page.tsx +++ b/packages/app/src/app/create/page.tsx @@ -18,10 +18,11 @@ import { brokerFeeForTreasury, brokerFeePctString, } from "@/config/contracts"; -import { getChainConfig } from "@/config/chains"; +import { getChainConfig, isSupportedDeploymentChain, DEFAULT_CHAIN_ID } from "@/config/chains"; import { erc20Abi, sablierLockupAbi, testUsdcAbi } from "@/config/abis"; import { ShareCard } from "@/components/ShareCard"; import { ErrorBoundary } from "@/components/ErrorBoundary"; +import { WrongChainPanel } from "@/components/WrongChainPanel"; import { useToast } from "@/components/Toast"; import { trackLockCreated, trackLockApproved, trackContractError } from "@/lib/analytics"; import { isUserRejection, extractErrorReason } from "@/lib/errors"; @@ -112,13 +113,22 @@ function CreateLockInner() { const { address, isConnected } = useAccount(); const chainId = useChainId(); + // Defensive lookup: if the wallet is on a chain we don't support, fall + // back to the deployment's default chain so the page can still render. + // The wrong-chain panel below blocks any tx attempt on the unsupported chain. const { sablierLockup, usdc: usdcAddress, usdcDecimals, treasury, explorerUrl, - } = useMemo(() => getChainConfig(chainId), [chainId]); + } = useMemo( + () => + getChainConfig( + isSupportedDeploymentChain(chainId, IS_TESTNET) ? chainId : DEFAULT_CHAIN_ID, + ), + [chainId], + ); const brokerFee = brokerFeeForTreasury(treasury); const brokerFeePct = brokerFeePctString(brokerFee); // 1 USDC minimum, scaled to the chain's decimals (6 on most, 18 on BNB). @@ -596,6 +606,7 @@ function CreateLockInner() {
+ {step === "success" ? ( )} +
diff --git a/packages/app/src/app/page.tsx b/packages/app/src/app/page.tsx index f100b52..066b9b5 100644 --- a/packages/app/src/app/page.tsx +++ b/packages/app/src/app/page.tsx @@ -1,7 +1,7 @@ "use client"; import { PRESETS, IS_TESTNET } from "@/config/contracts"; -import { getChainConfig } from "@/config/chains"; +import { getChainConfig, isSupportedDeploymentChain, DEFAULT_CHAIN_ID } from "@/config/chains"; import { useChainId } from "wagmi"; import Link from "next/link"; import { Header } from "@/components/Header"; @@ -120,7 +120,11 @@ function FAQItem({ export default function Home() { const chainId = useChainId(); - const { sablierLockup, explorerUrl } = getChainConfig(chainId); + // Unconnected users + users on unsupported chains see the default chain's + // contract link so the landing always has somewhere real to point at. + const { sablierLockup, explorerUrl } = getChainConfig( + isSupportedDeploymentChain(chainId, IS_TESTNET) ? chainId : DEFAULT_CHAIN_ID, + ); const sablierExplorerUrl = `${explorerUrl}/address/${sablierLockup}`; const githubUrl = "https://github.com/fielding/ripguard"; diff --git a/packages/app/src/app/vaults/page.tsx b/packages/app/src/app/vaults/page.tsx index 0de1eb9..33ba86f 100644 --- a/packages/app/src/app/vaults/page.tsx +++ b/packages/app/src/app/vaults/page.tsx @@ -13,8 +13,10 @@ import { useWriteContract, useWaitForTransactionReceipt, } from "wagmi"; -import { getChainConfig, type ChainConfig } from "@/config/chains"; +import { getChainConfig, isSupportedDeploymentChain, DEFAULT_CHAIN_ID, type ChainConfig } from "@/config/chains"; +import { IS_TESTNET } from "@/config/contracts"; import { sablierLockupAbi } from "@/config/abis"; +import { WrongChainPanel } from "@/components/WrongChainPanel"; import { ShareCard } from "@/components/ShareCard"; import { ErrorBoundary, CardErrorBoundary } from "@/components/ErrorBoundary"; import { useToast } from "@/components/Toast"; @@ -420,7 +422,12 @@ async function fetchFromChain( function VaultDashboard() { const { address, isConnected } = useAccount(); const chainId = useChainId(); - const chainConfig = getChainConfig(chainId); + // Defensive lookup: unsupported chains fall back to the deployment's + // default chain so the page renders. The wrong-chain panel below blocks + // any tx on the unsupported chain. + const chainConfig = getChainConfig( + isSupportedDeploymentChain(chainId, IS_TESTNET) ? chainId : DEFAULT_CHAIN_ID, + ); const { sablierLockup, usdcDecimals, explorerUrl } = chainConfig; const publicClient = usePublicClient(); const { toast } = useToast(); @@ -813,7 +820,9 @@ export default function Vaults() {
- + + +
diff --git a/packages/app/src/components/WrongChainPanel.tsx b/packages/app/src/components/WrongChainPanel.tsx new file mode 100644 index 0000000..73794d6 --- /dev/null +++ b/packages/app/src/components/WrongChainPanel.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useChainId, useSwitchChain } from "wagmi"; +import { CHAINS, isSupportedDeploymentChain } from "@/config/chains"; +import { IS_TESTNET } from "@/config/contracts"; + +/** Renders a "switch chain" prompt when the wallet is on a chain RipGuard + * doesn't support yet. Returns null when the chain is supported, so the + * caller can wrap their page content like: + * + * ```tsx + * {normalContent} + * ``` + */ +export function WrongChainPanel({ children }: { children: React.ReactNode }) { + const chainId = useChainId(); + const { switchChain, isPending } = useSwitchChain(); + + // Check against the active deployment specifically, not just registry + // membership — Base Sepolia is in the registry, but on the mainnet + // deployment it's still "wrong chain" for our purposes. + if (isSupportedDeploymentChain(chainId, IS_TESTNET)) { + return <>{children}; + } + + const supportedForDeployment = Object.values(CHAINS).filter( + (c) => c.isTestnet === IS_TESTNET + ); + + return ( +
+
Wrong network
+

+ Your wallet is on a chain we don't support yet. +

+

+ RipGuard runs on{" "} + {supportedForDeployment.map((c, i) => ( + + {i > 0 && i === supportedForDeployment.length - 1 ? " and " : i > 0 ? ", " : ""} + {c.name} + + ))} + . Switch your wallet to lock USDC. +

+
+ {supportedForDeployment.map((c) => ( + + ))} +
+
+ ); +} diff --git a/packages/app/src/config/chains.test.ts b/packages/app/src/config/chains.test.ts index e57366b..23563ae 100644 --- a/packages/app/src/config/chains.test.ts +++ b/packages/app/src/config/chains.test.ts @@ -5,6 +5,7 @@ import { SUPPORTED_CHAIN_IDS, getChainConfig, isSupportedChain, + isSupportedDeploymentChain, } from "./chains"; describe("chain registry", () => { @@ -57,4 +58,17 @@ describe("chain registry", () => { expect(isSupportedChain(1)).toBe(false); expect(isSupportedChain(undefined)).toBe(false); }); + + it("isSupportedDeploymentChain filters by deployment's testnet flag", () => { + // Mainnet deployment: Base mainnet ✓, Base Sepolia ✗ + expect(isSupportedDeploymentChain(8453, false)).toBe(true); + expect(isSupportedDeploymentChain(84532, false)).toBe(false); + // Testnet deployment: Base Sepolia ✓, Base mainnet ✗ + expect(isSupportedDeploymentChain(84532, true)).toBe(true); + expect(isSupportedDeploymentChain(8453, true)).toBe(false); + // Unregistered chain: false either way + expect(isSupportedDeploymentChain(999_999, false)).toBe(false); + expect(isSupportedDeploymentChain(999_999, true)).toBe(false); + expect(isSupportedDeploymentChain(undefined, false)).toBe(false); + }); }); diff --git a/packages/app/src/config/chains.ts b/packages/app/src/config/chains.ts index c62fe7f..1753947 100644 --- a/packages/app/src/config/chains.ts +++ b/packages/app/src/config/chains.ts @@ -81,3 +81,20 @@ export function getChainConfig(chainId: number): ChainConfig { export function isSupportedChain(chainId: number | undefined): chainId is number { return chainId !== undefined && chainId in CHAINS; } + +// "Supported by THIS deployment" — registry membership AND testnet flag matches. +// Use this (not isSupportedChain) for any logic that gates on whether the +// current deployment can actually transact on a chain. The registry holds +// both mainnets and testnets; isSupportedChain returns true for any of them, +// which would let a mainnet wallet through the wrong-chain guard on the +// testnet deployment (and vice-versa). +export function isSupportedDeploymentChain( + chainId: number | undefined, + isTestnetDeployment: boolean, +): chainId is number { + return ( + chainId !== undefined && + isSupportedChain(chainId) && + getChainConfig(chainId).isTestnet === isTestnetDeployment + ); +} diff --git a/packages/app/src/config/wagmi.ts b/packages/app/src/config/wagmi.ts index 3a2c24f..f5297e7 100644 --- a/packages/app/src/config/wagmi.ts +++ b/packages/app/src/config/wagmi.ts @@ -1,8 +1,32 @@ import { getDefaultConfig } from "@rainbow-me/rainbowkit"; -import { base, baseSepolia } from "wagmi/chains"; +import * as wagmiChains from "wagmi/chains"; +import { type Chain } from "wagmi/chains"; +import { CHAINS } from "./chains"; + +// Map a registry chainId to its wagmi chain definition. Add an entry here +// when adding a chain to the registry so wagmi can pick it up. +const chainIdToWagmiChain: Record = { + [wagmiChains.base.id]: wagmiChains.base, + [wagmiChains.baseSepolia.id]: wagmiChains.baseSepolia, +}; const isTestnet = process.env.NEXT_PUBLIC_CHAIN === "base-sepolia"; +// Derive wagmi's chain list from the registry, filtered to chains matching +// the deployment's testnet flag. Registry ⇒ wagmi is one-way: adding a +// chain to chains.ts (with a corresponding wagmi mapping above) auto-expands +// the wallet picker. +const supportedChains = Object.values(CHAINS) + .filter((c) => c.isTestnet === isTestnet) + .map((c) => chainIdToWagmiChain[c.chainId]) + .filter((c): c is Chain => c !== undefined); + +if (supportedChains.length === 0) { + throw new Error( + `No wagmi-mapped chains for ${isTestnet ? "testnet" : "mainnet"} deployment. Check chains.ts and the chainIdToWagmiChain map in wagmi.ts.` + ); +} + const wcProjectId = process.env.NEXT_PUBLIC_WC_PROJECT_ID; if (!wcProjectId && process.env.NODE_ENV === "development") { console.warn( @@ -13,6 +37,6 @@ if (!wcProjectId && process.env.NODE_ENV === "development") { export const config = getDefaultConfig({ appName: "RipGuard", projectId: wcProjectId || "YOUR_WC_PROJECT_ID", - chains: [isTestnet ? baseSepolia : base], + chains: supportedChains as [Chain, ...Chain[]], ssr: true, });