Skip to content

feat: multi-rollup support — view/claim rewards across rollup versions#58

Open
gpevnev wants to merge 18 commits intoAztecProtocol:mainfrom
NethermindEth:feat/multi-rollup-support
Open

feat: multi-rollup support — view/claim rewards across rollup versions#58
gpevnev wants to merge 18 commits intoAztecProtocol:mainfrom
NethermindEth:feat/multi-rollup-support

Conversation

@gpevnev
Copy link
Copy Markdown

@gpevnev gpevnev commented Apr 16, 2026

Summary

Closes #57

The dashboard previously hardcoded a single rollup via VITE_ROLLUP_ADDRESS. Sequencers could not view or claim rewards stranded on older rollups, and registration would target the wrong rollup after governance rotation.

This PR adds runtime rollup discovery via the on-chain IRegistry contract and fans out reward reads/claims across all discovered rollups.

What changed

  • Rollup Registry discoveryuseRollupRegistry hook reads StakingRegistry.ROLLUP_REGISTRY() → enumerates all versions from the governance Registry. Returns rollups[], canonical, configured, isStale.
  • 13 parameterized rollup hooks — each accepts optional rollupAddress, defaulting to contracts.rollup.address. No behavior change for existing callers.
  • Multi-rollup rewardsuseCoinbaseRewardsAcrossRollups multicalls getSequencerRewards(coinbase) across every (rollup × coinbase) pair. Per-rollup rows with version badges in the UI.
  • Per-rollup claiming — each reward row targets a specific rollup's claimSequencerRewards. useIsRewardsClaimableAcrossRollups gates each row's button independently.
  • Canonical rollup registrationRegistrationStake and WalletDirectStakingFlow use useRollupRegistry().canonical for deposit/stake calls regardless of VITE_ROLLUP_ADDRESS.
  • Stranded-stake withdrawaluseAttesterStakeLocation scans all rollups to find where an attester's stake lives, routes withdraw/finalize to the correct contract.
  • Indexer disclaimer — shown on provider pages when rollups.length > 1.
  • Claim engine rewrite — replaced 4 interleaved useEffects with a useReducer state machine (idle → ready_to_trigger → waiting_for_result → advancing). Fixed sequential task execution, modal unmounting, per-row spinner state, T&C gate.

Integration test

Includes a full integration test setup using real Aztec L1 contracts on local anvil:

  • scripts/multi-rollup-test/deploy-multi-rollup.sh — deploys 2 rollup versions + MockStakingRegistry
  • scripts/multi-rollup-test/seed-multi-rollup.ts — seeds rewards via anvil_setStorageAt
  • scripts/multi-rollup-test/seed-providers.ts — registers test providers for populated provider list
  • scripts/multi-rollup-test/README.md — setup guide with gotchas

Screenshots

Per-rollup reward rows with version badges and individual claim buttons

Screenshot 2026-04-16 at 15 22 10

Manage Reward Addresses — coinbase with rewards on two rollups

Screenshot 2026-04-16 at 15 22 35

Providers list with IndexerRollupDisclaimer at the bottom

Screenshot 2026-04-16 at 15 22 41

Claim All Rewards — sequential per-rollup task execution

Screenshot 2026-04-16 at 15 35 03

Claim All progress — task 1 completed, task 2 in progress

Screenshot 2026-04-16 at 15 35 12

Claim All success — both rollup tasks completed

Screenshot 2026-04-16 at 15 35 24

Out of scope

  • Indexer multi-rollup support (Ponder config stays single-rollup)
  • Per-rollup APR display
  • Override UI for choosing non-canonical rollup during registration

Test plan

  • Registry discovers multiple rollups (verified via RPC: numberOfVersions() == 2)
  • Per-rollup reward rows display with version badges and correct amounts
  • Claim from single rollup leaves other rollup's rewards intact
  • Claim All processes tasks sequentially across rollups
  • Registration reads activation threshold from canonical rollup
  • Indexer disclaimer renders when rollups.length > 1
  • yarn type-check passes
  • yarn build passes

gpevnev added 13 commits April 16, 2026 09:09
Discover all rollup versions at runtime from the Aztec governance Registry
contract instead of hardcoding a single rollup via VITE_ROLLUP_ADDRESS.

- Add RollupRegistry ABI (IRegistry: numberOfVersions, getVersion, getRollup,
  getCanonicalRollup)
- Add useRollupRegistry hook that chains StakingRegistry.ROLLUP_REGISTRY() →
  Registry enumeration → builds rollup list with canonical detection
