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,
});