diff --git a/apollo/subgraph.ts b/apollo/subgraph.ts index 96a56a01..359eeb31 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -8963,6 +8963,13 @@ export type TreasuryProposalsQueryVariables = Exact<{ [key: string]: never; }>; export type TreasuryProposalsQuery = { __typename: 'Query', treasuryProposals: Array<{ __typename: 'TreasuryProposal', id: string, description: string, calldatas: Array, targets: Array, values: Array, voteEnd: string, voteStart: string, proposer: { __typename: 'LivepeerAccount', id: string } }> }; +export type GetProposalsByIdsQueryVariables = Exact<{ + ids: Array | Scalars['ID']; +}>; + + +export type GetProposalsByIdsQuery = { __typename: 'Query', treasuryProposals: Array<{ __typename: 'TreasuryProposal', id: string, description: string, voteStart: string, voteEnd: string }> }; + export type VoteQueryVariables = Exact<{ id: Scalars['ID']; }>; @@ -9906,6 +9913,44 @@ export function useTreasuryProposalsLazyQuery(baseOptions?: Apollo.LazyQueryHook export type TreasuryProposalsQueryHookResult = ReturnType; export type TreasuryProposalsLazyQueryHookResult = ReturnType; export type TreasuryProposalsQueryResult = Apollo.QueryResult; +export const GetProposalsByIdsDocument = gql` + query getProposalsByIds($ids: [ID!]!) { + treasuryProposals(where: {id_in: $ids}) { + id + description + voteStart + voteEnd + } +} + `; + +/** + * __useGetProposalsByIdsQuery__ + * + * To run a query within a React component, call `useGetProposalsByIdsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetProposalsByIdsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetProposalsByIdsQuery({ + * variables: { + * ids: // value for 'ids' + * }, + * }); + */ +export function useGetProposalsByIdsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetProposalsByIdsDocument, options); + } +export function useGetProposalsByIdsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetProposalsByIdsDocument, options); + } +export type GetProposalsByIdsQueryHookResult = ReturnType; +export type GetProposalsByIdsLazyQueryHookResult = ReturnType; +export type GetProposalsByIdsQueryResult = Apollo.QueryResult; export const VoteDocument = gql` query vote($id: ID!) { vote(id: $id) { diff --git a/components/Account/index.tsx b/components/Account/index.tsx index 9f74de7a..e295376b 100644 --- a/components/Account/index.tsx +++ b/components/Account/index.tsx @@ -1,3 +1,4 @@ +import { formatAddress } from "@lib/utils"; import { Box, Flex, Link as A } from "@livepeer/design-system"; import { useAccountAddress, useEnsData } from "hooks"; import Link from "next/link"; @@ -52,11 +53,7 @@ const Account = () => { > - - {ens?.name - ? ens.name - : accountAddress.replace(accountAddress.slice(6, 38), "…")} - + {ens?.name ? ens.name : formatAddress(accountAddress)} diff --git a/components/AccountCell/index.tsx b/components/AccountCell/index.tsx index f9dc8d59..43f86c15 100644 --- a/components/AccountCell/index.tsx +++ b/components/AccountCell/index.tsx @@ -1,4 +1,4 @@ -import { textTruncate } from "@lib/utils"; +import { formatAddress, textTruncate } from "@lib/utils"; import { Box, Flex } from "@livepeer/design-system"; import { useEnsData } from "hooks"; import { QRCodeCanvas } from "qrcode.react"; @@ -88,7 +88,7 @@ const Index = ({ active, address }) => { {identity?.name ? textTruncate(identity.name, 12, "…") - : address.replace(address.slice(5, 36), "…")} + : formatAddress(address)} diff --git a/components/Claim/index.tsx b/components/Claim/index.tsx index b64985d2..d0abfa8e 100644 --- a/components/Claim/index.tsx +++ b/components/Claim/index.tsx @@ -1,6 +1,7 @@ import { LAYOUT_MAX_WIDTH } from "@layouts/constants"; import { l2Migrator } from "@lib/api/abis/bridge/L2Migrator"; import { getL2MigratorAddress } from "@lib/api/contracts"; +import { formatAddress } from "@lib/utils"; import { Box, Button, Container, Flex, Text } from "@livepeer/design-system"; import { ArrowTopRightIcon } from "@modulz/radix-icons"; import { constants, ethers } from "ethers"; @@ -202,10 +203,7 @@ const Claim = () => { borderBottom: "1px solid rgba(255,255,255, .2)", }} > - {migrationParams.delegate.replace( - migrationParams.delegate.slice(6, 38), - "…" - )} + {formatAddress(migrationParams.delegate)} diff --git a/components/DelegatingView/index.tsx b/components/DelegatingView/index.tsx index 83279309..0f32a1b9 100644 --- a/components/DelegatingView/index.tsx +++ b/components/DelegatingView/index.tsx @@ -1,7 +1,7 @@ import { ExplorerTooltip } from "@components/ExplorerTooltip"; import Stat from "@components/Stat"; import { bondingManager } from "@lib/api/abis/main/BondingManager"; -import { checkAddressEquality } from "@lib/utils"; +import { checkAddressEquality, formatAddress } from "@lib/utils"; import { Box, Button, Flex, Link as A, Text } from "@livepeer/design-system"; import { QuestionMarkCircledIcon } from "@modulz/radix-icons"; import { AccountQueryResult, OrchestratorsSortedQueryResult } from "apollo"; @@ -167,10 +167,7 @@ const Index = ({ delegator, transcoders, protocol, currentRound }: Props) => { {delegateIdentity?.name ? delegateIdentity?.name - : delegator?.delegate?.id.replace( - delegator?.delegate?.id.slice(7, 37), - "…" - )} + : formatAddress(delegator?.delegate?.id)} } />{" "} diff --git a/components/DelegatingWidget/Header.tsx b/components/DelegatingWidget/Header.tsx index 60cda370..b808c216 100644 --- a/components/DelegatingWidget/Header.tsx +++ b/components/DelegatingWidget/Header.tsx @@ -1,4 +1,5 @@ import { EnsIdentity } from "@lib/api/types/get-ens"; +import { formatAddress } from "@lib/utils"; import { Box, Flex, Heading } from "@livepeer/design-system"; import { QRCodeCanvas } from "qrcode.react"; @@ -58,7 +59,7 @@ const Header = ({ {delegateProfile?.name ? delegateProfile.name - : transcoder?.id.replace(transcoder.id.slice(7, 37), "…")} + : formatAddress(transcoder?.id)} - Delegated with{" "} - {event.newDelegate.id.replace( - event.newDelegate.id.slice(7, 37), - "…" - )} + Delegated with {formatAddress(event.newDelegate.id)} - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -271,10 +265,7 @@ function renderSwitch(event, i: number) { }} > - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -312,8 +303,7 @@ function renderSwitch(event, i: number) { > - Redelegated with{" "} - {event.delegate.id.replace(event.delegate.id.slice(7, 37), "…")} + Redelegated with {formatAddress(event.delegate.id)} - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -378,8 +365,7 @@ function renderSwitch(event, i: number) { > - Undelegated from{" "} - {event.delegate.id.replace(event.delegate.id.slice(7, 37), "…")} + Undelegated from {formatAddress(event.delegate.id)} - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -463,10 +446,7 @@ function renderSwitch(event, i: number) { }} > - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -526,10 +506,7 @@ function renderSwitch(event, i: number) { }} > - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -593,10 +570,7 @@ function renderSwitch(event, i: number) { }} > - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -655,10 +629,7 @@ function renderSwitch(event, i: number) { }} > - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -717,10 +688,7 @@ function renderSwitch(event, i: number) { }} > - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -780,10 +748,7 @@ function renderSwitch(event, i: number) { }} > - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} @@ -848,10 +813,7 @@ function renderSwitch(event, i: number) { }} > - {event.transaction.id.replace( - event.transaction.id.slice(6, 62), - "…" - )} + {formatTransactionHash(event.transaction.id)} diff --git a/components/OrchestratingView/index.tsx b/components/OrchestratingView/index.tsx index df2cf494..e89fc8de 100644 --- a/components/OrchestratingView/index.tsx +++ b/components/OrchestratingView/index.tsx @@ -1,12 +1,15 @@ import Stat from "@components/Stat"; import dayjs from "@lib/dayjs"; -import { Box, Flex } from "@livepeer/design-system"; -import { CheckIcon, Cross1Icon } from "@modulz/radix-icons"; +import { Box, Flex, Link as A, Text } from "@livepeer/design-system"; +import { ArrowTopRightIcon, CheckIcon, Cross1Icon } from "@modulz/radix-icons"; import { AccountQueryResult } from "apollo"; +import { CUBE_TYPE, getCubeData } from "cube/cube-client"; +import { getAccountVoterSummary } from "cube/query-generator"; import { useScoreData } from "hooks"; import { useRegionsData } from "hooks/useSwr"; +import Link from "next/link"; import numbro from "numbro"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import Masonry from "react-masonry-css"; const breakpointColumnsObj = { @@ -24,6 +27,11 @@ interface Props { isActive: boolean; } +interface GovStats { + voted: number; + eligible: number; +} + const Index = ({ currentRound, transcoder, isActive }: Props) => { const callsMade = useMemo( () => transcoder?.pools?.filter((r) => r.rewardTokens != null)?.length ?? 0, @@ -33,6 +41,41 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => { const scores = useScoreData(transcoder?.id); const knownRegions = useRegionsData(); + const [govStats, setGovStats] = useState(null); + + useEffect(() => { + const fetchGovStats = async () => { + if (!transcoder?.id) return; + + try { + const cubeQuery = getAccountVoterSummary(transcoder.id); + const response = await getCubeData(cubeQuery, { + type: CUBE_TYPE.SERVER, + }); + + // Some cube responses are wrapped in [0].data, others are direct arrays + const data = Array.isArray(response) + ? response[0]?.data || response + : null; + + if (data && data.length > 0) { + const votedCount = Number( + data[0]["LivepeerVoteProposals.numOfVoteCasted"] || 0 + ); + const eligibleCount = Number( + data[0]["LivepeerVoteProposals.numOfProposals"] || 0 + ); + + setGovStats({ voted: votedCount, eligible: eligibleCount }); + } + } catch (error) { + console.error("Error fetching governance stats:", error); + } + }; + + fetchGovStats(); + }, [transcoder?.id]); + const maxScore = useMemo(() => { const topTransData = Object.keys(scores?.scores ?? {}).reduce( (prev, curr) => { @@ -279,6 +322,111 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => { } /> )} + + + Number of proposals voted on relative to the number of proposals + the orchestrator was eligible for while active. + + } + value={ + govStats ? ( + + {govStats.voted} + + / {govStats.eligible} Proposals + + + ) : ( + "N/A" + ) + } + meta={ + + {govStats && ( + + + + )} + + {govStats && ( + + {numbro(govStats.voted / govStats.eligible).format({ + output: "percent", + mantissa: 0, + })}{" "} + Participation + + )} + + See history + + + + + } + /> + ); diff --git a/components/OrchestratorList/index.tsx b/components/OrchestratorList/index.tsx index 1370086e..dade2dec 100644 --- a/components/OrchestratorList/index.tsx +++ b/components/OrchestratorList/index.tsx @@ -11,7 +11,7 @@ import { ROIInflationChange, ROITimeHorizon, } from "@lib/roi"; -import { textTruncate } from "@lib/utils"; +import { formatAddress, textTruncate } from "@lib/utils"; import { Badge, Box, @@ -291,7 +291,7 @@ const OrchestratorList = ({ ) : ( - {row.values.id.replace(row.values.id.slice(7, 37), "…")} + {formatAddress(row.values.id)} )} {/* {(row?.original?.daysSinceChangeParams ?? diff --git a/components/OrchestratorVotingList/index.tsx b/components/OrchestratorVotingList/index.tsx new file mode 100644 index 00000000..b57d06de --- /dev/null +++ b/components/OrchestratorVotingList/index.tsx @@ -0,0 +1,318 @@ +import { ExplorerTooltip } from "@components/ExplorerTooltip"; +import Table from "@components/Table"; +import { formatAddress, textTruncate } from "@lib/utils"; +import { Badge, Box, Flex, Link as A, Text } from "@livepeer/design-system"; +import { CheckIcon, Cross2Icon, MinusIcon } from "@radix-ui/react-icons"; +import { useEnsData } from "hooks"; +import Link from "next/link"; +import numbro from "numbro"; +import QRCode from "qrcode.react"; +import { useMemo } from "react"; +import { Column } from "react-table"; + +export type VoterSummary = { + id: string; + noOfProposalsVotedOn: number; + noOfVotesCasted: number; + mostRecentVotes: (string | null)[]; + votingTurnout: number; +}; + +const OrchestratorVotingList = ({ + initialVoterData, + pageSize = 10, +}: { + initialVoterData?: VoterSummary[]; + pageSize: number; +}) => { + const columns = useMemo( + () => [ + { + Header: ( + + The account which is actively coordinating transcoders and + receiving fees/rewards. + + } + > + + Orchestrator + + + ), + accessor: "id", + Cell: ({ row }) => { + const identity = useEnsData(row.values.id); + + return ( + + + + + {+row.id + 1} + + + + {identity?.avatar ? ( + + ) : ( + + )} + {identity?.name ? ( + + + {textTruncate(identity.name, 20, "…")} + + + {row.values.id.substring(0, 6)} + + + ) : ( + + {formatAddress(row.values.id)} + + )} + + + + + ); + }, + }, + { + Header: ( + + The total number of governance proposals this orchestrator has + participated in by casting a vote. + + } + > + Number of Proposals Voted On + + ), + accessor: "noOfProposalsVotedOn", + Cell: ({ row }) => ( + + + {numbro(row.values.noOfProposalsVotedOn).format({ + mantissa: 0, + thousandSeparated: true, + })} + + + ), + sortType: "number", + }, + { + Header: ( + + The total count of individual votes submitted by this + orchestrator across all proposals. + + } + > + Number of Votes Casted + + ), + accessor: "noOfVotesCasted", + Cell: ({ row }) => ( + + + {numbro(row.values.noOfVotesCasted).format({ + mantissa: 0, + thousandSeparated: true, + })} + + + ), + sortType: "number", + }, + { + Header: ( + + A list of up to 5 of the orchestrator’s most recent votes, + marked as [✓] for For, [✗] for Against, and [–] for Abstain. + + } + > + Most Recent Votes + + ), + accessor: "mostRecentVotes", + Cell: ({ row }) => ( + + + {row.values.mostRecentVotes?.map((mostRecentVote, index) => { + const icon = + mostRecentVote == "for" ? ( + + ) : mostRecentVote == "against" ? ( + + ) : mostRecentVote == "abstain" ? ( + + ) : null; + + return ( + + {icon} + + ); + })} + + + ), + }, + { + Header: ( + + The percentage of total governance proposals this orchestrator + voted on, showing how actively they participate in protocol + decisions. + + } + > + Voting Turnout + + ), + accessor: "votingTurnout", + Cell: ({ row }) => ( + + + {numbro(row.values.votingTurnout).format({ + output: "percent", + mantissa: 2, + })} + + + ), + sortType: "number", + }, + ], + [] + ); + if (initialVoterData) { + return ( + []} + initialState={{ + pageSize, + }} + /> + ); + } else { + return null; + } +}; + +export default OrchestratorVotingList; diff --git a/components/PerformanceList/index.tsx b/components/PerformanceList/index.tsx index fede2324..3bb703ad 100644 --- a/components/PerformanceList/index.tsx +++ b/components/PerformanceList/index.tsx @@ -3,7 +3,7 @@ import IdentityAvatar from "@components/IdentityAvatar"; import Table from "@components/Table"; import { Pipeline } from "@lib/api/types/get-available-pipelines"; import { Region } from "@lib/api/types/get-regions"; -import { textTruncate } from "@lib/utils"; +import { formatAddress, textTruncate } from "@lib/utils"; import { Badge, Box, Flex, Link as A, Skeleton } from "@livepeer/design-system"; import { QuestionMarkCircledIcon } from "@modulz/radix-icons"; import { OrchestratorsQueryResult } from "apollo"; @@ -149,7 +149,7 @@ const PerformanceList = ({ ) : ( - {row.values.id.replace(row.values.id.slice(7, 37), "…")} + {formatAddress(row.values.id)} )} {typeof row.values.scores != "undefined" && diff --git a/components/Profile/index.tsx b/components/Profile/index.tsx index 2f5f1522..233226d9 100644 --- a/components/Profile/index.tsx +++ b/components/Profile/index.tsx @@ -1,6 +1,7 @@ import { ExplorerTooltip } from "@components/ExplorerTooltip"; import ShowMoreRichText from "@components/ShowMoreRichText"; import { EnsIdentity } from "@lib/api/types/get-ens"; +import { formatAddress } from "@lib/utils"; import { Box, Flex, Heading, Link as A, Text } from "@livepeer/design-system"; import { CheckIcon, @@ -107,9 +108,7 @@ const Index = ({ account, isMyAccount = false, identity }: Props) => { fontWeight: 700, }} > - {identity?.name - ? identity.name - : account.replace(account.slice(5, 39), "…")} + {identity?.name ? identity.name : formatAddress(account)} diff --git a/components/Search/index.tsx b/components/Search/index.tsx index 28853e7e..ad2f8d96 100644 --- a/components/Search/index.tsx +++ b/components/Search/index.tsx @@ -1,4 +1,5 @@ import Spinner from "@components/Spinner"; +import { formatAddress } from "@lib/utils"; import { Box, Dialog, @@ -182,14 +183,10 @@ const Index = ({ css = {}, ...props }) => { {result.item.name - ? `${result.item.name} (${result.item.id.replace( - result.item.id.slice(5, 39), - "…" + ? `${result.item.name} (${formatAddress( + result.item.id )})` - : result.item.id.replace( - result.item.id.slice(7, 37), - "…" - )} + : formatAddress(result.item.id, 8, 6)} diff --git a/components/StakeTransactions/index.tsx b/components/StakeTransactions/index.tsx index d4511cee..c2f244ad 100644 --- a/components/StakeTransactions/index.tsx +++ b/components/StakeTransactions/index.tsx @@ -4,6 +4,7 @@ import { parseEther } from "viem"; import { abbreviateNumber, + formatAddress, getHint, simulateNewActiveSetOrder, } from "../../lib/utils"; @@ -62,11 +63,7 @@ const Index = ({ delegator, transcoders, currentRound, isMyAccount }) => { > - Undelegating from{" "} - {lock.delegate.id.replace( - lock.delegate.id.slice(7, 37), - "…" - )} + Undelegating from {formatAddress(lock.delegate.id)} Tokens will be available for withdrawal in approximately{" "} @@ -136,13 +133,7 @@ const Index = ({ delegator, transcoders, currentRound, isMyAccount }) => { justifyContent: "space-between", }} > - - Undelegated from{" "} - {lock.delegate.id.replace( - lock.delegate.id.slice(7, 37), - "…" - )} - + Undelegated from {formatAddress(lock.delegate.id)} {isMyAccount && ( diff --git a/components/Table/index.tsx b/components/Table/index.tsx index 6e9832fa..84884998 100644 --- a/components/Table/index.tsx +++ b/components/Table/index.tsx @@ -80,7 +80,7 @@ function DataTable({ <> {input && ( @@ -98,7 +98,6 @@ function DataTable({ css={{ borderCollapse: "collapse", tableLayout: "auto", - minWidth: 980, width: "100%", "@bp4": { width: "100%", diff --git a/components/TransactionsList/index.tsx b/components/TransactionsList/index.tsx index 7fdb1e05..8b9fe98c 100644 --- a/components/TransactionsList/index.tsx +++ b/components/TransactionsList/index.tsx @@ -1,5 +1,6 @@ import Table from "@components/Table"; import dayjs from "@lib/dayjs"; +import { formatTransactionHash } from "@lib/utils"; import { Badge, Box, Flex, Link as A, Text } from "@livepeer/design-system"; import { ArrowTopRightIcon } from "@modulz/radix-icons"; import { EventsQueryResult } from "apollo"; @@ -79,7 +80,7 @@ const Transaction = (props: { id: string | undefined }) => { } > - {props.id ? props.id.replace(props.id.slice(6, 62), "…") : "N/A"} + {props.id ? formatTransactionHash(props.id) : "N/A"} {`Voted `} - {+event?.choiceID === 0 ? '"Yes"' : '"No"'} + {+event?.choiceID === 0 ? '"Against"' : '"For"'} {` on a proposal`} {renderEmoji("👩‍⚖️")} diff --git a/components/TreasuryVotingReason/index.tsx b/components/TreasuryVotingReason/index.tsx index 52724984..67b77998 100644 --- a/components/TreasuryVotingReason/index.tsx +++ b/components/TreasuryVotingReason/index.tsx @@ -1,4 +1,4 @@ -import { Text, TextArea } from "@livepeer/design-system"; +import { Box, Text, TextArea } from "@livepeer/design-system"; const MAX_INPUT_LENGTH = 256; const MIN_INPUT_LENGTH = 3; @@ -22,17 +22,30 @@ const Index = ({ const charsLeft = MAX_INPUT_LENGTH - reason.length; return ( - <> - + + Reason (optional)