- Export RollupInstance type and useRollupRegistry from rollup hooks barrel
- Register rollupRegistry ABI in contracts/index.ts (address discovered at
  runtime, not configured statically)
Each rollup hook now accepts an optional rollupAddress parameter that defaults
to contracts.rollup.address. This enables callers to target a specific rollup
(e.g. an old rollup for stranded rewards/stakes) without changing the default
behavior for existing consumers.

Parameterized hooks: useSequencerRewards, useClaimSequencerRewards,
useIsRewardsClaimable, useRollupData, useActivationThresholdFormatted,
useEjectionThreshold, useAttesterView, useStakeHealth, useSequencerStatus,
useWalletDirectStake, useWalletInitiateWithdraw, useFinalizeWithdraw,
useApproveRollup.
Fan out reward reads across all discovered rollups so sequencers can see and
claim rewards stranded on older rollups.

- Add useCoinbaseRewardsAcrossRollups: multicalls getSequencerRewards across
  every (rollup x coinbase) pair, returns per-rollup breakdown
- Rewrite useMultipleCoinbaseRewards as thin wrapper over the new fan-out hook
- Add rollupAddress/rollupVersion to CoinbaseBreakdown and ClaimTask types
- Thread rollupAddress through useClaimAllRewards engine (per-task targeting)
- Thread rollupAddress through useClaimCoinbaseRewards
- Add useIsRewardsClaimableAcrossRollups: multicall isRewardsClaimable() per
  rollup for per-row claim button gating
- Add useAttesterStakeLocation: scans all rollups via getAttesterView to find
  where a stranded stake lives
…isclaimers

UI components updated to surface multi-rollup data:

Rewards:
- CoinbaseAddressList: per-rollup rows with version badges, per-row claim
  targeting specific rollup, per-row claimability gating
- ClaimSelfStakeRewardsModal: per-rollup balances with individual claim buttons
- ClaimAllRewardsSummary: rollup version badges on coinbase rows
- ClaimAllRewardsProgress: truncate long error messages to prevent overflow
- ATPStakingOverviewClaimableRewards: gate Claim All on T&C acceptance

Registration:
- RegistrationStake: read activation threshold from canonical rollup
- WalletDirectStakingFlow: deposit targets canonical rollup address

Withdrawals:
- WalletDirectStakeItem: resolve stake location via useAttesterStakeLocation
- WalletWithdrawalActions: accept rollupAddress prop for stranded-stake withdrawals

Disclaimers:
- Add IndexerRollupDisclaimer component (shown when rollups.length > 1)
- Add to StakingProvidersPage and StakingProviderDetailPage
End-to-end test setup that deploys real Aztec L1 contracts to local anvil with
2 rollup versions, seeds reward state, and verifies the dashboard's multi-rollup
features.

- deploy-multi-rollup.sh: orchestrates full L1 deploy (DeployAztecL1Contracts +
  DeployRollupForUpgrade), registers v2 via anvil_impersonateAccount, deploys
  MockStakingRegistry, deploys Multicall3, writes contract_addresses.json
- seed-multi-rollup.ts: seeds sequencer rewards and isRewardsClaimable via
  anvil_setStorageAt using namespaced storage layout, mints fee tokens to
  rollups for claim payouts
- MockStakingRegistry.sol: minimal mock (only contract not in aztec-packages)
- README.md: full setup guide with architecture, gotchas, troubleshooting

Requires AZTEC_PACKAGES_DIR env var (set automatically by .envrc).
The Positions Overview section (with Claimable Rewards) was gated only on
having staked positions from the indexer. Users who added coinbase addresses
for self-stake reward tracking but had no ATP staking events wouldn't see
their rewards. Now also checks for saved coinbase addresses.
… timeout

The completion effect created a setTimeout to advance to the next task and
returned a cleanup function to clear it. But setTasks() inside the same effect
changes `tasks` (a dependency), triggering an effect re-run whose cleanup
clears the timeout before it fires. The engine permanently stalls after the
first task.

Fix: use a ref-based timeout (advanceTimeoutRef) that persists across effect
re-runs. The handledCompletionRef guard prevents re-processing, so the effect
re-running is harmless. Also store claim hooks in refs to remove hook identity
from trigger effect deps — prevents premature firing during reset cycles.
onSuccess (which refetches rewards) was called as soon as isSuccess became
true. The refetch zeroed out the rewards, causing hasStakedPositions to
become false, unmounting the parent and the modal with it.

