Skip to content

feat(pages): per-chain page lookups + decimal-aware approve/lock flow#11

Closed
fielding wants to merge 4 commits into
feat/chain-registryfrom
feat/page-refactor
Closed

feat(pages): per-chain page lookups + decimal-aware approve/lock flow#11
fielding wants to merge 4 commits into
feat/chain-registryfrom
feat/page-refactor

Conversation

@fielding

Copy link
Copy Markdown
Owner

Summary

Stacked on #8. Switches every consumer from the default-chain singletons to `getChainConfig(useChainId())` lookups, threads `usdcDecimals` through the approve/lock/share flow, and addresses the broker-fee follow-up from the #8 review.

Closes `RG-9430aa` (page refactor) and `RG-42e939` (per-chain decimals).

What changed

`config/contracts.ts` — chain-specific singletons (`SABLIER_LOCKUP`, `USDC_ADDRESS`, `TREASURY`, `EXPLORER_URL`, `STREAM_START_BLOCK`, `LOG_CHUNK_SIZE`) deleted. `BROKER_FEE` becomes `BROKER_FEE_RATE` (the constant rate) plus two helpers — `brokerFeeForTreasury(treasury)` and `brokerFeePctString(fee)` — so each call site computes broker fee from its active chain's treasury (per the #8 review note). `IS_TESTNET` (site-level) and `PRESETS` (chain-agnostic) stay.

`lib/schedule.ts` — `parseAmountParam` now takes an optional decimal cap (default 6). BNB's 18-decimal USDC won't be truncated by the URL-param sanity regex.

`app/create/page.tsx` — `useChainId()` + destructured chain config at the top of `CreateLockInner`. `parseStreamIdFromReceipt` takes `sablierAddress` as a parameter. Sub-components (`VestingCalculator`, `TimelinePreview`, `ConfirmDialog`, `SuccessView`) take `usdcDecimals` / `brokerFee` / `brokerFeePct` / `sablierAddress` / `explorerUrl` as props.

`app/vaults/page.tsx` — `fetchFromSubgraph` and `fetchFromChain` take the active `chainId` and chain config as parameters. The Envio indexer query filters `chainId: { _eq: "" }` instead of the env-derived constant. `VaultCard` takes the chain-derived values as props.

`app/page.tsx` — landing reads the active chain via `useChainId()`. Unconnected users see the wagmi default chain's contract (Base on the mainnet site). The chain chip row in `RG-213ce7` will give per-chain links.

The minimum lock amount is now `BigInt(10) ** BigInt(usdcDecimals)` so BNB also enforces a 1-USDC floor.

Why a sub-component prop pattern instead of context

A React context for `useChainConfig()` would be cleaner long-term, but it adds a provider, opens up a wider blast radius, and isn't needed yet — only `/create` and `/vaults` have the sub-component depth that benefits from threaded values, and they're the only chain-aware pages. If the chip row in PR 4 ends up needing the chain config from many components, a context will make sense; for this PR, props keep the change focused.

Test plan

  • `pnpm --filter app test` — 68 tests pass (was 63; +3 contracts.test.ts, +2 schedule.test.ts)
  • `pnpm --filter app exec tsc --noEmit` — clean
  • `NODE_ENV=production pnpm --filter app build` — clean, 9 routes emit
  • Smoke test on Vercel preview against Base mainnet: `/` link to BaseScan, `/create` connect → enter amount → review → approve → lock, `/vaults` claim flow, share card
  • Smoke test the testnet preview against Base Sepolia: faucet button still appears, mint test USDC, full lock+claim cycle on test funds

🤖 Generated with Claude Code

fielding and others added 3 commits April 24, 2026 21:14
Replaces the default-chain BROKER_FEE singleton with BROKER_FEE_RATE
and two helpers — brokerFeeForTreasury(treasury) and
brokerFeePctString(fee) — so each call site can compute the broker
fee from its active chain's treasury rather than the deployment's
default. Addresses the third review note on PR #8.

parseAmountParam now takes an optional decimals cap (default 6) so
BNB Chain's 18-decimal USDC isn't truncated by the URL-param sanity
regex. The default keeps every existing call site unchanged.

The chain-specific singleton exports (SABLIER_LOCKUP, USDC_ADDRESS,
TREASURY, EXPLORER_URL, STREAM_START_BLOCK, LOG_CHUNK_SIZE) are
removed in this commit; the next commit switches every consumer over
to getChainConfig(useChainId()).

Refs: RG-9430aa, RG-42e939

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches /create, /vaults, and the landing page from default-chain
singletons to getChainConfig(useChainId()) lookups. Every per-chain
value — Sablier address, USDC address, USDC decimals, treasury,
explorer URL, broker fee, stream start block, log chunk size — now
flows from the active chain's registry entry.

The minimum lock amount, formerly a hardcoded 1_000_000 (1 USDC at
6 decimals), is now BigInt(10) ** BigInt(usdcDecimals) so BNB's
18-decimal USDC also enforces a 1-USDC floor (1e18 base units).

