From ff503b72508386e5e29f5ce68e6459cada3f97cb Mon Sep 17 00:00:00 2001 From: Fielding Johnston Date: Fri, 24 Apr 2026 10:11:40 -0500 Subject: [PATCH 1/3] chore(tix): multi-EVM + Solana epics, changelog idea, start RG-bd4d82 Carries forward the task setup from the 2026-04-24 planning session plus a newly captured changelog task. Marks RG-bd4d82 as in_progress since the chain registry work is now being landed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .tix/issues.jsonl | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.tix/issues.jsonl b/.tix/issues.jsonl index 7fb875d..21c6128 100644 --- a/.tix/issues.jsonl +++ b/.tix/issues.jsonl @@ -1 +1,39 @@ {"type":"issue","id":"RG-59d954","title":"Brand refresh: new SVG mark + 3D padlock PNG","body":"User wants to replace the current RipGuard mark with a new design this session. Key decisions still open: (a) new SVG icon for the flat mark, (b) whether to refresh the 3D padlock PNG to match (currently based on a bitcoin-looking shape the user had mixed feelings about), (c) whether to remove the tiny shield on top of the padlock. After settling, update all touchpoints.\n\nTouchpoints (confirmed by grep during 2026-04-11 handoff):\n\n1. packages/app/src/components/Brand.tsx — RipGuardMark SVG is hand-coded at lines 11-64. Paste the new SVG path set here. The small shield the user mentioned removing is the path at lines 41-46 ('M66 79V68C66 60.268 72.268 54 80 54C87.732 54 94 60.268 94 68V79').\n2. packages/app/src/components/Brand.tsx — RipGuardMark3D points at /mark-1024.png via next/image. Replace the file, no code change needed unless path changes.\n3. packages/app/public/mark-1024.png — the 3D raster. Regenerate.\n4. packages/app/public/logo-icon.png — used by layout.tsx favicon + apple-icon AND manifest.ts PWA icon. Regenerate.\n5. packages/app/public/og-image.png — used by layout.tsx OpenGraph + Twitter metadata. Regenerate (or rely on the dynamic opengraph-image.tsx route if it generates from the mark).\n6. packages/app/src/app/favicon.ico — browser tab icon.\n7. packages/app/src/app/opengraph-image.tsx and twitter-image.tsx — dynamic OG routes. Check if they reference the mark; update if so.\n\nStale PNGs in packages/app/public/ worth auditing after the new mark lands: icon-192.png, icon-512.png, mark-72.png, mark-288.png, padlock-2048.png, hero-bg.png, hero-full.png, hero-logo.png, hero-wordmark.png, vault-icon.png, vault-logo.png. None are referenced in code grep as of this handoff, likely legacy from earlier iterations.\n\nPlaces the mark is actually rendered:\n- Header.tsx:35 — RipGuardMark at h-9 w-9 (36px)\n- page.tsx:708 — RipGuardMark at h-6 w-6 (footer, 24px)\n- page.tsx:222 — RipGuardMark3D at h-64 w-64 sm:h-80 sm:w-80 lg:h-[26rem] lg:w-[26rem] (hero)\n\nVerify at each size after updating — the flat mark needs to stay legible at 24px AND look right at 36px, and the 3D mark needs to hold detail at the hero size without losing the cyan halo from globals.css blur-[48px].\n\nAfter updating, push main and fast-forward testnet as usual (see CONTEXT.md deployments section).","status":"open","priority":2,"assignee":"","created_at":1775941000817,"updated_at":1775941000817,"tags":["brand","design"]} +{"type":"issue","id":"RG-7ce12d","title":"Multi-EVM chain support (Ethereum, Arbitrum, Optimism, Polygon, Avalanche, BNB)","body":"","status":"open","priority":2,"assignee":"","created_at":1777031317716,"updated_at":1777031317716,"tags":["multichain","evm"]} +{"type":"issue","id":"RG-bd4d82","title":"Build chain registry + refactor config/contracts.ts to getChainConfig()","body":"","status":"open","priority":2,"assignee":"","created_at":1777031317741,"updated_at":1777031317741,"tags":["multichain","evm"]} +{"type":"issue","id":"RG-b53952","title":"Update wagmi config for multi-chain array + wrong-chain guard on /create","body":"","status":"open","priority":2,"assignee":"","created_at":1777031317760,"updated_at":1777031317760,"tags":["multichain","evm"]} +{"type":"issue","id":"RG-9430aa","title":"Refactor create + vaults pages to useChainId() lookups","body":"","status":"open","priority":2,"assignee":"","created_at":1777031317780,"updated_at":1777031317780,"tags":["multichain","evm"]} +{"type":"issue","id":"RG-42e939","title":"Per-chain USDC decimals (BNB=18, others=6)","body":"","status":"open","priority":2,"assignee":"","created_at":1777031317799,"updated_at":1777031317799,"tags":["multichain","evm"]} +{"type":"issue","id":"RG-213ce7","title":"Landing page chain chip row + cross-chain hints","body":"","status":"open","priority":3,"assignee":"","created_at":1777031317819,"updated_at":1777031317819,"tags":["multichain","evm"]} +{"type":"issue","id":"RG-644a80","title":"Per-chain ops: deploy treasuries, verify Sablier+USDC addresses, smoke test","body":"","status":"open","priority":2,"assignee":"","created_at":1777031317838,"updated_at":1777031317838,"tags":["multichain","evm","ops"]} +{"type":"dep","src_id":"RG-bd4d82","dst_id":"RG-b53952","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-bd4d82","dst_id":"RG-9430aa","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-bd4d82","dst_id":"RG-42e939","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-9430aa","dst_id":"RG-213ce7","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-b53952","dst_id":"RG-644a80","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-9430aa","dst_id":"RG-644a80","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-42e939","dst_id":"RG-644a80","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-bd4d82","dst_id":"RG-7ce12d","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-b53952","dst_id":"RG-7ce12d","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-9430aa","dst_id":"RG-7ce12d","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-42e939","dst_id":"RG-7ce12d","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-213ce7","dst_id":"RG-7ce12d","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-644a80","dst_id":"RG-7ce12d","kind":"blocks","state":"active"} +{"type":"issue","id":"RG-d26076","title":"Solana subdomain (sol.ripguard.xyz)","body":"","status":"open","priority":3,"assignee":"","created_at":1777031336457,"updated_at":1777031336457,"tags":["solana"]} +{"type":"issue","id":"RG-762dce","title":"Phase 0 research: SolSab broker fee support, indexer option, IDL + program IDs","body":"","status":"open","priority":3,"assignee":"","created_at":1777031336477,"updated_at":1777031336477,"tags":["solana","research"]} +{"type":"issue","id":"RG-da7b6a","title":"Factor packages/ui + packages/brand out of app (shared with EVM)","body":"","status":"open","priority":3,"assignee":"","created_at":1777031336496,"updated_at":1777031336496,"tags":["refactor"]} +{"type":"issue","id":"RG-5dd036","title":"Scaffold packages/app-sol: Next.js + Solana Wallet Adapter + accent swap","body":"","status":"open","priority":3,"assignee":"","created_at":1777031336515,"updated_at":1777031336515,"tags":["solana"]} +{"type":"issue","id":"RG-4d3d55","title":"Solana create + vaults flows (single-sig lock, ATA bootstrap, stream discovery)","body":"","status":"open","priority":3,"assignee":"","created_at":1777031336534,"updated_at":1777031336534,"tags":["solana"]} +{"type":"issue","id":"RG-1d43a7","title":"Solana subdomain deploy, Squads treasury multisig, mainnet-beta smoke test","body":"","status":"open","priority":3,"assignee":"","created_at":1777031336553,"updated_at":1777031336553,"tags":["solana","ops"]} +{"type":"dep","src_id":"RG-762dce","dst_id":"RG-5dd036","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-762dce","dst_id":"RG-4d3d55","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-da7b6a","dst_id":"RG-5dd036","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-5dd036","dst_id":"RG-4d3d55","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-4d3d55","dst_id":"RG-1d43a7","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-762dce","dst_id":"RG-d26076","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-da7b6a","dst_id":"RG-d26076","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-5dd036","dst_id":"RG-d26076","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-4d3d55","dst_id":"RG-d26076","kind":"blocks","state":"active"} +{"type":"dep","src_id":"RG-1d43a7","dst_id":"RG-d26076","kind":"blocks","state":"active"} +{"type":"issue","id":"RG-bd4d82","title":"Build chain registry + refactor config/contracts.ts to getChainConfig()","body":"","status":"in_progress","priority":2,"assignee":"","created_at":1777031317741,"updated_at":1777043176315,"tags":["multichain","evm"]} +{"type":"issue","id":"RG-b19ddf","title":"Auto-generated changelog (commits → CHANGELOG.md + /changelog page)","body":"","status":"open","priority":3,"assignee":"","created_at":1777043479029,"updated_at":1777043479029,"tags":["docs","infra"]} From 743767cc8c5f912fa30171e9ab1e7a2e18aa2b46 Mon Sep 17 00:00:00 2001 From: Fielding Johnston Date: Fri, 24 Apr 2026 10:11:51 -0500 Subject: [PATCH 2/3] feat(config): chain registry for multi-EVM support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces Record registry in config/chains.ts seeded with Base (8453) and Base Sepolia (84532). Every per-chain value — Sablier Lockup address, USDC address, USDC decimals, treasury, explorer URL, stream start block, log chunk size — lives in the registry now, preparing for the 7-chain rollout (Ethereum, Arbitrum, Optimism, Polygon, Avalanche, BNB to follow in later PRs). usdcDecimals is part of the shape from day one so the page refactor (RG-9430aa) can thread it through the approve/lock flow without another schema churn — BNB's 18-decimal USDC is the sharp edge. config/contracts.ts keeps every existing singleton export but now reads them from getChainConfig(DEFAULT_CHAIN_ID). Behavior on Base mainnet and Base Sepolia is bit-identical to the pre-refactor state; call sites are unchanged. The page-level switch to useChainId() + getChainConfig() and the deletion of these shims lands in RG-9430aa. Refs: RG-bd4d82, RG-42e939 (partial — data shape only) Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/app/src/config/chains.test.ts | 57 ++++++++++++++++++ packages/app/src/config/chains.ts | 83 ++++++++++++++++++++++++++ packages/app/src/config/contracts.ts | 54 ++++++----------- 3 files changed, 157 insertions(+), 37 deletions(-) create mode 100644 packages/app/src/config/chains.test.ts create mode 100644 packages/app/src/config/chains.ts diff --git a/packages/app/src/config/chains.test.ts b/packages/app/src/config/chains.test.ts new file mode 100644 index 0000000..b782f32 --- /dev/null +++ b/packages/app/src/config/chains.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "vitest"; +import { + CHAINS, + DEFAULT_CHAIN_ID, + SUPPORTED_CHAIN_IDS, + getChainConfig, + isSupportedChain, +} from "./chains"; + +describe("chain registry", () => { + it("contains Base mainnet (8453)", () => { + const base = getChainConfig(8453); + expect(base.name).toBe("Base"); + expect(base.chainId).toBe(8453); + expect(base.isTestnet).toBe(false); + expect(base.usdcDecimals).toBe(6); + }); + + it("contains Base Sepolia (84532)", () => { + const sepolia = getChainConfig(84532); + expect(sepolia.chainId).toBe(84532); + expect(sepolia.isTestnet).toBe(true); + expect(sepolia.usdcDecimals).toBe(6); + }); + + it("throws on unsupported chainId", () => { + expect(() => getChainConfig(1)).toThrow(/Unsupported chainId: 1/); + }); + + it("DEFAULT_CHAIN_ID resolves to a registered chain", () => { + expect(() => getChainConfig(DEFAULT_CHAIN_ID)).not.toThrow(); + }); + + it("every chain has valid addresses and positive numerics", () => { + for (const chain of Object.values(CHAINS)) { + expect(chain.sablierLockup, `${chain.name} sablierLockup`).toMatch( + /^0x[a-fA-F0-9]{40}$/ + ); + expect(chain.usdc, `${chain.name} usdc`).toMatch(/^0x[a-fA-F0-9]{40}$/); + expect(chain.usdcDecimals, `${chain.name} usdcDecimals`).toBeGreaterThan(0); + expect(chain.streamStartBlock > BigInt(0), `${chain.name} streamStartBlock`).toBe(true); + expect(chain.logChunkSize > BigInt(0), `${chain.name} logChunkSize`).toBe(true); + expect(chain.explorerUrl, `${chain.name} explorerUrl`).toMatch(/^https:\/\//); + } + }); + + it("SUPPORTED_CHAIN_IDS matches CHAINS keys", () => { + expect(SUPPORTED_CHAIN_IDS.sort()).toEqual(Object.keys(CHAINS).map(Number).sort()); + }); + + it("isSupportedChain recognizes registered chains", () => { + expect(isSupportedChain(8453)).toBe(true); + expect(isSupportedChain(84532)).toBe(true); + expect(isSupportedChain(1)).toBe(false); + expect(isSupportedChain(undefined)).toBe(false); + }); +}); diff --git a/packages/app/src/config/chains.ts b/packages/app/src/config/chains.ts new file mode 100644 index 0000000..c62fe7f --- /dev/null +++ b/packages/app/src/config/chains.ts @@ -0,0 +1,83 @@ +import { type Address } from "viem"; + +export type ChainConfig = { + chainId: number; + name: string; + shortName: string; + sablierLockup: Address; + usdc: Address; + usdcDecimals: number; + treasury: Address; + explorerUrl: string; + streamStartBlock: bigint; + logChunkSize: bigint; + isTestnet: boolean; +}; + +export const ZERO_ADDRESS: Address = "0x0000000000000000000000000000000000000000"; + +const BASE_DEFAULT: ChainConfig = { + chainId: 8453, + name: "Base", + shortName: "Base", + sablierLockup: "0xb5D78DD3276325f5FAF3106Cc4Acc56E28e0Fe3B", + usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + usdcDecimals: 6, + treasury: "0x847F640bE052b0700C31F72Dce622F4C6286934E", + explorerUrl: "https://basescan.org", + streamStartBlock: BigInt(22_000_000), + logChunkSize: BigInt(50_000), + isTestnet: false, +}; + +const BASE_SEPOLIA_DEFAULT: ChainConfig = { + chainId: 84532, + name: "Base Sepolia", + shortName: "Base Sepolia", + sablierLockup: "0xa4777ca525d43a7af55d45b11b430606d7416f8d", + usdc: "0x54C0f145D70ca4792e695697B6498552F1EC0009", + usdcDecimals: 6, + treasury: ZERO_ADDRESS, + explorerUrl: "https://sepolia.basescan.org", + streamStartBlock: BigInt(38_540_000), + logChunkSize: BigInt(10_000), + isTestnet: true, +}; + +export const DEFAULT_CHAIN_ID: number = + process.env.NEXT_PUBLIC_CHAIN === "base-sepolia" + ? BASE_SEPOLIA_DEFAULT.chainId + : BASE_DEFAULT.chainId; + +// Env overrides only apply to the current deployment's default chain. +// Kept for testnet staging flexibility; new chains should put addresses in code. +function withEnvOverrides(cfg: ChainConfig): ChainConfig { + if (cfg.chainId !== DEFAULT_CHAIN_ID) return cfg; + return { + ...cfg, + sablierLockup: (process.env.NEXT_PUBLIC_SABLIER_LOCKUP as Address) || cfg.sablierLockup, + usdc: (process.env.NEXT_PUBLIC_USDC_ADDRESS as Address) || cfg.usdc, + treasury: (process.env.NEXT_PUBLIC_TREASURY_ADDRESS as Address) || cfg.treasury, + }; +} + +export const CHAINS: Record = { + [BASE_DEFAULT.chainId]: withEnvOverrides(BASE_DEFAULT), + [BASE_SEPOLIA_DEFAULT.chainId]: withEnvOverrides(BASE_SEPOLIA_DEFAULT), +}; + +export const SUPPORTED_CHAIN_IDS = Object.keys(CHAINS).map(Number); + +export function getChainConfig(chainId: number): ChainConfig { + const cfg = CHAINS[chainId]; + if (!cfg) { + throw new Error( + `Unsupported chainId: ${chainId}. Supported: ${SUPPORTED_CHAIN_IDS.join(", ")}` + ); + } + return cfg; +} + +export function isSupportedChain(chainId: number | undefined): chainId is number { + return chainId !== undefined && chainId in CHAINS; +} diff --git a/packages/app/src/config/contracts.ts b/packages/app/src/config/contracts.ts index 11cc3b5..c83f826 100644 --- a/packages/app/src/config/contracts.ts +++ b/packages/app/src/config/contracts.ts @@ -1,23 +1,23 @@ import { type Address } from "viem"; +import { DEFAULT_CHAIN_ID, ZERO_ADDRESS, getChainConfig } from "./chains"; -// Chain-aware addresses — set via env vars for testnet deployments -export const SABLIER_LOCKUP: Address = - (process.env.NEXT_PUBLIC_SABLIER_LOCKUP as Address) || - "0xb5D78DD3276325f5FAF3106Cc4Acc56E28e0Fe3B"; // Base mainnet default +// Default-chain view of the registry. These singleton exports are kept as a +// compatibility layer for the current single-chain call sites. The page +// refactor (RG-9430aa) will switch consumers to useChainId() + getChainConfig() +// and delete these, but landing the registry separately keeps production +// behavior identical while the refactor rolls out. +const defaultChain = getChainConfig(DEFAULT_CHAIN_ID); -export const USDC_ADDRESS: Address = - (process.env.NEXT_PUBLIC_USDC_ADDRESS as Address) || - "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base mainnet default +export const SABLIER_LOCKUP: Address = defaultChain.sablierLockup; +export const USDC_ADDRESS: Address = defaultChain.usdc; +export const TREASURY: Address = defaultChain.treasury; +export const EXPLORER_URL: string = defaultChain.explorerUrl; +export const STREAM_START_BLOCK: bigint = defaultChain.streamStartBlock; +export const LOG_CHUNK_SIZE: bigint = defaultChain.logChunkSize; +export const IS_TESTNET: boolean = defaultChain.isTestnet; -// RipGuard Audit Fund treasury -export const TREASURY: Address = - (process.env.NEXT_PUBLIC_TREASURY_ADDRESS as Address) || - "0x847F640bE052b0700C31F72Dce622F4C6286934E"; // Base mainnet default - -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; - -// 0.5% broker fee in Sablier fixed-point (1e18 = 100%) -// Disabled if treasury is zero address (Sablier reverts on zero-address broker with non-zero fee) +// 0.5% broker fee in Sablier fixed-point (1e18 = 100%). Global — same fee on every chain. +// Disabled if treasury is zero address (Sablier reverts on zero-address broker with non-zero fee). export const BROKER_FEE = TREASURY === ZERO_ADDRESS ? BigInt(0) : BigInt("5000000000000000"); // 5e15 @@ -27,27 +27,7 @@ export const BROKER_FEE_PCT = ? `${Number((BROKER_FEE * BigInt(10000)) / BigInt("1000000000000000000")) / 100}%` : "0%"; -// Whether we're running on a testnet -export const IS_TESTNET = process.env.NEXT_PUBLIC_CHAIN === "base-sepolia"; - -// Block explorer base URL -export const EXPLORER_URL = IS_TESTNET - ? "https://sepolia.basescan.org" - : "https://basescan.org"; - -// Safe starting block for event queries -// Testnet: TestUSDC (0x54C0f145D70ca4792e695697B6498552F1EC0009) deployed between blocks 38,540,000–38,550,000 -// Mainnet: safe baseline before RipGuard launch -export const STREAM_START_BLOCK = IS_TESTNET - ? BigInt(38_540_000) // Just before TestUSDC deployment on Base Sepolia - : BigInt(22_000_000); // Base mainnet - -// Max block range per getLogs call. -// Public Base Sepolia RPCs allow up to 10k blocks per call. -// Mainnet paid RPCs (Alchemy etc.) support up to 50k. -export const LOG_CHUNK_SIZE = IS_TESTNET ? BigInt(10_000) : BigInt(50_000); - -// Schedule presets — "build your own reloads" +// Schedule presets — "build your own reloads". Global: schedules aren't chain-scoped. export const PRESETS = { hourly1d: { label: "Hourly Payouts (24h)", From dc30d21931f44edc1f350a201436ca050104c144 Mon Sep 17 00:00:00 2001 From: Fielding Johnston Date: Fri, 24 Apr 2026 21:04:31 -0500 Subject: [PATCH 3/3] test(chains): cover treasury address, avoid mutating SUPPORTED_CHAIN_IDS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #8: - Add treasury to the address-validity sweep so a malformed treasury (or one accidentally swapped with a non-address value) gets caught alongside sablierLockup and usdc. - Spread SUPPORTED_CHAIN_IDS before sorting so the test doesn't mutate the exported array — the in-place sort would have leaked module state across test runs. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/app/src/config/chains.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/app/src/config/chains.test.ts b/packages/app/src/config/chains.test.ts index b782f32..e57366b 100644 --- a/packages/app/src/config/chains.test.ts +++ b/packages/app/src/config/chains.test.ts @@ -37,6 +37,7 @@ describe("chain registry", () => { /^0x[a-fA-F0-9]{40}$/ ); expect(chain.usdc, `${chain.name} usdc`).toMatch(/^0x[a-fA-F0-9]{40}$/); + expect(chain.treasury, `${chain.name} treasury`).toMatch(/^0x[a-fA-F0-9]{40}$/); expect(chain.usdcDecimals, `${chain.name} usdcDecimals`).toBeGreaterThan(0); expect(chain.streamStartBlock > BigInt(0), `${chain.name} streamStartBlock`).toBe(true); expect(chain.logChunkSize > BigInt(0), `${chain.name} logChunkSize`).toBe(true); @@ -45,7 +46,9 @@ describe("chain registry", () => { }); it("SUPPORTED_CHAIN_IDS matches CHAINS keys", () => { - expect(SUPPORTED_CHAIN_IDS.sort()).toEqual(Object.keys(CHAINS).map(Number).sort()); + expect([...SUPPORTED_CHAIN_IDS].sort()).toEqual( + Object.keys(CHAINS).map(Number).sort() + ); }); it("isSupportedChain recognizes registered chains", () => {