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
58 changes: 0 additions & 58 deletions constants/manualPolls.ts

This file was deleted.

3 changes: 1 addition & 2 deletions layouts/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {
useTreasuryProposalsQuery,
} from "apollo";
import { BRIDGE_LPT_URL, GET_LPT_URL } from "constants/links";
import { mergeManualPolls } from "constants/manualPolls";
import { BigNumber } from "ethers";
import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "lib/chains";
import dynamic from "next/dynamic";
Expand Down Expand Up @@ -179,7 +178,7 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => {

const totalActivePolls = useMemo(
() =>
mergeManualPolls(pollData?.polls).filter(
pollData?.polls.filter(
(p) =>
(currentRound?.currentL1Block ?? Number.MAX_VALUE) <=
parseInt(p.endBlock)
Expand Down
45 changes: 18 additions & 27 deletions lib/api/polls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,33 +170,24 @@ const getEstimatedEndTimeByBlockNumber = async (
};

const getTotalStake = async (l2BlockNumber?: number | undefined) => {
try {
const client = getApollo();

const protocolResponse = await client.query<
ProtocolByBlockQuery,
ProtocolByBlockQueryVariables
>({
query: ProtocolByBlockDocument,
variables: l2BlockNumber
? {
block: {
number: l2BlockNumber,
},
}
: undefined,
fetchPolicy: "network-only",
});

return protocolResponse?.data?.protocol?.totalActiveStake;
} catch (err) {
// The subgraph can be unavailable (e.g. an indexing halt). Total stake only
// feeds participation percentages, so degrade gracefully instead of failing
// the whole poll render — this keeps manually-listed polls viewable/votable.
const detail = err instanceof Error ? err.message : String(err);
console.warn(`Could not fetch total stake from subgraph (${detail})`);
return undefined;
}
const client = getApollo();

const protocolResponse = await client.query<
ProtocolByBlockQuery,
ProtocolByBlockQueryVariables
>({
query: ProtocolByBlockDocument,
variables: l2BlockNumber
? {
block: {
number: l2BlockNumber,
},
}
: undefined,
fetchPolicy: "network-only",
});

return protocolResponse?.data?.protocol?.totalActiveStake;
Comment on lines 172 to +190

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unguarded Apollo query — subgraph failures now propagate unhandled.

getTotalStake no longer catches failures from client.query. With Apollo's default errorPolicy: "none", a network/GraphQL error rejects the promise, and since getPollExtended awaits this with no try/catch either, any subgraph hiccup will now throw through the poll rendering path instead of degrading gracefully (previously logged + returned undefined). Given that subgraph instability is exactly what motivated the original manual-poll workaround this PR reverts, losing this safety net reintroduces the failure mode it was built to avoid.

If reverting to pre-#734 behavior is intentional, worth confirming the team is fine with poll pages failing outright on subgraph errors rather than degrading.

🛡️ Optional fix to restore graceful degradation
 const getTotalStake = async (l2BlockNumber?: number | undefined) => {
   const client = getApollo();
 
-  const protocolResponse = await client.query<
-    ProtocolByBlockQuery,
-    ProtocolByBlockQueryVariables
-  >({
-    query: ProtocolByBlockDocument,
-    variables: l2BlockNumber
-      ? {
-          block: {
-            number: l2BlockNumber,
-          },
-        }
-      : undefined,
-    fetchPolicy: "network-only",
-  });
-
-  return protocolResponse?.data?.protocol?.totalActiveStake;
+  try {
+    const protocolResponse = await client.query<
+      ProtocolByBlockQuery,
+      ProtocolByBlockQueryVariables
+    >({
+      query: ProtocolByBlockDocument,
+      variables: l2BlockNumber
+        ? {
+            block: {
+              number: l2BlockNumber,
+            },
+          }
+        : undefined,
+      fetchPolicy: "network-only",
+    });
+
+    return protocolResponse?.data?.protocol?.totalActiveStake;
+  } catch (err) {
+    console.warn(`Could not fetch total stake: ${err}`);
+    return undefined;
+  }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getTotalStake = async (l2BlockNumber?: number | undefined) => {
try {
const client = getApollo();
const protocolResponse = await client.query<
ProtocolByBlockQuery,
ProtocolByBlockQueryVariables
>({
query: ProtocolByBlockDocument,
variables: l2BlockNumber
? {
block: {
number: l2BlockNumber,
},
}
: undefined,
fetchPolicy: "network-only",
});
return protocolResponse?.data?.protocol?.totalActiveStake;
} catch (err) {
// The subgraph can be unavailable (e.g. an indexing halt). Total stake only
// feeds participation percentages, so degrade gracefully instead of failing
// the whole poll render — this keeps manually-listed polls viewable/votable.
const detail = err instanceof Error ? err.message : String(err);
console.warn(`Could not fetch total stake from subgraph (${detail})`);
return undefined;
}
const client = getApollo();
const protocolResponse = await client.query<
ProtocolByBlockQuery,
ProtocolByBlockQueryVariables
>({
query: ProtocolByBlockDocument,
variables: l2BlockNumber
? {
block: {
number: l2BlockNumber,
},
}
: undefined,
fetchPolicy: "network-only",
});
return protocolResponse?.data?.protocol?.totalActiveStake;
const getTotalStake = async (l2BlockNumber?: number | undefined) => {
const client = getApollo();
try {
const protocolResponse = await client.query<
ProtocolByBlockQuery,
ProtocolByBlockQueryVariables
>({
query: ProtocolByBlockDocument,
variables: l2BlockNumber
? {
block: {
number: l2BlockNumber,
},
}
: undefined,
fetchPolicy: "network-only",
});
return protocolResponse?.data?.protocol?.totalActiveStake;
} catch (err) {
console.warn(`Could not fetch total stake: ${err}`);
return undefined;
}
};
🤖 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 `@lib/api/polls.ts` around lines 172 - 190, Restore error handling in
getTotalStake around the Apollo client.query call so network or GraphQL failures
are caught, logged using the existing logging mechanism, and result in undefined
rather than rejecting. Preserve the current successful response behavior and
ensure getPollExtended continues to degrade gracefully when the subgraph is
unavailable.

};

const getL2BlockRangeForL1 = async (l1BlockNumber: number) => {
Expand Down
30 changes: 4 additions & 26 deletions pages/voting/[poll].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
useVoteQuery,
} from "apollo";
import { sentenceCase } from "change-case";
import { getManualPoll, isManualPoll } from "constants/manualPolls";
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
Expand Down Expand Up @@ -91,22 +90,18 @@ const Poll = () => {

useEffect(() => {
const init = async () => {
// Fall back to a manually-listed poll when the subgraph doesn't have it
// (e.g. created while indexing was down), so it stays viewable/votable.
const source = data?.poll ?? getManualPoll(pollId);
if (source && currentRound?.currentL1Block) {
if (data && currentRound?.currentL1Block) {
const response = await getPollExtended(
source,
data.poll,
currentRound.currentL1Block
);
setPollData(response);
Comment on lines +93 to 98

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'poll.*\.(ts|tsx)$|voting.*\.(ts|tsx)$' . | sed 's#^\./##' | head -100

echo "== target file outline/size =="
wc -l pages/voting/[poll].tsx 2>/dev/null || true
ast-grep outline "pages/voting/[poll].tsx" 2>/dev/null || true

echo "== relevant lines =="
cat -n "pages/voting/[poll].tsx" | sed -n '1,180p'

echo "== getPollExtended usages/definition =="
rg -n "function getPollExtended|const getPollExtended|getPollExtended\(" . -S

Repository: livepeer/explorer

Length of output: 7063


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate source without relying on exact path
echo "== git ls files matching poll_extended or polling =="
git ls-files | rg -i 'poll|extended' | head -120

echo "== search api call helpers =="
rg -n "getPollExtended|poll_extended|extended" . -S | head -200

Repository: livepeer/explorer

Length of output: 1755


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lib/api/polls.ts =="
cat -n lib/api/polls.ts | sed -n '1,180p'

echo "== apollo schema/types around poll =="
cat -n apollo/createSchema.ts | sed -n '1,180p'
rg -n -C 4 "poll|Poll|id: ID|status: PollStatus|currentL1Block" apollo queries lib/api pages/voting -S | head -260

echo "== all uses of pollData attributes/status =="
cat -n pages/voting/[poll].tsx | sed -n '180,430p'

Repository: livepeer/explorer

Length of output: 32155


🌐 Web query:

GraphQL Livepeer subgraph Poll id query poll(id: $id): Poll generated schema optional id argument

💡 Result:

In the Livepeer subgraph schema, the Poll entity is defined with an ID field of type ID! [1]. When querying individual entities in a GraphQL subgraph, you can typically retrieve a specific entity by its ID using the singular form of the entity name as a query field (e.g., poll(id: $id): Poll) [2][3]. Since the Poll entity's id field is marked as non-nullable (ID!) [1], the query input for retrieving a single poll by its ID requires an ID argument. While the schema definition itself specifies the ID field's requirements, the generated GraphQL schema for The Graph subgraphs includes top-level query fields for each entity, which natively support fetching by the entity's ID [1][2]. If you are writing a query to fetch a specific poll, you would structure it as follows: query GetPoll($id: String!) { poll(id: $id) { id proposal endBlock quorum quota tally { id } } } In this structure, the id argument is required to identify the specific Poll entity [1][2]. If you are encountering issues where the id argument appears to be optional or missing in certain documentation or generated types, ensure that your client-side GraphQL generator (e.g., Apollo Client) is correctly pointing to the latest subgraph schema, as entity-specific query fields for objects with ID types are standard for subgraphs [2][3].

Citations:


🌐 Web query:

The Graph generated singular entity query poll(id) null response when id not found

💡 Result:

In The Graph, a singular entity query (e.g., entity(id: "123")) returns a standard GraphQL response where the data field for that entity is null if the requested ID is not found [1][2]. When this occurs, the response typically follows this structure: { "data": { "entityName": null }, "errors": [ { "message": "Entity not found", "locations": [{"line": 2, "column": 3}], "path": ["entityName"] } ] } Key technical points: 1. Expected Behavior: Returning null for a missing entity is the standard GraphQL behavior for nullable singular query fields [1][2][3]. 2. Error Response: While the data value is null, The Graph often includes an entry in the errors array explicitly stating "Entity not found" [1][2]. 3. Troubleshooting: If you are encountering unexpected null results when you believe an entity should exist, verify that the id string passed in the query exactly matches the ID stored in the subgraph database [4]. Common issues include: - Case sensitivity (e.g., "0x0b" vs "0xb"). - Incorrect padding or formatting of Hex/Bytes IDs [4]. - Synchronization delays where the entity has not yet been indexed [4]. If you are seeing a null response without an error, or if you suspect it should be returning an object, double-check your ID generation logic in your subgraph's mapping code to ensure it aligns perfectly with the queries you are performing [4].

Citations:


🌐 Web query:

site:github.com/livepeer/explorer apollo/subgraph.ts generated types usePollQuery poll data

💡 Result:

The file apollo/subgraph.ts in the livepeer/explorer repository is an auto-generated file containing Apollo Client types and hooks derived from GraphQL operations [1][2]. It is not a library-provided file but rather a product of code generation, typically using a tool like GraphQL Code Generator [1]. In this context, usePollQuery is a generated React hook that corresponds to a GraphQL query defined (usually in a.graphql file) within the project [2]. When you define a query named Poll (e.g., in a file like queries/poll.graphql), the code generation process automatically creates a corresponding React hook named usePollQuery [2]. To work with poll data via this hook: 1. Identification: The hook is generated based on the name of the operation in your.graphql files. If your operation is named query Poll, the generated hook will be usePollQuery [2]. 2. Usage: You can use this hook in your React components to fetch poll data from the subgraph [2]. It returns an object containing the data, loading state, error state, and other standard Apollo Client hook properties [2]. 3. Generation: Because apollo/subgraph.ts is auto-generated, you should not edit it directly [1]. Instead, update the underlying GraphQL query files (e.g., in the queries/ directory) and re-run your generation command (often configured in codegen.yml) to update the types and hooks [1][2]. If you are trying to access specific poll data, ensure that your GraphQL query explicitly requests the necessary fields (such as poll ID, choiceID, or voter details) and that your codegen configuration is set to output React hooks [2].

Citations:


Guard data.poll before calling getPollExtended.

  • Problem: data can be successfully populated with data.poll === null, but this still calls getPollExtended(null, ...), which builds a PollExtended from null and can render a poll page with status === "active" and endBlock === 0.
  • Why it matters: Unknown/missing poll IDs bypass the existing 404 path and show malformed poll data instead.
  • Suggested fix: Only call getPollExtended when data?.poll is non-null; otherwise return 404 for a successful query with no matching poll.
🤖 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 `@pages/voting/`[poll].tsx around lines 93 - 98, Guard the getPollExtended call
so it runs only when data?.poll is non-null and currentRound.currentL1Block is
available. In the successful-query branch where data.poll is missing, follow the
existing 404 response path instead of constructing poll data from null.

}
Comment on lines 91 to 99
};
init();
}, [data, pollId, currentRound?.currentL1Block]);
}, [data, currentRound?.currentL1Block]);

// Only 404 on a genuine subgraph error with no manual fallback for this poll.
if (pollError && !getManualPoll(pollId)) {
if (pollError) {
return <FourZeroFour />;
Comment on lines +104 to 105

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## files"
git ls-files | sed -n '1,120p'
echo

echo "## target outline/size"
wc -l pages/voting/\[poll\].tsx 2>/dev/null || true
ast-grep outline 'pages/voting/[poll].tsx' 2>/dev/null || true
echo

echo "## target lines 1-180"
cat -n 'pages/voting/[poll].tsx' | sed -n '1,180p'
echo

echo "## hunt for FourZeroFour and pollError usages"
rg -n "FourZeroFour|pollError|apolloError|networkError|graphQLErrors|NotFoundError|Poll" . --glob '!node_modules' --glob '!dist' --glob '!build' | sed -n '1,200p'

Repository: livepeer/explorer

Length of output: 39702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## files"
git ls-files | sed -n '1,120p'
echo

echo "## target outline/size"
wc -l 'pages/voting/[poll].tsx' 2>/dev/null || true
ast-grep outline 'pages/voting/[poll].tsx' 2>/dev/null || true
echo

echo "## target lines 1-180"
cat -n 'pages/voting/[poll].tsx' | sed -n '1,180p'
echo

echo "## hunt for FourZeroFour and pollError usages"
rg -n "FourZeroFour|pollError|apolloError|networkError|graphQLErrors|NotFoundError|Poll" . --glob '!node_modules' --glob '!dist' --glob '!build' | sed -n '1,200p'

Repository: livepeer/explorer

Length of output: 39584


Do not map every Apollo error to 404.

  • Problem: pollError can be a network failure, server/gateway error, or execution error, but this branch returns <FourZeroFour /> for all of them.
  • Why it matters: Subgraph or network outages make a valid poll URL look like “not found,” hiding recoverable failures from the user.
  • Suggested fix: Only show 404 for poll === null after a successful fetch, or for GraphQL errors that explicitly represent a missing poll. Render an error/retry state for network and other non-missing-entity failures.
🤖 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 `@pages/voting/`[poll].tsx around lines 104 - 105, Update the error handling in
the poll page around pollError so only a confirmed missing poll—poll === null
after a successful fetch or an explicitly missing-poll GraphQL error—renders
FourZeroFour. Route network, server, gateway, and other execution failures to
the existing error/retry state, preserving normal rendering for successful
polls.

}
Comment on lines +104 to 106

Expand Down Expand Up @@ -217,23 +212,6 @@ const Poll = () => {
</Box>

<Box>
{isManualPoll(pollData.id) && (
<Box
css={{
marginBottom: "$3",
padding: "$3",
borderRadius: "$3",
border: "1px solid $blue5",
backgroundColor: "$blue2",
}}
>
<Text size="1" css={{ color: "$blue11" }}>
Live vote counts aren&apos;t available for this poll yet —
the subgraph is still indexing it. The totals below may read
0 until indexing catches up. Voting works normally.
</Text>
</Box>
)}
<Box
css={{
display: "grid",
Expand Down
8 changes: 2 additions & 6 deletions pages/voting/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
} from "@livepeer/design-system";
import { usePollsQuery } from "apollo";
import { sentenceCase } from "change-case";
import { mergeManualPolls } from "constants/manualPolls";
import { useCurrentRoundData } from "hooks";
import { LAYOUT_MAX_WIDTH } from "layouts/constants";
import { getLayout } from "layouts/main";
Expand Down Expand Up @@ -51,14 +50,11 @@ const Voting = () => {
const currentRound = useCurrentRoundData();

useEffect(() => {
// Render as soon as the (on-chain) current round is available, even if the
// subgraph query returned nothing, so manually-listed polls still show while
// indexing is down. mergeManualPolls tolerates `data` being undefined.
if (currentRound?.currentL1Block) {
if (data && currentRound?.currentL1Block) {
const init = async () => {
setPolls(
await Promise.all(
mergeManualPolls(data?.polls).map((p) =>
(data?.polls ?? []).map((p) =>
getPollExtended(p, currentRound.currentL1Block)
)
)
Comment on lines 52 to 60
Expand Down
Loading