Sub-components (VestingCalculator, TimelinePreview, ConfirmDialog,
SuccessView, VaultCard) take the chain-derived values as props
rather than reading module-level singletons. parseStreamIdFromReceipt
takes sablierAddress as a parameter. The vaults indexer query
parameterizes chainId from useChainId() instead of an env-derived
constant, and fetchFromChain takes the chain config so the on-chain
fallback uses the right Sablier address, start block, and chunk size.

The landing's "view contract" link reads from the active chain via
useChainId() — unconnected users still see the wagmi default chain's
contract, which on the mainnet site is Base.

No behavior change on Base mainnet or Base Sepolia. Tests + typecheck
+ production build all clean.

Refs: RG-9430aa, RG-42e939

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ripguard Ready Ready Preview, Comment Apr 25, 2026 9:54pm
ripguard-testnet Ready Ready Preview, Comment Apr 25, 2026 9:54pm

@fielding

Copy link
Copy Markdown
Owner Author

Reviewer orientation — multi-EVM stack [2/6]

This PR is part of a 6-PR stacked multi-EVM rollout. Review #8 first if you haven't — this builds on its registry.

PR Branch What it does
1 #8 feat/chain-registry registry foundation
→ 2 #11 feat/page-refactor switch pages to useChainId (you are here)
3 #12 feat/wagmi-multichain registry-driven wagmi config + wrong-chain guard
4 #13 feat/expand-chains add 6 new chains + BNB disclosure
5 #14 feat/landing-chain-chips landing chain chip row
6 #15 feat/balances-script `pnpm balances` CLI

Move to #12 when done.

Heads up: this PR introduces a regression where users on unsupported chains crash on `getChainConfig()` — that's fixed in #12's wrong-chain guard. Don't merge this one without #12 right behind it.

@fielding fielding left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on this layer.

The refactor makes the tx flow chain-aware, but several callbacks/effects still close over chain-specific values without listing them as deps. pnpm --filter app lint flags the important ones in /create and /vaults:

  • depositAmount depends on usdcDecimals.
  • fee depends on brokerFee.
  • handleApprove uses usdcAddress and sablierLockup.
  • handleLock and the approve-to-lock effect use sablierLockup, usdcAddress, treasury, brokerFee, and usdcDecimals.
  • The lock success effect uses explorerUrl, sablierLockup, and usdcDecimals.
  • The vault claim callback/effect use sablierLockup and explorerUrl.

This is not just lint noise now that the page can move between supported chains while mounted. A user can start an approve/lock flow on one chain, switch networks, and have UI state and callbacks disagree about token, Sablier address, treasury, fee, decimals, or explorer URL. The delayed approve-to-lock timeout is the riskiest path.

Please fix the deps and add a guard for network changes during an active tx flow. I would either:

useEffect(() => {
  if (step !== "schedule" && step !== "success") {
    resetForm();
    toast("Network changed. Review the lock again on the new chain.", "error");
  }
}, [chainId]);

or store a flowChainId when the user enters confirm/approve/lock and refuse to continue if the current chain no longer matches.

One smaller follow-up while you are in here: the remembered schedule label key is only ripguard:lock:${streamId}. Stream IDs can collide across chains/contracts, so the key should include chainId or the Sablier address.

Validation I ran on the top of the stack:

pnpm --filter app test
pnpm --filter app exec tsc --noEmit
NODE_ENV=production pnpm --filter app build
pnpm --filter app lint

Tests, typecheck, and build passed. Lint fails overall because of pre-existing React 19 rules too, but the missing-dependency warnings above are introduced by this refactor and are real for multi-chain behavior.

…d labels

Addresses review feedback on #11.

1. Missing useEffect/useCallback deps for chain-derived values are
   now listed: usdcDecimals, brokerFee, sablierLockup, usdcAddress,
   treasury, explorerUrl. Without these, a chain switch mid-render
   could leave callbacks closed over stale values from the previous
   chain — most dangerously the approve→lock priming timeout, which
   would have fired writeLock against the old chain's Sablier address.

2. Chain-change guard: if useChainId() flips while step is past
   schedule (confirm/approve/lock), reset the form and toast the user.
   Stops the cross-chain race entirely. Uses a lastChainIdRef so the
   effect only acts on actual chain transitions, not initial mount.
   (The dep array includes step/resetForm/toast for lint, but the ref
   short-circuit means the body only runs when chainId changed.)

3. localStorage label key now namespaced by chainId:
   `ripguard:lock:<chainId>:<streamId>`. Sablier stream IDs reset
   per chain — without this, stream #5 on Arbitrum would steal the
   schedule label of stream #5 on Base. getLockLabel takes chainId
   as a parameter; VaultCard receives it as a prop from VaultDashboard.

Existing locks under the old key fall back to the generic schedule
type on /vaults — acceptable, since the fallback path was already
in place.

Refs: RG-9430aa (review feedback)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fielding

Copy link
Copy Markdown
Owner Author