Move onSuccess to handleDone so it fires when the user manually dismisses
the success screen, not when the claims complete.
…he active one

The CoinbaseAddressList shared a single useClaimCoinbaseRewards hook instance
across all rows. When one row was claiming, isPending/isConfirming applied to
every row's button, making them all show "Confirming..." simultaneously.

Track which row is actively claiming via a claimingRowKey state. Only the
active row shows the spinner; other rows are disabled but show "Claim Rewards".
Extend MockStakingRegistry with registerProvider() that emits
ProviderRegistered events. The Ponder indexer watches for these events
and populates the /api/providers endpoint.

Add seed-providers.ts that registers the first 10 providers from the
provider metadata (providers/*.json) on-chain so the providers list
is populated during integration testing.
…irst claim

Two bugs fixed:

1. handledCompletionRef stuck after single-task claim: the ref was set during
   completion handling but never cleared in the "all done" branch, startClaiming,
   cancelClaiming, or reset. After a successful claim, re-opening the modal
   would stall forever because the completion effect returned early on the
   guard check. Now reset in all exit paths.

2. ClaimSelfStakeRewardsModal auto-closes after first per-rollup claim: the
   success effect called onClose(), dismissing the modal and clearing the
   coinbase input. Users with rewards on multiple rollups had to reopen and
   re-enter the address for each rollup. Now resets the claim hook and
   re-checks rewards instead, so the remaining rollup rows stay visible.
…addresses stay visible

P1: The claim engine previously stopped on the first error (e.g. a locked
rollup), never reaching later claimable tasks. Now marks the failed task
as errored and advances to the next one. Progress counts both completed
and failed tasks. isSuccess fires when all tasks are processed.

P2: useCoinbaseRewardsAcrossRollups now returns both allCoinbaseBreakdown
(including zero-balance rows) and coinbaseBreakdown (non-zero only). The
management UI uses allCoinbaseBreakdown so saved addresses with no rewards
remain visible and removable. Claim UIs continue using the filtered version.
P0: ATPStakingOverview — the hasLoadedOnce guard no longer narrowed
decimals/symbol/activationThreshold types. Add a second runtime guard
for type narrowing after the initial-load shortcut.

P0: ClaimSelfStakeRewardsModal — debouncedCheckRewards takes 0 args
but was called with coinbaseAddress. Remove the argument.

P1: ClaimAllRewardsModal — mixed-result runs (some succeeded, some
failed) transitioned to the success phase, hiding the retry UI. Now
only transitions to success when isSuccess && !isError.

All three pass tsc --noEmit.
@gpevnev gpevnev force-pushed the feat/multi-rollup-support branch from 0d9be7e to 39c4172 Compare April 16, 2026 12:03
gpevnev added 3 commits April 16, 2026 14:11
- ClaimAllRewardsModal: destructure reset to stabilize the dependency
- ClaimSelfStakeRewardsModal: add debouncedCheckRewards to deps with suppress
- useClaimAllRewards: suppress derived-dep warning for currentTask
Replace `coinbaseAddress.length === 42 && coinbaseAddress.startsWith('0x')`
with `validateAddress(coinbaseAddress)` which uses viem's `isAddress` for
proper hex and checksum validation.
The inline reward row rendering (rollup badge, reward amount, claim button,
loading/locked states) was deeply nested and hard to follow. Extract it into
a focused RollupRewardRow component with clear props.
@gpevnev gpevnev marked this pull request as ready for review April 16, 2026 13:17
@gpevnev gpevnev requested a review from a team April 16, 2026 13:17
@gpevnev gpevnev requested a review from a team as a code owner April 16, 2026 13:17
gpevnev added 2 commits April 16, 2026 15:23
Replace 4 interleaved useEffects, 5 refs, and 6 state variables with
an explicit state machine using useReducer.

Phases: idle → ready_to_trigger → waiting_for_result → advancing → next

Each phase has exactly one effect:
  1. trigger: call claim function
  2. result: watch hook isSuccess/isError
  3. advance: setTimeout → reset hooks → dispatch ADVANCED
  4. substep: update delegation sub-step display

All task mutations happen in the pure reducer function.
Effect cleanups are safe because phases don't change mid-timeout.
No guard refs, no hasTriggeredClaim, no handledCompletionRef.
@gpevnev gpevnev force-pushed the feat/multi-rollup-support branch from 95071ae to 8a296d3 Compare April 16, 2026 13:33
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.

Support reward management of older rollups

1 participant