feat: add delegators view#719
Conversation
Adds a Delegators tab to the account layout listing an orchestrator's delegators, backed by a new orchestratorDelegators subgraph query.
|
@moudi-network is attempting to deploy a commit to the Livepeer Foundation Team on Vercel. A member of the Team first needs to authorize it. |
|
Warning Review limit reached
Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughAdds an orchestrator delegator route and table, generated GraphQL support, persisted explorer list state, subgraph health reporting, and updates to round and score data handling. ChangesOrchestrator delegators
Explorer list persistence
Data handling and health reporting
Estimated code review effort: 5 (Critical) | ~100 minutes Sequence Diagram(s)sequenceDiagram
participant AccountLayout
participant DelegatorsView
participant ApolloHooks
participant Subgraph
participant DelegatorList
AccountLayout->>DelegatorsView: render delegators view
DelegatorsView->>ApolloHooks: query transcoder id and first page
ApolloHooks->>Subgraph: orchestratorDelegators(...)
Subgraph-->>ApolloHooks: transcoder.delegators
ApolloHooks-->>DelegatorsView: data and fetchMore
DelegatorsView->>DelegatorList: append delegator rows
DelegatorList-->>AccountLayout: render table or empty state
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/DelegatorsView/index.tsx`:
- Around line 15-23: The delegators list is being silently truncated by the
hard-coded `first: 1000` in `useOrchestratorDelegatorsQuery`, while the table
only paginates the fetched data client-side. Update `DelegatorsView` to drive
pagination from table state and fetch additional rows from the server using
`first`/`skip` (or cursors) instead of a fixed limit. Use the existing
`useOrchestratorDelegatorsQuery` setup and the table’s pagination controls to
wire the requested page size and offset through the query variables.
- Around line 15-27: The `DelegatorsView` render path is treating both in-flight
and failed `useOrchestratorDelegatorsQuery` results as an empty list, so
`DelegatorList` shows a false “No delegators found” state. Update
`DelegatorsView` to read `loading` and `error` from
`useOrchestratorDelegatorsQuery`, render a loading or error state before
`DelegatorList`, and only pass through to the empty-state logic after a
successful query with zero delegators.
In `@pages/accounts/`[account]/delegators.tsx:
- Around line 59-88: The delegators page currently allows any valid account
through `getStaticProps`, but this route should only serve orchestrator
accounts. In `getStaticProps`, after `getAccount(client, accountId)` and before
fetching orchestrators, check `account.data.transcoder`; if it is missing,
return `notFound: true` or redirect to `/delegating` instead of building
`PageProps`. Keep the existing `getAccount`, `getSortedOrchestrators`, and
`PageProps` flow only for accounts that have a transcoder.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 70cf5959-3344-4ea1-be5d-effb16836fa5
📒 Files selected for processing (7)
apollo/subgraph.tscomponents/DelegatorList/index.tsxcomponents/DelegatorsView/index.tsxcomponents/Table/index.tsxlayouts/account.tsxpages/accounts/[account]/delegators.tsxqueries/orchestratorDelegators.graphql
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/DelegatorsView/index.tsx`:
- Around line 38-69: The `fetchNext` pagination path in `DelegatorsView` only
logs `fetchMore` failures, which can leave the list silently incomplete after a
later page load fails. Update `fetchNext` so it sets a visible failure state
(for example `pageFetchFailed`) when `fetchMore` throws, and keep the state
reset in `finally` so retries remain possible. Then use that state in
`DelegatorsView` to render a small banner/notice near the table, and optionally
provide a retry action, so users know paging stopped early.
- Around line 93-94: The DelegatorsView render path is performing a side effect
by calling console.error(error) directly inside the component body when error &&
!delegators is true. Move this logging into a useEffect in DelegatorsView keyed
on error (and any other needed state) so the log only runs after render and
avoids duplicate or discarded render side effects.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f80ae7c1-d6b1-4b3f-9281-a72a2621c274
📒 Files selected for processing (2)
components/DelegatorsView/index.tsxpages/accounts/[account]/delegators.tsx
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/DelegatorsView/index.tsx (2)
21-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a brief description for this non-trivial component.
DelegatorsViewimplements query wiring, cursor-less pagination, retry/failure handling, and multiple render states, but has no descriptive comment explaining its behavior/contract for future maintainers.As per path instructions, "at minimum, include a general description for new functions/components that are not self-explanatory."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/DelegatorsView/index.tsx` around lines 21 - 33, DelegatorsView is a non-trivial component and needs a brief descriptive comment explaining its purpose and contract for maintainers. Add a short component-level description near the DelegatorsView definition that summarizes its query wiring via useOrchestratorDelegatorsQuery, pagination/fetchMore behavior, and the loading/error/empty/content render states so the intent is clear without reading the full implementation.Source: Path instructions
42-85: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winStop auto-paging once the last page is shorter than
PAGE_SIZE.
delegators.length % PAGE_SIZE === 0treats an exact multiple as “more data,” so the final empty page can keep re-triggeringfetchNextwith the sameskipand spam the subgraph. Track ahasMoreflag from the last response instead of inferring it from the current length.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/DelegatorsView/index.tsx` around lines 42 - 85, The auto-paging logic in fetchNext/useEffect is inferring pagination from delegators.length % PAGE_SIZE, which can keep requesting the same skip value after the last full page. Update the DelegatorsView paging flow to track an explicit hasMore flag from the latest fetchMore response and use that in the useEffect guard instead of relying on the current list length; make sure fetchNext clears hasMore when fetchMoreResult.transcoder.delegators is empty or shorter than PAGE_SIZE.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@components/DelegatorsView/index.tsx`:
- Around line 21-33: DelegatorsView is a non-trivial component and needs a brief
descriptive comment explaining its purpose and contract for maintainers. Add a
short component-level description near the DelegatorsView definition that
summarizes its query wiring via useOrchestratorDelegatorsQuery,
pagination/fetchMore behavior, and the loading/error/empty/content render states
so the intent is clear without reading the full implementation.
- Around line 42-85: The auto-paging logic in fetchNext/useEffect is inferring
pagination from delegators.length % PAGE_SIZE, which can keep requesting the
same skip value after the last full page. Update the DelegatorsView paging flow
to track an explicit hasMore flag from the latest fetchMore response and use
that in the useEffect guard instead of relying on the current list length; make
sure fetchNext clears hasMore when fetchMoreResult.transcoder.delegators is
empty or shorter than PAGE_SIZE.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b22ee49f-1b03-4b4a-86f3-a6bf8a034fd7
📒 Files selected for processing (1)
components/DelegatorsView/index.tsx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@moudi-network thanks for the contribution. @mehrdadmms did an initial review, and everything looks good so far. The implementation works, and the code quality is high enough for the bounty to be paid out. I may request a few minor code and UI changes after Adam has reviewed it. I will try to merge this in in the comming weeks when I have more bandwith. |
The text for the headers seems to be wrapping into two lines, not sure if this is intentional. Also some conflicts with the main. |
* feat: preserve orch table state on back nav * fix: address Copilot comments * feat: preserve state for all major tables * docs: add JSDoc to new persisted... hook * chore: move default sort outside of memo * chore: clean up persisted table state defaults
…eer#729) One unreachable upstream failed the whole request with a 502. Fetch each independently and fall back to null for that field, failing only when all are down. Partial responses cache for a minute so recovery is not hidden, and the UI shows "Unavailable" rather than "N/A" so our outage is not read as the orchestrator having no score.
…vepeer#731) /current-round mixed sources: round id/startBlock/initialized came from the subgraph, but the current block from a live RPC call. When subgraph indexing stalls the stale round is compared against a live block, so the tracker shows a stale round number with counters running past the round length. Read the timing from the RoundsManager contract (currentRound, currentRoundStartBlock, currentRoundInitialized, blockNum) via l2PublicClient, keeping startBlock and the current block consistent and independent of subgraph health. Response shape is unchanged; aggregate stats still use the subgraph.
Show a site-wide info banner when the subgraph is significantly behind the L2 chain head, signalling Explorer data may be temporarily stale while the protocol operates normally.
…ivepeer#734) The voting list/detail pages and the Governance nav badge source polls entirely from the subgraph, so a poll created during an indexing halt (LIP-118 "Delegated Reward Calling") doesn't appear and can't be reached for voting through the UI — even though voting itself is an on-chain action. Add a temporary MANUAL_POLLS constant (subgraph-shaped entries) merged into the voting list, the poll detail page, and the active-poll nav count, deduped by poll address so the real tallied copy wins and the manual entry drops out once indexing recovers. Manual polls carry no tally, so the detail page shows a "counts not loaded" note; casting votes works normally on-chain. getTotalStake now degrades gracefully if the subgraph is unavailable so manual polls still render. Revert once the subgraph has reindexed the poll.
…livepeer#735) * fix: source round length and lock state on-chain in the round tracker RoundStatus still mixed sources after livepeer#731: round timing came from the RoundsManager via /current-round, but roundLength and the lock state came from the subgraph (protocol.roundLength / protocol.lockPeriod). The lock state was re-derived client-side (blocksRemaining <= lockPeriod) because the subgraph, being event-driven, can't keep a per-block boolean fresh. Extend /api/current-round to also return roundLength and locked (from RoundsManager.roundLength and currentRoundLocked), and read both from useCurrentRoundData. The lock state now comes straight from currentRoundLocked — one authoritative snapshot instead of re-deriving it — and the round tracker (ring, blocks remaining, lock badge) is fully on-chain and consistent. The spinner now only waits on the on-chain round data, so the tracker renders even when the subgraph is fully down; the subgraph-only stats (fees, rewards, supply) are hidden in that case. * chore: drop redundant comment in RoundStatus * fix: show a loading skeleton for the round lock badge isRoundLocked falls back to false before currentRoundInfo resolves, so the header badge asserted "Initialized" while the round data was still loading. Render a skeleton until currentRoundInfo is available, matching the spinner in the content area below. ---------
…r#736) /api/pending-stake read the current round from the subgraph and passed it to on-chain pendingStake/pendingFees calls. When indexing falls behind, the stale round understates both values, and a subgraph error or empty response threw "No current round found" and returned a 500. Read currentRound from the RoundsManager via l2PublicClient instead, so the route is a pure contract read that no longer depends on subgraph health. Response shape is unchanged. This was the last caller of the subgraph-backed getCurrentRound() helper, so drop it. queries/currentRound.graphql stays in use by getOrchestrators().
…h resyncs (livepeer#749) Stopgap while the subgraph resyncs from scratch (livepeer/subgraph#247), which leaves the manually surfaced LIP-118 poll showing 0% support and 0% participation even for people who already voted. Revert this and livepeer#734 once the subgraph has reindexed the poll. --------- Co-authored-by: Copilot <copilot-pull-request-reviewer[bot]@users.noreply.github.com>
…ivepeer#748) Stopgap while the subgraph resyncs from scratch (livepeer/subgraph#247). Locks created after indexing stalled are missing from the withdrawal list, so an unbond appears to do nothing, and a matured lock stays invisible while being withdrawable on-chain. Lock ids are sequential and never reused, so anything the subgraph has not seen sits above its highest known id. Add /api/unbonding-locks/[address] to read that tail from the contract and append it to the subgraph's list rather than replacing it, with the round from /api/current-round since a stale round hides a matured lock's withdraw button. Only fetched when the subgraph reports degraded and on the user's own account, capped at 200 ids. Revert once the subgraph has reindexed. --------- Co-authored-by: CodeRabbit <coderabbitai[bot]@users.noreply.github.com>
…dexed (#…" (livepeer#752) This reverts commit 081de4b as the new subgraph has been indexed.
… subgrap…" (livepeer#750) This reverts commit 98f5694 as the new subgraph has been indexed.
… behind …" (livepeer#753) This reverts commit 29fd3cc now the new subgraph is fully indexed.
…-view # Conflicts: # apollo/subgraph.ts # components/Table/index.tsx
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/DelegatorList/index.tsx (1)
80-146: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRender the ordinal from the current row order.
row.indexis the row’s index in the originaldataarray, so with the declarative default sort bybondedAmountNumberand sortable columns, the displayed ordinal can jump ahead after sorting or page navigation. Derive the visible ordinal from the table-rendered rows, e.g. by mappingpage/rowswith its own index and passing it into the row cell, rather than usingrow.index + 1.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/DelegatorList/index.tsx` around lines 80 - 146, Update the DelegatorList table rendering so the ordinal uses each row’s current visible order rather than row.index from the original data array. Derive the index while mapping the rendered page/rows and pass that value into the Cell rendering context, then replace row.index + 1 with the passed visible ordinal while preserving one-based numbering across sorting and pagination.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@hooks/useSubgraphHealth.ts`:
- Around line 11-13: Update the SWR request key in useSubgraphHealth to
"/api/subgraph-health" so it targets the existing API route and continues
receiving the { degraded } payload.
In `@pages/api/current-round.tsx`:
- Around line 55-69: Verify the deployed RoundsManager contract’s blockNum()
implementation or authoritative contract reference and confirm that
currentL1Block is L1-based before it is compared with currentRoundStartBlock in
RoundStatus. If blockNum() is not demonstrably L1-based, update the
current-round data flow to use a concrete L1 block source so both values share
the same basis.
---
Outside diff comments:
In `@components/DelegatorList/index.tsx`:
- Around line 80-146: Update the DelegatorList table rendering so the ordinal
uses each row’s current visible order rather than row.index from the original
data array. Derive the index while mapping the rendered page/rows and pass that
value into the Cell rendering context, then replace row.index + 1 with the
passed visible ordinal while preserving one-based numbering across sorting and
pagination.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8ecc2325-4f10-474f-bf16-d240cc5edabb
📒 Files selected for processing (26)
apollo/subgraph.tscomponents/DelegatorList/index.tsxcomponents/GatewayList/index.tsxcomponents/OrchestratingView/index.tsxcomponents/OrchestratorList/index.tsxcomponents/PerformanceList/index.tsxcomponents/RoundStatus/index.tsxcomponents/Table/index.tsxcomponents/TransactionsList/index.tsxhooks/index.tsxhooks/useExplorerStore.tsxhooks/usePersistedExplorerListState.tshooks/useSubgraphHealth.tslayouts/main.tsxlib/api/ssr.tslib/api/types/get-current-round.tslib/api/types/get-performance.tspages/api/current-round.tsxpages/api/pending-stake/[address].tsxpages/api/score/[address].tsxpages/api/subgraph-health.tsxpages/gateways.tsxpages/index.tsxpages/orchestrators.tsxpages/transactions.tsxqueries/meta.graphql
💤 Files with no reviewable changes (1)
- lib/api/ssr.ts
ab0314c to
29403dc
Compare
Fixed this with bc916b0 . Setting whitespace to
|



Description
Adds a Delegators tab to the orchestrator account page, listing every account that has delegated stake to the orchestrator. Each row shows the delegator's Stake (subgraph
bondedAmount) and Pending Rewards (live on-chainpendingStake − bondedAmount), so a row's total matches what each delegator sees on their own profile.Built entirely on existing infrastructure: the subgraph
Transcoder.delegatorsfield (no subgraph/indexer change) and the existing/api/pending-stakeendpoint (no new server code). Pending Rewards is fetched per visible row via the existingusePendingFeesAndStakeDatahook — the same per-cell pattern already used for ENS data. Only the current page (~10 rows) are fetched.Type of Change
Related Issue(s)
Related: #711
Closes: #106
Changes Made
Delegators List
isOrchestrator || isMyDelegate), positioned after Delegatingqueries/orchestratorDelegators.graphqlquerying the existingTranscoder.delegatorsfield (regenerated Apollo types viapnpm codegen)components/DelegatorList(react-table: columns, customnumericSort, client pagination, empty state) andcomponents/DelegatorsViewwrapperpages/accounts/[account]/delegators.tsx(mirrorsorchestrating.tsx)bondedAmount; Pending Rewards column computed live per row asmax(0, pendingStake/1e18 − bondedAmount)via/api/pending-stakeTesting
How to test (optional unless test is not trivial)
Delegators List
0x1416…orchestrator → delegator0xa69e…3db3: 131.43K + 28.72K = 160.15K (matches profile).pnpm codegen,pnpm typecheck,pnpm exec eslint … --max-warnings 0,prettierall clean.Impact / Risk
Risk level: Low
Impacted areas: UI (new tab/route + right-rail copy), read-only API reuse (
/api/pending-stake). No DB, infra, config, or new server code.User impact: Orchestrator account pages gain a new Delegators tab. No change for plain delegator accounts. Purely additive. Nothing existing is altered.
Rollback plan: Plain PR revert. The change is additive (new route + tab branch + new components), so reverting removes the tab with no migration or config cleanup.
Screenshots / Recordings (if applicable)
Additional Notes
/api/pending-stake) to keep the PR minimal. No new API route/hook/type. Trade-off: the column is not globally sortable (the value only exists for on-screen rows). A batched-endpoint alternative (sortable column, 1 request) can be implemented if wanted later.Delegator.feesis lifetime-cumulative across all orchestrators and event-lagged, so it isn't attributable to this orchestrator.first: 1000delegators sorted by stake, paginated client-side (10/page); fine for current orchestrator sizes.Summary by CodeRabbit