Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .tix/issues.jsonl
Original file line number Diff line number Diff line change
@@ -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"]}
60 changes: 60 additions & 0 deletions packages/app/src/config/chains.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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.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);
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);
});
});
83 changes: 83 additions & 0 deletions packages/app/src/config/chains.ts
Original file line number Diff line number Diff line change
@@ -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<number, ChainConfig> = {
[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;
}
54 changes: 17 additions & 37 deletions packages/app/src/config/contracts.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)",
Expand Down