Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ NEXT_PUBLIC_SUBGRAPH_ID=FE63YgkzcpVocxdCEyEYbvjYqEf2kb1A6daMYRxmejYC
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
NEXT_PUBLIC_METRICS_SERVER_URL=https://livepeer-leaderboard-serverless.vercel.app
NEXT_PUBLIC_AI_METRICS_SERVER_URL=https://leaderboard-api.livepeer.cloud
NEXT_PUBLIC_SNAPSHOT_SPACE=livepeer.eth
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ To run the application in production mode, follow these steps:
| `NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` | WalletConnect (Reown) Cloud Project ID. Used to enhance wallet UX for users of Explorer. |
| `NEXT_PUBLIC_METRICS_SERVER_URL` | The Transcoding performance API server used by Explorer. |
| `NEXT_PUBLIC_AI_METRICS_SERVER_URL` | The AI performance API server used by Explorer. |
| `NEXT_PUBLIC_SNAPSHOT_SPACE` | The Snapshot space ID for community governance (e.g., `livepeer.eth`). Used to fetch and display community proposals via Snapshot. |

## Developing on Arbitrum Rinkeby

Expand Down Expand Up @@ -112,4 +113,25 @@ To test Livepeer Improvement Proposals (LIPs), follow these steps:

With the namespace and your forked repository, you can now test LIPs locally within the application.

## Community Governance via Snapshot

The Explorer integrates with [Snapshot](https://snapshot.org) to enable community governance voting on proposals outside of LIPs and treasury funding. This allows orchestrators and delegators to express support or opposition to smaller governance changes.

### Features

- **Browse Proposals**: View all community proposals from the Livepeer Snapshot space at `/governance`
- **Vote on Proposals**: Cast votes directly using your connected wallet
- **Voting Power**: Your voting power is calculated based on your stake at the proposal's snapshot block
- **Real-time Results**: See current vote distributions and participation rates

### Configuration

To use a custom Snapshot space for testing:

```env
NEXT_PUBLIC_SNAPSHOT_SPACE=your-space.eth
```

The default is `livepeer.eth` which is the official Livepeer community space.

---
90 changes: 90 additions & 0 deletions components/SnapshotProposalRow/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Badge, Box, Card, Flex, Heading, Link as A } from "@livepeer/design-system";
import Link from "next/link";
import { SnapshotProposal } from "@lib/api/snapshot";
import { sentenceCase } from "change-case";
import dayjs from "@lib/dayjs";

export const BadgeVariantByState = {
active: "blue",
pending: "lime",
closed: "gray",
} as const;

type Props = {
key: string;
proposal: SnapshotProposal;
};

const SnapshotProposalRow = ({ key, proposal, ...props }: Props) => {
const startDate = dayjs.unix(proposal.start);
const endDate = dayjs.unix(proposal.end);
const now = dayjs();

const isActive = now.isAfter(startDate) && now.isBefore(endDate);
const isPending = now.isBefore(startDate);

return (
<A
as={Link}
{...props}
href={`/governance/${proposal.id}`}
passHref
css={{
cursor: "pointer",
display: "block",
textDecoration: "none",
"&:hover": { textDecoration: "none" },
}}
>
<Card
variant="interactive"
css={{
padding: "$4",
marginBottom: "$3",
border: "1px solid $neutral4",
}}
>
<Flex
css={{
flexDirection: "column-reverse",
justifyContent: "space-between",
alignItems: "flex-start",
"@bp2": {
flexDirection: "row",
alignItems: "center",
},
}}
>
<Box>
<Heading size="1" css={{ mb: "$1" }}>
{proposal.title}
</Heading>
<Box css={{ fontSize: "$1", color: "$neutral10" }}>
{proposal.state === "closed" ? (
<Box>Voting ended on {endDate.format("MMM D, YYYY")}</Box>
) : isActive ? (
<Box>Voting will end on ~{endDate.format("MMM D, YYYY")}</Box>
) : isPending ? (
<Box>Voting starts on {startDate.format("MMM D, YYYY")}</Box>
) : (
<Box>Voting ended on {endDate.format("MMM D, YYYY")}</Box>
)}
</Box>
</Box>
<Badge
size="2"
variant={BadgeVariantByState[proposal.state] || "neutral"}
css={{
textTransform: "capitalize",
fontWeight: 700,
}}
>
{sentenceCase(proposal.state)}
</Badge>
</Flex>
</Card>
</A>
);
};

export default SnapshotProposalRow;
75 changes: 75 additions & 0 deletions components/SnapshotVoteButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Button } from "@livepeer/design-system";
import { useAccountAddress } from "hooks";
import { useState } from "react";
import { castSnapshotVote } from "@lib/api/snapshot";
import { useWalletClient } from "wagmi";

type Props = React.ComponentProps<typeof Button> & {
proposalId: string;
choice: number;
reason?: string;
};

const SnapshotVoteButton = ({
proposalId,
choice,
reason,
children,
...props
}: Props) => {
const accountAddress = useAccountAddress();
const { data: walletClient } = useWalletClient();
const [isVoting, setIsVoting] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleVote = async () => {
if (!walletClient || !accountAddress) {
setError("Please connect your wallet");
return;
}

setIsVoting(true);
setError(null);
try {
const provider = walletClient;
await castSnapshotVote(
provider,
accountAddress,
proposalId,
choice,
reason
);

// Refresh the page to show updated vote
setTimeout(() => {
window.location.reload();
}, 2000);
} catch (err) {
console.error("Error casting vote:", err);
setError(
err instanceof Error ? err.message : "Failed to cast vote"
);
} finally {
setIsVoting(false);
}
};

if (!accountAddress) {
return null;
}

return (
<>
<Button disabled={isVoting} onClick={handleVote} {...props}>
{isVoting ? "Voting..." : children}
</Button>
{error && (
<span style={{ color: "red", fontSize: "12px", marginTop: "4px" }}>
{error}
</span>
)}
</>
);
};

export default SnapshotVoteButton;
Loading