Pushed fixes addressing your changes-requested review:

Missing deps — added the chain-derived values to every callback/effect dep array you flagged:

  • `depositAmount` ← `usdcDecimals`
  • `fee` ← `brokerFee`
  • `handleApprove` ← `usdcAddress`, `sablierLockup`
  • `handleLock` ← `sablierLockup`, `usdcAddress`, `treasury`, `brokerFee`
  • approve→lock effect ← `sablierLockup`, `usdcAddress`, `treasury`, `brokerFee`, `usdcDecimals`
  • lock-success effect ← `explorerUrl`, `sablierLockup`, `usdcDecimals`, `chainId`
  • vault claim callback ← `sablierLockup`
  • vault claim effect ← `explorerUrl`

Chain-change guard — went with the resetForm approach you suggested. Uses a `lastChainIdRef` so the effect body only fires on actual chainId transitions (not on initial mount or unrelated step changes). Toasts the user, clears the form, returns to the schedule step. Closes the approve→lock priming-timeout race entirely.

localStorage label key — namespaced by chainId now: `ripguard:lock::`. `getLockLabel` takes `chainId` as a parameter; `VaultCard` receives it as a prop. Existing locks under the old key fall back to the generic schedule type — acceptable since the fallback path was already there.

Tests + typecheck + production build pass. Ready for re-review.

@fielding fielding left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the fix commit. The blocker from my prior review is addressed.

The missing chain-derived deps are gone, pnpm --filter app lint no longer reports the multi-chain hook dependency warnings, and the chain-change guard resets an active create flow before stale approve or lock values can be reused. The localStorage preset label key is also namespaced by chain ID now.

I do not see remaining blockers in this layer.

@fielding fielding deleted the branch feat/chain-registry April 26, 2026 08:06
@fielding fielding closed this Apr 26, 2026
fielding added a commit that referenced this pull request Apr 26, 2026
…d labels

Addresses review feedback on #11.

1. Missing useEffect/useCallback deps for chain-derived values are
   now listed: usdcDecimals, brokerFee, sablierLockup, usdcAddress,
   treasury, explorerUrl. Without these, a chain switch mid-render
   could leave callbacks closed over stale values from the previous
   chain — most dangerously the approve→lock priming timeout, which
   would have fired writeLock against the old chain's Sablier address.

2. Chain-change guard: if useChainId() flips while step is past
   schedule (confirm/approve/lock), reset the form and toast the user.
   Stops the cross-chain race entirely. Uses a lastChainIdRef so the
   effect only acts on actual chain transitions, not initial mount.
   (The dep array includes step/resetForm/toast for lint, but the ref
   short-circuit means the body only runs when chainId changed.)

3. localStorage label key now namespaced by chainId:
   `ripguard:lock:<chainId>:<streamId>`. Sablier stream IDs reset
   per chain — without this, stream #5 on Arbitrum would steal the
   schedule label of stream #5 on Base. getLockLabel takes chainId
   as a parameter; VaultCard receives it as a prop from VaultDashboard.

Existing locks under the old key fall back to the generic schedule
type on /vaults — acceptable, since the fallback path was already
in place.

Refs: RG-9430aa (review feedback)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fielding added a commit that referenced this pull request Apr 26, 2026
…#11)

Switches /create, /vaults, and the landing page from default-chain
singletons to getChainConfig(useChainId()) lookups. Every per-chain
value — Sablier address, USDC address, USDC decimals, treasury,
explorer URL, broker fee, stream start block, log chunk size — now
flows from the active chain's registry entry.

The minimum lock amount is now BigInt(10) ** BigInt(usdcDecimals) so
BNB's 18-decimal USDC also enforces a 1-USDC floor. Sub-components
(VestingCalculator, TimelinePreview, ConfirmDialog, SuccessView,
VaultCard) take chain-derived values as props.

Includes the review fixes: chain-aware deps on every callback/effect,
chain-change guard with lastChainIdRef so an active tx flow resets if
the wallet switches networks mid-flight, localStorage label key
namespaced by chainId so cross-chain stream IDs don't collide.

Refactors BROKER_FEE singleton into BROKER_FEE_RATE + helper functions
brokerFeeForTreasury(treasury) and brokerFeePctString(fee), so each
chain's fee is computed from its active treasury rather than the
deployment default. Parameterizes parseAmountParam with a decimals cap
(default 6, BNB needs 18) so URL-param sanity regex doesn't truncate
high-decimal amounts.

Refs: RG-9430aa, RG-42e939
Closes #11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fielding

Copy link
Copy Markdown
Owner Author

Merged via 85b386d on main — the squash of #8 with --delete-branch caused this PR to be auto-closed before its base could flip to main, and GitHub then refused to reopen it ("Could not open the pull request"). Fix landed via local squash-merge of the rebased branch directly to main; same content, just bypassed the broken GitHub state.

Lesson for future stacks: when squash-merging the base of a stack, don't pass --delete-branch. Let the next PR's base auto-flip to main, then delete the branch after.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant