diff --git a/apollo/subgraph.ts b/apollo/subgraph.ts index 2a53b8fc..83dffd79 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -9655,15 +9655,12 @@ export type TransactionsQueryVariables = Exact<{ export type TransactionsQuery = { __typename: 'Query', transactions: Array<{ __typename: 'Transaction', events?: Array<{ __typename: 'BondEvent', additionalAmount: string, delegator: { __typename: 'Delegator', id: string }, newDelegate: { __typename: 'Transcoder', id: string }, oldDelegate?: { __typename: 'Transcoder', id: string } | null, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'BurnEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'DepositFundedEvent', amount: string, sender: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'EarningsClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'MigrateDelegatorFinalizedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'MintEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'NewRoundEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ParameterUpdateEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'PauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'PollCreatedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'RebondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ReserveClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ReserveFundedEvent', amount: string, reserveHolder: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'RewardEvent', rewardTokens: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ServiceURIUpdateEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'SetCurrentRewardTokensEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'StakeClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderActivatedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderDeactivatedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderEvictedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderResignedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderSlashedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderUpdateEvent', rewardCut: string, feeShare: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TransferBondEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TreasuryVoteEvent', id: string, reason?: string | null, support: TreasuryVoteSupport, timestamp: number, weight: string, proposal: { __typename: 'TreasuryProposal', id: string, targets: Array, description: string }, treasuryVoter: { __typename: 'LivepeerAccount', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'UnbondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'UnpauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'VoteEvent', voter: string, choiceID: string, id: string, timestamp: number, poll: { __typename: 'Poll', id: string, proposal: string, endBlock: string, quorum: string, quota: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number }, round: { __typename: 'Round', id: string } } | { __typename: 'WinningTicketRedeemedEvent', faceValue: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'WithdrawFeesEvent', amount: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'WithdrawStakeEvent', amount: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'WithdrawalEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } }> | null }>, winningTicketRedeemedEvents: Array<{ __typename: 'WinningTicketRedeemedEvent', id: string, faceValue: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } }> }; -export type TranscoderActivatedEventsQueryVariables = Exact<{ - where?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; +export type TranscoderActivationHistoryQueryVariables = Exact<{ + delegate: Scalars['String']; }>; -export type TranscoderActivatedEventsQuery = { __typename: 'Query', transcoderActivatedEvents: Array<{ __typename: 'TranscoderActivatedEvent', activationRound: string, id: string }> }; +export type TranscoderActivationHistoryQuery = { __typename: 'Query', transcoderActivatedEvents: Array<{ __typename: 'TranscoderActivatedEvent', activationRound: string }>, transcoderDeactivatedEvents: Array<{ __typename: 'TranscoderDeactivatedEvent', deactivationRound: string }> }; export type TreasuryProposalQueryVariables = Exact<{ id: Scalars['ID']; @@ -10637,50 +10634,52 @@ export function useTransactionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio export type TransactionsQueryHookResult = ReturnType; export type TransactionsLazyQueryHookResult = ReturnType; export type TransactionsQueryResult = Apollo.QueryResult; -export const TranscoderActivatedEventsDocument = gql` - query transcoderActivatedEvents($where: TranscoderActivatedEvent_filter, $first: Int, $orderBy: TranscoderActivatedEvent_orderBy, $orderDirection: OrderDirection) { +export const TranscoderActivationHistoryDocument = gql` + query transcoderActivationHistory($delegate: String!) { transcoderActivatedEvents( - where: $where - first: $first - orderBy: $orderBy - orderDirection: $orderDirection + where: {delegate: $delegate} + orderBy: activationRound + orderDirection: asc ) { activationRound - id + } + transcoderDeactivatedEvents( + where: {delegate: $delegate} + orderBy: deactivationRound + orderDirection: asc + ) { + deactivationRound } } `; /** - * __useTranscoderActivatedEventsQuery__ + * __useTranscoderActivationHistoryQuery__ * - * To run a query within a React component, call `useTranscoderActivatedEventsQuery` and pass it any options that fit your needs. - * When your component renders, `useTranscoderActivatedEventsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useTranscoderActivationHistoryQuery` and pass it any options that fit your needs. + * When your component renders, `useTranscoderActivationHistoryQuery` 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 } = useTranscoderActivatedEventsQuery({ + * const { data, loading, error } = useTranscoderActivationHistoryQuery({ * variables: { - * where: // value for 'where' - * first: // value for 'first' - * orderBy: // value for 'orderBy' - * orderDirection: // value for 'orderDirection' + * delegate: // value for 'delegate' * }, * }); */ -export function useTranscoderActivatedEventsQuery(baseOptions?: Apollo.QueryHookOptions) { +export function useTranscoderActivationHistoryQuery(baseOptions: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(TranscoderActivatedEventsDocument, options); + return Apollo.useQuery(TranscoderActivationHistoryDocument, options); } -export function useTranscoderActivatedEventsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useTranscoderActivationHistoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(TranscoderActivatedEventsDocument, options); + return Apollo.useLazyQuery(TranscoderActivationHistoryDocument, options); } -export type TranscoderActivatedEventsQueryHookResult = ReturnType; -export type TranscoderActivatedEventsLazyQueryHookResult = ReturnType; -export type TranscoderActivatedEventsQueryResult = Apollo.QueryResult; +export type TranscoderActivationHistoryQueryHookResult = ReturnType; +export type TranscoderActivationHistoryLazyQueryHookResult = ReturnType; +export type TranscoderActivationHistoryQueryResult = Apollo.QueryResult; export const TreasuryProposalDocument = gql` query treasuryProposal($id: ID!) { treasuryProposal(id: $id) { diff --git a/components/OrchestratingView/index.tsx b/components/OrchestratingView/index.tsx index f6fb506a..b0e6368a 100644 --- a/components/OrchestratingView/index.tsx +++ b/components/OrchestratingView/index.tsx @@ -4,9 +4,7 @@ import { Box, Flex, Link as A, Text } from "@livepeer/design-system"; import { ArrowTopRightIcon, CheckIcon, Cross1Icon } from "@modulz/radix-icons"; import { AccountQueryResult, - OrderDirection, - TranscoderActivatedEvent_OrderBy, - useTranscoderActivatedEventsQuery, + useTranscoderActivationHistoryQuery, useTreasuryProposalsQuery, useTreasuryVotesQuery, } from "apollo"; @@ -32,56 +30,128 @@ interface Props { isActive: boolean; } -const Index = ({ currentRound, transcoder, isActive }: Props) => { - const callsMade = useMemo( - () => transcoder?.pools?.filter((r) => r.rewardTokens != null)?.length ?? 0, - [transcoder?.pools] +type ActivationWindow = { start: number; end: number }; +type Participation = { voted: number; eligible: number }; + +const buildActiveWindows = ( + activations: { activationRound: string }[], + deactivations: { deactivationRound: string }[] +): ActivationWindow[] => { + const timeline = [ + ...activations.map((a) => ({ + round: Number(a.activationRound), + type: "activation" as const, + })), + ...deactivations.map((d) => ({ + round: Number(d.deactivationRound), + type: "deactivation" as const, + })), + ].sort((a, b) => a.round - b.round || (a.type === "deactivation" ? -1 : 1)); + + const windows: ActivationWindow[] = []; + let start: number | null = null; + + for (const { type, round } of timeline) { + if (type === "activation") { + if (start === null) { + start = round; + } + } else if (start !== null && round >= start) { + windows.push({ start, end: round }); + start = null; + } + } + + return start !== null + ? [...windows, { start, end: Number.POSITIVE_INFINITY }] + : windows; +}; + +const isDuringWindow = (round: number, windows: ActivationWindow[]) => + windows.some((w) => round >= w.start && round < w.end); + +const isActiveProposal = (voteStart: string, currentRoundId?: string) => + currentRoundId ? Number(voteStart) <= Number(currentRoundId) : true; + +const useGovernanceParticipation = ( + delegateId?: string, + currentRoundId?: string +): { treasury: Participation | null; loading: boolean } => { + const hasDelegate = Boolean(delegateId); + + const { data: activationData, loading: activationLoading } = + useTranscoderActivationHistoryQuery({ + ...(hasDelegate ? { variables: { delegate: delegateId! } } : {}), + fetchPolicy: "cache-and-network", + skip: !hasDelegate, + }); + + const { data: votesData, loading: votesLoading } = useTreasuryVotesQuery({ + ...(hasDelegate ? { variables: { where: { voter: delegateId! } } } : {}), + fetchPolicy: "cache-and-network", + skip: !hasDelegate, + }); + + const activations = useMemo( + () => activationData?.transcoderActivatedEvents ?? [], + [activationData?.transcoderActivatedEvents] + ); + const deactivations = useMemo( + () => activationData?.transcoderDeactivatedEvents ?? [], + [activationData?.transcoderDeactivatedEvents] ); - const scores = useScoreData(transcoder?.id); - const knownRegions = useRegionsData(); + const firstActivationRound = activations[0]?.activationRound; - const { data: firstTranscoderActivatedEventsData } = - useTranscoderActivatedEventsQuery({ - variables: { - where: { - delegate: transcoder?.id, - }, - first: 1, - orderBy: TranscoderActivatedEvent_OrderBy.ActivationRound, - orderDirection: OrderDirection.Asc, - }, + const windows = useMemo( + () => buildActiveWindows(activations, deactivations), + [activations, deactivations] + ); + + const { data: proposalsData, loading: proposalsLoading } = + useTreasuryProposalsQuery({ + variables: firstActivationRound + ? { where: { voteStart_gte: firstActivationRound } } + : undefined, + skip: !firstActivationRound, + fetchPolicy: "cache-and-network", }); - const firstActivationRound = useMemo(() => { - return firstTranscoderActivatedEventsData?.transcoderActivatedEvents[0] - ?.activationRound; - }, [firstTranscoderActivatedEventsData]); + const treasuryParticipation = useMemo(() => { + if (!proposalsData || !votesData) return null; + if (!firstActivationRound) return null; - const { data: treasuryVotesData } = useTreasuryVotesQuery({ - variables: { - where: { - voter: transcoder?.id, - }, - }, - }); + const eligible = proposalsData.treasuryProposals.filter( + (proposal) => + isActiveProposal(proposal.voteStart, currentRoundId) && + isDuringWindow(Number(proposal.voteStart), windows) + ).length; + const voted = votesData.treasuryVotes.filter( + (vote) => + isActiveProposal(vote.proposal.voteStart, currentRoundId) && + isDuringWindow(Number(vote.proposal.voteStart), windows) + ).length; + return { voted, eligible }; + }, [proposalsData, votesData, firstActivationRound, windows, currentRoundId]); - const { data: eligebleProposalsData } = useTreasuryProposalsQuery({ - variables: { - where: { - voteStart_gt: firstActivationRound, - }, - }, - skip: !firstActivationRound, - }); + return { + treasury: treasuryParticipation, + loading: activationLoading || votesLoading || proposalsLoading, + }; +}; - const govStats = useMemo(() => { - if (!treasuryVotesData || !eligebleProposalsData) return null; - return { - voted: treasuryVotesData?.treasuryVotes.length ?? 0, - eligible: eligebleProposalsData?.treasuryProposals.length ?? 0, - }; - }, [treasuryVotesData, eligebleProposalsData]); +const Index = ({ currentRound, transcoder, isActive }: Props) => { + const callsMade = useMemo( + () => transcoder?.pools?.filter((r) => r.rewardTokens != null)?.length ?? 0, + [transcoder?.pools] + ); + + const scores = useScoreData(transcoder?.id); + const knownRegions = useRegionsData(); + const { treasury: govStats } = useGovernanceParticipation( + transcoder?.id, + currentRound?.id + ); const maxScore = useMemo(() => { const topTransData = Object.keys(scores?.scores ?? {}).reduce( @@ -145,6 +215,9 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => { : { score: "N/A", modelText: "" }; }, [knownRegions?.regions, maxScore]); + const govParticipation = + govStats && govStats.eligible > 0 ? govStats.voted / govStats.eligible : 0; + return ( { > { size="2" css={{ color: "$neutral11", fontWeight: 600 }} > - {numbro(govStats.voted / govStats.eligible).format({ + {numbro(govParticipation).format({ output: "percent", mantissa: 0, })}{" "} diff --git a/queries/transcoderActivatedEvents.graphql b/queries/transcoderActivatedEvents.graphql deleted file mode 100644 index de44c762..00000000 --- a/queries/transcoderActivatedEvents.graphql +++ /dev/null @@ -1,16 +0,0 @@ -query transcoderActivatedEvents( - $where: TranscoderActivatedEvent_filter - $first: Int - $orderBy: TranscoderActivatedEvent_orderBy - $orderDirection: OrderDirection -) { - transcoderActivatedEvents( - where: $where - first: $first - orderBy: $orderBy - orderDirection: $orderDirection - ) { - activationRound - id - } -} diff --git a/queries/transcoderActivationHistory.graphql b/queries/transcoderActivationHistory.graphql new file mode 100644 index 00000000..8e05db3d --- /dev/null +++ b/queries/transcoderActivationHistory.graphql @@ -0,0 +1,16 @@ +query transcoderActivationHistory($delegate: String!) { + transcoderActivatedEvents( + where: { delegate: $delegate } + orderBy: activationRound + orderDirection: asc + ) { + activationRound + } + transcoderDeactivatedEvents( + where: { delegate: $delegate } + orderBy: deactivationRound + orderDirection: asc + ) { + deactivationRound + } +}