diff --git a/apollo/subgraph.ts b/apollo/subgraph.ts index 388763d3..c91df564 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -9597,7 +9597,7 @@ export type EventsQueryVariables = Exact<{ }>; -export type EventsQuery = { __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, from: string } } | { __typename: 'BurnEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'DepositFundedEvent', amount: string, sender: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'EarningsClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'MigrateDelegatorFinalizedEvent', l1Addr: string, l2Addr: string, stake: string, delegatedStake: string, fees: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'MintEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'NewRoundEvent', transaction: { __typename: 'Transaction', from: string, id: string, timestamp: number }, round: { __typename: 'Round', id: string } } | { __typename: 'ParameterUpdateEvent', param: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'PauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'PollCreatedEvent', endBlock: string, poll: { __typename: 'Poll', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'RebondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, delegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'ReserveClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'ReserveFundedEvent', amount: string, reserveHolder: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'RewardEvent', rewardTokens: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'ServiceURIUpdateEvent', addr: string, serviceURI: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'SetCurrentRewardTokensEvent', currentInflation: string, currentMintableTokens: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'StakeClaimedEvent', stake: string, fees: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderActivatedEvent', activationRound: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderDeactivatedEvent', deactivationRound: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderEvictedEvent', delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderResignedEvent', delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderSlashedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderUpdateEvent', rewardCut: string, feeShare: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TransferBondEvent', amount: string, newDelegator: { __typename: 'Delegator', id: string }, oldDelegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TreasuryVoteEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'UnbondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, delegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'UnpauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'VoteEvent', voter: string, choiceID: string, poll: { __typename: 'Poll', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'WinningTicketRedeemedEvent', faceValue: string, recipient: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'WithdrawFeesEvent', amount: string, delegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'WithdrawStakeEvent', amount: string, delegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'WithdrawalEvent', deposit: string, reserve: string, sender: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } }> | null }>, transcoders: Array<{ __typename: 'Transcoder', id: string }> }; +export type EventsQuery = { __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, from: string } } | { __typename: 'BurnEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'DepositFundedEvent', amount: string, sender: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'EarningsClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'MigrateDelegatorFinalizedEvent', l1Addr: string, l2Addr: string, stake: string, delegatedStake: string, fees: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'MintEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'NewRoundEvent', transaction: { __typename: 'Transaction', from: string, id: string, timestamp: number }, round: { __typename: 'Round', id: string } } | { __typename: 'ParameterUpdateEvent', param: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'PauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'PollCreatedEvent', endBlock: string, poll: { __typename: 'Poll', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'RebondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, delegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'ReserveClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'ReserveFundedEvent', amount: string, reserveHolder: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'RewardEvent', rewardTokens: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'ServiceURIUpdateEvent', addr: string, serviceURI: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'SetCurrentRewardTokensEvent', currentInflation: string, currentMintableTokens: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'StakeClaimedEvent', stake: string, fees: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderActivatedEvent', activationRound: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderDeactivatedEvent', deactivationRound: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderEvictedEvent', delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderResignedEvent', delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderSlashedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TranscoderUpdateEvent', rewardCut: string, feeShare: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TransferBondEvent', amount: string, newDelegator: { __typename: 'Delegator', id: string }, oldDelegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'TreasuryVoteEvent', support: TreasuryVoteSupport, proposal: { __typename: 'TreasuryProposal', id: string, description: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'UnbondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, delegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'UnpauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'VoteEvent', voter: string, choiceID: string, poll: { __typename: 'Poll', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'WinningTicketRedeemedEvent', faceValue: string, recipient: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'WithdrawFeesEvent', amount: string, delegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'WithdrawStakeEvent', amount: string, delegator: { __typename: 'Delegator', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } } | { __typename: 'WithdrawalEvent', deposit: string, reserve: string, sender: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number, from: string } }> | null }>, transcoders: Array<{ __typename: 'Transcoder', id: string }> }; export type MetaQueryVariables = Exact<{ [key: string]: never; }>; @@ -9653,7 +9653,17 @@ 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', 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', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __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 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 TranscoderActivatedEventsQuery = { __typename: 'Query', transcoderActivatedEvents: Array<{ __typename: 'TranscoderActivatedEvent', activationRound: string, id: string }> }; export type TreasuryProposalQueryVariables = Exact<{ id: Scalars['ID']; @@ -9662,11 +9672,30 @@ export type TreasuryProposalQueryVariables = Exact<{ export type TreasuryProposalQuery = { __typename: 'Query', treasuryProposal?: { __typename: 'TreasuryProposal', id: string, description: string, calldatas: Array, targets: Array, values: Array, voteEnd: string, voteStart: string, proposer: { __typename: 'LivepeerAccount', id: string } } | null }; -export type TreasuryProposalsQueryVariables = Exact<{ [key: string]: never; }>; +export type TreasuryProposalsQueryVariables = Exact<{ + where?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; +}>; 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 TreasuryVoteEventsQueryVariables = Exact<{ + first?: InputMaybe; + where?: InputMaybe; +}>; + + +export type TreasuryVoteEventsQuery = { __typename: 'Query', treasuryVoteEvents: Array<{ __typename: 'TreasuryVoteEvent', id: string, reason?: string | null, support: TreasuryVoteSupport, timestamp: number, weight: string, proposal: { __typename: 'TreasuryProposal', id: string, targets: Array, description: string }, voter: { __typename: 'LivepeerAccount', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } }> }; + +export type TreasuryVotesQueryVariables = Exact<{ + where?: InputMaybe; +}>; + + +export type TreasuryVotesQuery = { __typename: 'Query', treasuryVotes: Array<{ __typename: 'TreasuryVote', id: string, reason?: string | null, support: TreasuryVoteSupport, weight: string, proposal: { __typename: 'TreasuryProposal', id: string, voteStart: string, voteEnd: string }, voter: { __typename: 'LivepeerAccount', id: string, delegate?: { __typename: 'Transcoder', id: string, activationRound: string, deactivationRound: string } | null } }> }; + export type VoteQueryVariables = Exact<{ id: Scalars['ID']; }>; @@ -9904,7 +9933,12 @@ export type DaysLazyQueryHookResult = ReturnType; export type DaysQueryResult = Apollo.QueryResult; export const EventsDocument = gql` query events($first: Int) { - transactions(first: $first, orderBy: timestamp, orderDirection: desc) { + transactions( + first: $first + orderBy: timestamp + orderDirection: desc + where: {timestamp_lt: 1768380104} + ) { events { __typename round { @@ -10066,6 +10100,13 @@ export const EventsDocument = gql` stake fees } + ... on TreasuryVoteEvent { + support + proposal { + id + description + } + } } } transcoders(where: {active: true}) { @@ -10507,6 +10548,48 @@ export const TransactionsDocument = gql` } amount } + ... on TreasuryVoteEvent { + id + reason + support + timestamp + proposal { + id + targets + description + } + treasuryVoter: voter { + id + } + weight + round { + id + } + transaction { + id + timestamp + } + } + ... on VoteEvent { + voter + poll { + id + proposal + endBlock + quorum + quota + } + transaction { + id + timestamp + } + choiceID + id + round { + id + } + timestamp + } } } winningTicketRedeemedEvents( @@ -10559,6 +10642,50 @@ 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) { + transcoderActivatedEvents( + where: $where + first: $first + orderBy: $orderBy + orderDirection: $orderDirection + ) { + activationRound + id + } +} + `; + +/** + * __useTranscoderActivatedEventsQuery__ + * + * 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 + * 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({ + * variables: { + * where: // value for 'where' + * first: // value for 'first' + * orderBy: // value for 'orderBy' + * orderDirection: // value for 'orderDirection' + * }, + * }); + */ +export function useTranscoderActivatedEventsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(TranscoderActivatedEventsDocument, options); + } +export function useTranscoderActivatedEventsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(TranscoderActivatedEventsDocument, options); + } +export type TranscoderActivatedEventsQueryHookResult = ReturnType; +export type TranscoderActivatedEventsLazyQueryHookResult = ReturnType; +export type TranscoderActivatedEventsQueryResult = Apollo.QueryResult; export const TreasuryProposalDocument = gql` query treasuryProposal($id: ID!) { treasuryProposal(id: $id) { @@ -10604,8 +10731,12 @@ export type TreasuryProposalQueryHookResult = ReturnType; export type TreasuryProposalQueryResult = Apollo.QueryResult; export const TreasuryProposalsDocument = gql` - query treasuryProposals { - treasuryProposals(orderBy: voteStart, orderDirection: desc) { + query treasuryProposals($where: TreasuryProposal_filter, $orderBy: TreasuryProposal_orderBy = voteStart, $orderDirection: OrderDirection = desc) { + treasuryProposals( + where: $where + orderBy: $orderBy + orderDirection: $orderDirection + ) { id description calldatas @@ -10632,6 +10763,9 @@ export const TreasuryProposalsDocument = gql` * @example * const { data, loading, error } = useTreasuryProposalsQuery({ * variables: { + * where: // value for 'where' + * orderBy: // value for 'orderBy' + * orderDirection: // value for 'orderDirection' * }, * }); */ @@ -10646,6 +10780,117 @@ export function useTreasuryProposalsLazyQuery(baseOptions?: Apollo.LazyQueryHook export type TreasuryProposalsQueryHookResult = ReturnType; export type TreasuryProposalsLazyQueryHookResult = ReturnType; export type TreasuryProposalsQueryResult = Apollo.QueryResult; +export const TreasuryVoteEventsDocument = gql` + query treasuryVoteEvents($first: Int, $where: TreasuryVoteEvent_filter) { + treasuryVoteEvents( + orderBy: timestamp + orderDirection: desc + first: $first + where: $where + ) { + id + reason + support + timestamp + proposal { + id + targets + description + } + voter { + id + } + weight + round { + id + } + transaction { + id + timestamp + } + } +} + `; + +/** + * __useTreasuryVoteEventsQuery__ + * + * To run a query within a React component, call `useTreasuryVoteEventsQuery` and pass it any options that fit your needs. + * When your component renders, `useTreasuryVoteEventsQuery` 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 } = useTreasuryVoteEventsQuery({ + * variables: { + * first: // value for 'first' + * where: // value for 'where' + * }, + * }); + */ +export function useTreasuryVoteEventsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(TreasuryVoteEventsDocument, options); + } +export function useTreasuryVoteEventsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(TreasuryVoteEventsDocument, options); + } +export type TreasuryVoteEventsQueryHookResult = ReturnType; +export type TreasuryVoteEventsLazyQueryHookResult = ReturnType; +export type TreasuryVoteEventsQueryResult = Apollo.QueryResult; +export const TreasuryVotesDocument = gql` + query treasuryVotes($where: TreasuryVote_filter) { + treasuryVotes(where: $where) { + id + reason + support + weight + proposal { + id + voteStart + voteEnd + } + voter { + id + delegate { + id + activationRound + deactivationRound + } + } + } +} + `; + +/** + * __useTreasuryVotesQuery__ + * + * To run a query within a React component, call `useTreasuryVotesQuery` and pass it any options that fit your needs. + * When your component renders, `useTreasuryVotesQuery` 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 } = useTreasuryVotesQuery({ + * variables: { + * where: // value for 'where' + * }, + * }); + */ +export function useTreasuryVotesQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(TreasuryVotesDocument, options); + } +export function useTreasuryVotesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(TreasuryVotesDocument, options); + } +export type TreasuryVotesQueryHookResult = ReturnType; +export type TreasuryVotesLazyQueryHookResult = ReturnType; +export type TreasuryVotesQueryResult = Apollo.QueryResult; export const VoteDocument = gql` query vote($id: ID!) { vote(id: $id) { diff --git a/components/EthAddressBadge/index.tsx b/components/EthAddressBadge/index.tsx new file mode 100644 index 00000000..5faf81f2 --- /dev/null +++ b/components/EthAddressBadge/index.tsx @@ -0,0 +1,21 @@ +import { Badge } from "@livepeer/design-system"; +import { useEnsData } from "hooks"; +import Link from "next/link"; + +interface EthAddressBadgeProps { + value: string | undefined; +} + +const EthAddressBadge = ({ value }: EthAddressBadgeProps) => { + const ensName = useEnsData(value); + + return ( + + + {ensName?.name ? ensName?.name : ensName?.idShort ?? ""} + + + ); +}; + +export default EthAddressBadge; diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index 1bfd9c97..cbc0ce70 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -1,20 +1,31 @@ import Spinner from "@components/Spinner"; +import TransactionBadge from "@components/TransactionBadge"; +import { Fm, parsePollIpfs } from "@lib/api/polls"; +import { parseProposalText, Proposal } from "@lib/api/treasury"; +import { POLL_VOTES, VOTING_SUPPORT_MAP } from "@lib/api/types/votes"; import dayjs from "@lib/dayjs"; -import { formatAddress, formatTransactionHash } from "@lib/utils"; +import { formatAddress } from "@lib/utils"; import { + Badge, Box, Card as CardBase, Flex, Link as A, styled, } from "@livepeer/design-system"; -import { ExternalLinkIcon } from "@modulz/radix-icons"; -import { useTransactionsQuery } from "apollo"; +import { + TransactionsQuery, + TreasuryVoteEvent, + TreasuryVoteSupport, + useTransactionsQuery, + VoteEvent, +} from "apollo"; import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "lib/chains"; import { useRouter } from "next/router"; import numbro from "numbro"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import InfiniteScroll from "react-infinite-scroll-component"; +import { catIpfsJson, IpfsPoll } from "utils/ipfs"; const Card = styled(CardBase, { length: {}, @@ -28,16 +39,20 @@ const Index = () => { const query = router.query; const account = query.account as string; - const { data, loading, error, fetchMore, stopPolling } = useTransactionsQuery( - { - variables: { - account: account.toLowerCase(), - first: 10, - skip: 0, - }, - notifyOnNetworkStatusChange: true, - } - ); + const { + data, + loading, + error, + fetchMore: fetchMoreTransactions, + stopPolling, + } = useTransactionsQuery({ + variables: { + account: account.toLowerCase(), + first: 10, + skip: 0, + }, + notifyOnNetworkStatusChange: true, + }); const events = useMemo(() => { // First reverse the order of the array of events per transaction to have events in descending order @@ -50,26 +65,115 @@ const Index = () => { return reversedEvents?.flatMap(({ events: e }) => e ?? []) ?? []; }, [data]); + type TransactionEvent = NonNullable< + TransactionsQuery["transactions"][number]["events"] + >[number]; + const isType = + (t: T) => + (e: TransactionEvent): e is Extract => + e.__typename === t; + const isVoteEvent = isType("VoteEvent"); + const isTreasuryVoteEvent = isType("TreasuryVoteEvent"); + const lastEventTimestamp = useMemo( () => Number(events?.[(events?.length || 0) - 1]?.transaction?.timestamp ?? 0), [events] ); + const [extendedVoteEventsData, setExtendedVoteEventsData] = useState< + (VoteEvent & { attributes: Fm | null })[] + >([]); + useEffect(() => { + // Enrich poll vote events with parsed IPFS proposal metadata. + const getExtendedVoteEventsData = async () => { + const newVoteEvents = events + .filter(isVoteEvent) + .filter((e) => !extendedVoteEventsData.find((ve) => ve.id === e.id)); + const newExtendedVoteEventsData = await Promise.all( + newVoteEvents.map(async (voteEvent) => { + const ipfsObject = await catIpfsJson( + voteEvent.poll?.proposal + ); + const attributes = parsePollIpfs(ipfsObject); + return { + ...voteEvent, + attributes, + }; + }) || [] + ); + setExtendedVoteEventsData( + (current) => + [...current, ...newExtendedVoteEventsData] as (VoteEvent & { + attributes: Fm | null; + })[] + ); + }; + getExtendedVoteEventsData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [events]); + + const [extendedTreasuryVoteEventsData, setExtendedTreasuryVoteEventsData] = + useState<(TreasuryVoteEvent & { attributes: Fm | null })[]>([]); + useEffect(() => { + // Attach parsed treasury proposal attributes to treasury vote events. + const newTreasuryVoteEvents = events + .filter(isTreasuryVoteEvent) + .filter( + (e) => !extendedTreasuryVoteEventsData.find((te) => te.id === e.id) + ); + const newExtendedTreasureVoteEventsData = newTreasuryVoteEvents.map( + (treasuryVoteEvent) => { + const parsed = parseProposalText( + treasuryVoteEvent.proposal as Proposal + ); + return { + ...treasuryVoteEvent, + attributes: parsed.attributes, + }; + } + ); + setExtendedTreasuryVoteEventsData( + (current) => + [ + ...current, + ...newExtendedTreasureVoteEventsData, + ] as (TreasuryVoteEvent & { attributes: Fm | null })[] + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [events]); + // performs filtering of winning ticket redeemed events and merges with separate "winning tickets" // this is so Os winning tickets show properly: https://github.com/livepeer/explorer/issues/108 const mergedEvents = useMemo( () => [ - ...events.filter((e) => e?.__typename !== "WinningTicketRedeemedEvent"), + ...events.filter( + (e) => + e?.__typename !== "WinningTicketRedeemedEvent" && + e?.__typename !== "TreasuryVoteEvent" && + e?.__typename !== "VoteEvent" + ), ...(data?.winningTicketRedeemedEvents?.filter( (e) => (e?.transaction?.timestamp ?? 0) > lastEventTimestamp ) ?? []), + ...extendedTreasuryVoteEventsData.filter( + (e) => (e?.transaction?.timestamp ?? 0) > lastEventTimestamp + ), + ...extendedVoteEventsData.filter( + (e) => (e?.transaction?.timestamp ?? 0) > lastEventTimestamp + ), ].sort( (a, b) => (b?.transaction?.timestamp ?? 0) - (a?.transaction?.timestamp ?? 0) ), - [events, data, lastEventTimestamp] + [ + events, + data, + lastEventTimestamp, + extendedTreasuryVoteEventsData, + extendedVoteEventsData, + ] ); if (error) { @@ -104,7 +208,7 @@ const Index = () => { stopPolling(); if (!loading && data.transactions.length >= 10) { try { - await fetchMore({ + await fetchMoreTransactions({ variables: { skip: data.transactions.length, }, @@ -112,6 +216,7 @@ const Index = () => { if (!fetchMoreResult) { return previousResult; } + return { ...previousResult, transactions: [ @@ -203,19 +308,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + {" "} @@ -263,19 +358,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + Round # @@ -320,19 +405,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + {" "} @@ -382,19 +457,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + {" "} @@ -444,19 +509,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + {" "} @@ -504,19 +559,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + @@ -568,19 +613,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + {" "} @@ -627,19 +662,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + {" "} @@ -686,19 +711,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + {" "} @@ -746,19 +761,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + {" "} @@ -811,19 +816,9 @@ function renderSwitch(event, i: number) { .format("MM/DD/YYYY h:mm:ss a")}{" "} - Round #{event.round.id} - - - {formatTransactionHash(event.transaction.id)} - - - + + + {" "} @@ -839,6 +834,143 @@ function renderSwitch(event, i: number) { ); + case "TreasuryVoteEvent": + const supportTreasuryVoteEvent = + VOTING_SUPPORT_MAP[event.support] || + VOTING_SUPPORT_MAP[TreasuryVoteSupport.Abstain]; + return ( + + + + + Voted on treasury proposal " + {event.attributes?.title?.trim()}" + + + {dayjs + .unix(event.transaction.timestamp) + .format("MM/DD/YYYY h:mm:ss a")}{" "} + - Round #{event.round.id} + + + + + + + + + {supportTreasuryVoteEvent.text} + + + + + ); + case "VoteEvent": + const supportVoteEvent = POLL_VOTES[event.choiceID]; + if (!supportVoteEvent) { + return null; + } + return ( + + + + + Voted on poll "{event.attributes?.title?.trim()}" + + + {dayjs + .unix(event.transaction.timestamp) + .format("MM/DD/YYYY h:mm:ss a")}{" "} + - Round #{event.round.id} + + + + + + + + + {supportVoteEvent.text} + + + + + ); default: return null; } diff --git a/components/HorizontalScrollContainer/index.tsx b/components/HorizontalScrollContainer/index.tsx index 44ea0d80..5a0d95b4 100644 --- a/components/HorizontalScrollContainer/index.tsx +++ b/components/HorizontalScrollContainer/index.tsx @@ -97,8 +97,14 @@ const HorizontalScrollContainer = forwardRef< { const scores = useScoreData(transcoder?.id); const knownRegions = useRegionsData(); + const { data: firstTranscoderActivatedEventsData } = + useTranscoderActivatedEventsQuery({ + variables: { + where: { + delegate: transcoder?.id, + }, + first: 1, + orderBy: TranscoderActivatedEvent_OrderBy.ActivationRound, + orderDirection: OrderDirection.Asc, + }, + }); + + const firstActivationRound = useMemo(() => { + return firstTranscoderActivatedEventsData?.transcoderActivatedEvents[0] + ?.activationRound; + }, [firstTranscoderActivatedEventsData]); + + const { data: treasuryVotesData } = useTreasuryVotesQuery({ + variables: { + where: { + voter: transcoder?.id, + }, + }, + }); + + const { data: eligebleProposalsData } = useTreasuryProposalsQuery({ + variables: { + where: { + voteStart_gt: firstActivationRound, + }, + }, + skip: !firstActivationRound, + }); + + const govStats = useMemo(() => { + if (!treasuryVotesData || !eligebleProposalsData) return null; + return { + voted: treasuryVotesData?.treasuryVotes.length ?? 0, + eligible: eligebleProposalsData?.treasuryProposals.length ?? 0, + }; + }, [treasuryVotesData, eligebleProposalsData]); + const maxScore = useMemo(() => { const topTransData = Object.keys(scores?.scores ?? {}).reduce( (prev, curr) => { @@ -279,6 +329,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/Poll/PollVotingWidget/index.tsx b/components/Poll/PollVotingWidget/index.tsx new file mode 100644 index 00000000..9420870f --- /dev/null +++ b/components/Poll/PollVotingWidget/index.tsx @@ -0,0 +1,641 @@ +import VoteButton from "@components/VoteButton"; +import { PollExtended } from "@lib/api/polls"; +import dayjs from "@lib/dayjs"; +import { abbreviateNumber, formatAddress } from "@lib/utils"; +import { + Box, + Button, + Dialog, + DialogClose, + DialogContent, + DialogTitle, + Flex, + Heading, + Text, + useSnackbar, +} from "@livepeer/design-system"; +import { + CheckCircledIcon, + Cross1Icon, + CrossCircledIcon, +} from "@radix-ui/react-icons"; +import { AccountQuery, PollChoice, TranscoderStatus } from "apollo"; +import { useAccountAddress, usePendingFeesAndStakeData } from "hooks"; +import { useEffect, useMemo, useState } from "react"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { formatPercent, getVotingPower } from "utils/voting"; + +import Check from "../../../public/img/check.svg"; +import Copy from "../../../public/img/copy.svg"; + +type Props = { + poll: PollExtended; + delegateVote: + | { + __typename: "Vote"; + choiceID?: PollChoice; + voteStake: string; + nonVoteStake: string; + } + | undefined + | null; + vote: + | { + __typename: "Vote"; + choiceID?: PollChoice; + voteStake: string; + nonVoteStake: string; + } + | undefined + | null; + myAccount: AccountQuery; +}; + +const SectionLabel = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +const Index = ({ data }: { data: Props }) => { + const accountAddress = useAccountAddress(); + const [copied, setCopied] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [openSnackbar] = useSnackbar(); + + useEffect(() => { + if (copied) { + setTimeout(() => { + setCopied(false); + }, 5000); + } + }, [copied]); + + const pendingFeesAndStake = usePendingFeesAndStakeData( + data?.myAccount?.delegator?.id + ); + + const votingPower = useMemo( + () => + getVotingPower( + accountAddress ?? "", + data?.myAccount, + data?.vote, + pendingFeesAndStake?.pendingStake + ? pendingFeesAndStake?.pendingStake + : "0" + ), + [accountAddress, data, pendingFeesAndStake] + ); + + let delegate: { + __typename: "Transcoder"; + id: string; + active: boolean; + status: TranscoderStatus; + totalStake: string; + } | null = null; + + if (data?.myAccount?.delegator?.delegate) { + delegate = data?.myAccount?.delegator?.delegate; + } + + return ( + + + + Do you support LIP-{data?.poll?.attributes?.lip ?? "ERR"}? + + + {/* ========== RESULTS SECTION ========== */} + + Results + + + {/* For bar */} + + + + For + + + + + + + + {formatPercent(data.poll.percent.yes, 2)} + + + + {/* Against bar */} + + + + + Against + + + + + + + + + {formatPercent(data.poll.percent.no, 2)} + + + + + + {data.poll.votes.length}{" "} + {`${ + data.poll.votes.length > 1 || data.poll.votes.length === 0 + ? "votes" + : "vote" + }`}{" "} + · {abbreviateNumber(data.poll.stake.voters, 4)} LPT ·{" "} + {data.poll.status !== "active" + ? "Final Results" + : dayjs + .duration( + dayjs().unix() - data.poll.estimatedEndTime, + "seconds" + ) + .humanize() + " left"} + + + + {/* ========== YOUR VOTE SECTION ========== */} + {accountAddress ? ( + + Your vote + + + + My Delegate Vote{" "} + {delegate && `(${formatAddress(delegate?.id)})`} + + + {data?.delegateVote?.choiceID + ? data?.delegateVote?.choiceID === "Yes" + ? "For" + : "Against" + : "N/A"} + + + + + My Vote ({formatAddress(accountAddress)}) + + + {data?.vote?.choiceID + ? data?.vote?.choiceID === "Yes" + ? "For" + : "Against" + : "N/A"} + + + {((!data?.vote?.choiceID && data.poll.status === "active") || + data?.vote?.choiceID) && ( + + + My Voting Power + + + + {abbreviateNumber(votingPower, 4)} LPT ( + {( + (+votingPower / + (data.poll.stake.nonVoters + + data.poll.stake.voters)) * + 100 + ).toPrecision(2)} + %) + + + + )} + + {data.poll.status === "active" && ( + + )} + + ) : ( + + Your vote + + + + Connect your wallet to vote. + + + + )} + + + {data.poll.status === "active" && ( + + + Are you an orchestrator?{" "} + setModalOpen(true)} + css={{ color: "$primary11", cursor: "pointer" }} + > + Follow these instructions + {" "} + if you prefer to vote with the Livepeer CLI. + + + )} + + + + + + Livepeer CLI Voting Instructions + + + + + + + + + + + Run the Livepeer CLI and select the option to "Vote on a + poll". When prompted for a contract address, copy and paste + this poll's contract address: + + + {data.poll.id} + { + setCopied(true); + openSnackbar("Copied to clipboard"); + }} + > + + {copied ? ( + + ) : ( + + )} + + + + + + + The Livepeer CLI will prompt you for your vote. Enter 0 to vote + "For" or 1 to vote "Against". + + + + + Once your vote is confirmed, check back here to see it reflected + in the UI. + + + + + + + ); +}; + +export default Index; + +function PollVoteButton({ + vote, + poll, + pendingStake, +}: { + vote: Props["vote"]; + poll: Props["poll"]; + pendingStake: string; +}) { + switch (vote?.choiceID) { + case "Yes": + return ( + 0)} + css={{ + marginTop: "$4", + width: "100%", + backgroundColor: "$tomato3", + color: "$tomato11", + fontWeight: 600, + border: "1px solid $tomato4", + "&:hover": { + backgroundColor: "$tomato4", + borderColor: "$tomato5", + }, + }} + size="4" + choiceId={1} + pollAddress={poll?.id} + > + + + Change Vote To Against + + + ); + case "No": + return ( + 0)} + css={{ + marginTop: "$4", + width: "100%", + backgroundColor: "$grass3", + color: "$grass11", + fontWeight: 600, + border: "1px solid $grass4", + "&:hover": { + backgroundColor: "$grass4", + borderColor: "$grass5", + }, + }} + size="4" + choiceId={0} + pollAddress={poll?.id} + > + + + Change Vote To For + + + ); + default: + return ( + + 0)} + css={{ + backgroundColor: "$grass3", + color: "$grass11", + fontWeight: 600, + border: "1px solid $grass4", + "&:hover": { + backgroundColor: "$grass4", + borderColor: "$grass5", + }, + }} + choiceId={0} + size="4" + pollAddress={poll?.id} + > + + + For + + + 0)} + css={{ + backgroundColor: "$tomato3", + color: "$tomato11", + fontWeight: 600, + border: "1px solid $tomato4", + "&:hover": { + backgroundColor: "$tomato4", + borderColor: "$tomato5", + }, + }} + size="4" + choiceId={1} + pollAddress={poll?.id} + > + + + Against + + + + ); + } +} diff --git a/components/Table/index.tsx b/components/Table/index.tsx index c0d2df11..a58a2d7b 100644 --- a/components/Table/index.tsx +++ b/components/Table/index.tsx @@ -77,7 +77,7 @@ function DataTable({ <> {input && ( @@ -95,7 +95,7 @@ function DataTable({ css={{ borderCollapse: "collapse", tableLayout: "auto", - minWidth: 980, + minWidth: 960, width: "100%", "@bp4": { width: "100%", diff --git a/components/TransactionBadge/index.tsx b/components/TransactionBadge/index.tsx new file mode 100644 index 00000000..cac3d7d0 --- /dev/null +++ b/components/TransactionBadge/index.tsx @@ -0,0 +1,52 @@ +import { formatTransactionHash } from "@lib/utils"; +import { Badge, Box, Link as A } from "@livepeer/design-system"; +import { ArrowTopRightIcon } from "@modulz/radix-icons"; + +interface TransactionBadgeProps { + id: string | undefined; +} + +const TransactionBadge = ({ id }: TransactionBadgeProps) => { + return ( + *": { + border: "1.5px solid $grass7 !important", + backgroundColor: "$grass3 !important", + color: "$grass11 !important", + }, + }} + > + + {formatTransactionHash(id)} + + + + ); +}; + +export default TransactionBadge; diff --git a/components/TransactionsList/index.tsx b/components/TransactionsList/index.tsx index f887da8a..b1b10d78 100644 --- a/components/TransactionsList/index.tsx +++ b/components/TransactionsList/index.tsx @@ -1,12 +1,12 @@ +import EthAddressBadge from "@components/EthAddressBadge"; import Table from "@components/Table"; +import TransactionBadge from "@components/TransactionBadge"; +import { parseProposalText } from "@lib/api/treasury"; +import { VOTING_SUPPORT_MAP } from "@lib/api/types/votes"; 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"; +import { EventsQueryResult, TreasuryProposal } from "apollo"; import { sentenceCase } from "change-case"; -import { useEnsData } from "hooks"; -import Link from "next/link"; import numbro from "numbro"; import { useCallback, useMemo } from "react"; @@ -57,39 +57,6 @@ const getPercentAmount = (number: number | string | undefined) => { ); }; -const EthAddress = (props: { value: string | undefined }) => { - const ensName = useEnsData(props.value); - - return ( - - - {ensName?.name ? ensName?.name : ensName?.idShort ?? ""} - - - ); -}; - -const Transaction = (props: { id: string | undefined }) => { - return ( - - - {props.id ? formatTransactionHash(props.id) : "N/A"} - - - - ); -}; - const renderEmoji = (emoji: string) => ( {emoji} @@ -113,86 +80,89 @@ const TransactionsList = ({ ) => { switch (event.__typename) { case "BondEvent": - return ; + return ; case "UnbondEvent": - return ; + return ; case "RebondEvent": - return ; + return ; case "TranscoderUpdateEvent": - return ; + return ; case "RewardEvent": - return ; + return ; case "WithdrawStakeEvent": - return ; + return ; case "WithdrawFeesEvent": - return ; + return ; case "WinningTicketRedeemedEvent": - return ; + return ; case "DepositFundedEvent": - return ; + return ; case "ReserveFundedEvent": - return ; + return ; case "TransferBondEvent": - return ; + return ; case "TranscoderActivatedEvent": - return ; + return ; case "TranscoderDeactivatedEvent": - return ; + return ; // case "EarningsClaimedEvent": - // return ; + // return ; case "TranscoderResignedEvent": - return ; + return ; case "TranscoderEvictedEvent": - return ; + return ; case "NewRoundEvent": - return ; + return ; case "WithdrawalEvent": - return ; + return ; case "SetCurrentRewardTokensEvent": - return ; + return ; case "PauseEvent": - return ; + return ; case "UnpauseEvent": - return ; + return ; case "ParameterUpdateEvent": - return ; + return ; case "VoteEvent": - return ; + return ; case "PollCreatedEvent": - return ; + return ; case "ServiceURIUpdateEvent": - return ; + return ; // case "MintEvent": - // return ; + // return ; case "BurnEvent": - return ; + return ; case "MigrateDelegatorFinalizedEvent": - return ; + return ; case "StakeClaimedEvent": - return ; + return ; + + case "TreasuryVoteEvent": + return ; default: return {`Error fetching event information.`}; @@ -212,16 +182,16 @@ const TransactionsList = ({ return event?.additionalAmount === "0" && event?.oldDelegate?.id ? ( {`Migrated from `} - + {` to `} - + ) : ( {`Delegated `} {getLptAmount(event?.additionalAmount)} {` to `} - + ); case "UnbondEvent": @@ -230,7 +200,7 @@ const TransactionsList = ({ {`Undelegated `} {getLptAmount(event.amount)} {` from `} - + ); case "RebondEvent": @@ -239,7 +209,7 @@ const TransactionsList = ({ {`Rebonded `} {getLptAmount(event.amount)} {` to `} - + ); case "TranscoderUpdateEvent": @@ -303,9 +273,9 @@ const TransactionsList = ({ {getLptAmount(Number(event?.amount))} {` was transferred between `} - + {` and `} - + ); case "TranscoderActivatedEvent": @@ -379,10 +349,14 @@ const TransactionsList = ({ {`Voted `} - {+event?.choiceID === 0 ? '"Yes"' : '"No"'} + {+event?.choiceID === 0 ? '"For"' : '"Against"'} {` on a proposal`} {renderEmoji("👩‍⚖️")} @@ -392,7 +366,7 @@ const TransactionsList = ({ return ( {`Poll `} - + {` has been created and will end on block ${getRound( event?.endBlock )}`} @@ -425,7 +399,24 @@ const TransactionsList = ({ {` from L1 Ethereum`} ); + case "TreasuryVoteEvent": { + const support = VOTING_SUPPORT_MAP[event.support]; + const title = parseProposalText(event.proposal as TreasuryProposal) + .attributes.title; + return ( + + Voted{" "} + + "{support.text}" + {" "} + on{" "} + + {title} + + + ); + } default: return {`Error fetching event information.`}; } @@ -544,7 +535,7 @@ const TransactionsList = ({ }} size="2" > - + ), diff --git a/components/Treasury/TreasuryVoteDetail/index.tsx b/components/Treasury/TreasuryVoteDetail/index.tsx new file mode 100644 index 00000000..80797173 --- /dev/null +++ b/components/Treasury/TreasuryVoteDetail/index.tsx @@ -0,0 +1,356 @@ +import TransactionBadge from "@components/TransactionBadge"; +import { parseProposalText } from "@lib/api/treasury"; +import { VOTING_SUPPORT_MAP } from "@lib/api/types/votes"; +import dayjs from "@lib/dayjs"; +import { + Badge, + Box, + Card, + Flex, + Heading, + Link, + Text, +} from "@livepeer/design-system"; +import { ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons"; +import { TreasuryVoteEvent, TreasuryVoteSupport } from "apollo"; +import React, { useState } from "react"; + +interface TreasuryVoteDetailProps { + vote: TreasuryVoteEvent; + formatWeight: (weight: string) => string; +} + +const Index: React.FC = ({ vote, formatWeight }) => { + const [isExpanded, setIsExpanded] = useState(false); + const support = + VOTING_SUPPORT_MAP[vote.support] || + VOTING_SUPPORT_MAP[TreasuryVoteSupport.Abstain]; + const hasReason = + vote.reason && vote.reason.toLowerCase() !== "no reason provided"; + + const title = parseProposalText(vote.proposal).attributes.title; + const reasonId = `reason-${vote.transaction.id}`; + + return ( + + {/* Mobile Card Layout */} + + + {/* Hero: Vote badge */} + + + {support.text} + + + {/* Title link */} + + + {title} + + + + {/* Weight */} + + {formatWeight(vote.weight)} + + + {/* Collapsible Reason */} + {hasReason && ( + + setIsExpanded(!isExpanded)} + aria-expanded={isExpanded} + aria-controls={reasonId} + css={{ + display: "flex", + alignItems: "center", + gap: "$1", + color: "$primary11", + cursor: "pointer", + border: "none", + backgroundColor: "transparent", + padding: "$2", + margin: "-$2", + borderRadius: "$1", + minHeight: "44px", + fontSize: "$1", + fontWeight: 600, + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: "$neutral3", + }, + "&:focus-visible": { + outline: "2px solid $primary11", + outlineOffset: "2px", + }, + }} + > + + {isExpanded ? "Hide reason" : "Show reason"} + + {isExpanded && ( + + + “{vote.reason}” + + + )} + + )} + + {/* Footer: Transaction + Timestamp */} + + {vote.transaction.id ? ( + + ) : ( + + N/A + + )} + + · + + + {dayjs.unix(vote.transaction.timestamp).format("MMM D, h:mm a")} + + + + + + {/* Desktop Timeline Layout */} + + {/* Timeline Dot */} + + + + + {/* Hero: Vote badge */} + + + {support.text} + + + {/* Title link */} + + + {title} + + + + {/* Weight */} + + {formatWeight(vote.weight)} + + + {/* Collapsible Reason */} + {hasReason && ( + + setIsExpanded(!isExpanded)} + aria-expanded={isExpanded} + aria-controls={reasonId} + css={{ + display: "flex", + alignItems: "center", + gap: "$1", + color: "$primary11", + cursor: "pointer", + border: "none", + backgroundColor: "transparent", + padding: "$2", + margin: "-$2", + borderRadius: "$1", + minHeight: "44px", + fontSize: "$1", + fontWeight: 600, + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: "$neutral3", + }, + "&:focus-visible": { + outline: "2px solid $primary11", + outlineOffset: "2px", + }, + }} + > + + {isExpanded ? "Hide reason" : "Show reason"} + + {isExpanded && ( + + + “{vote.reason}” + + + )} + + )} + + {/* Footer: Transaction + Timestamp */} + + {vote.transaction.id ? ( + + ) : ( + + N/A + + )} + + · + + + {dayjs.unix(vote.transaction.timestamp).format("MMM D, h:mm a")} + + + + + + + ); +}; + +export default Index; diff --git a/components/Treasury/TreasuryVoteHistoryModal/index.tsx b/components/Treasury/TreasuryVoteHistoryModal/index.tsx new file mode 100644 index 00000000..68dea1b2 --- /dev/null +++ b/components/Treasury/TreasuryVoteHistoryModal/index.tsx @@ -0,0 +1,222 @@ +import { Box, Text } from "@livepeer/design-system"; +import { Cross1Icon } from "@radix-ui/react-icons"; +import React, { useEffect, useRef } from "react"; +import { createPortal } from "react-dom"; + +interface TreasuryVoteHistoryModalProps { + onClose: () => void; + children: React.ReactNode; + title?: string; + header?: React.ReactNode; +} + +const Index: React.FC = ({ + onClose, + children, + title, + header, +}) => { + const modalRef = useRef(null); + + useEffect(() => { + // Disable scroll on mount + const originalStyle = window.getComputedStyle(document.body).overflow; + document.body.style.overflow = "hidden"; + + // Focus management + const focusableElementsSelector = + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const firstFocusableElement = modalRef.current?.querySelectorAll( + focusableElementsSelector + )[0] as HTMLElement; + + if (firstFocusableElement) { + firstFocusableElement.focus(); + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + + if (e.key === "Tab") { + const focusableContent = modalRef.current?.querySelectorAll( + focusableElementsSelector + ); + if (!focusableContent) return; + + const focusableArray = Array.from(focusableContent) as HTMLElement[]; + const firstElement = focusableArray[0]; + const lastElement = focusableArray[focusableArray.length - 1]; + + if (e.shiftKey) { + // if shift key pressed for shift + tab combination + if (document.activeElement === firstElement) { + lastElement.focus(); // add focus for the last focusable element + e.preventDefault(); + } + } else { + // if tab key is pressed + if (document.activeElement === lastElement) { + // if focused has reached to last element then focus again first element + firstElement.focus(); // add focus for the first focusable element + e.preventDefault(); + } + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + document.body.style.overflow = originalStyle; + window.removeEventListener("keydown", handleKeyDown); + }; + }, [onClose]); + + return createPortal( + + e.stopPropagation()} + > + + + {title && ( + + {title} + + )} + + + ESC TO CLOSE + + + + + + + {header && {header}} + + + + {children} + + + , + document.body + ); +}; + +export default Index; diff --git a/components/Treasury/TreasuryVotePopover/index.tsx b/components/Treasury/TreasuryVotePopover/index.tsx new file mode 100644 index 00000000..24bbdd07 --- /dev/null +++ b/components/Treasury/TreasuryVotePopover/index.tsx @@ -0,0 +1,252 @@ +import Spinner from "@components/Spinner"; +import { TREASURY_VOTES } from "@lib/api/types/votes"; +import { Badge, Box, Flex, Link, Text } from "@livepeer/design-system"; +import { ArrowTopRightIcon } from "@radix-ui/react-icons"; +import { + TreasuryVoteEvent, + TreasuryVoteSupport, + useTreasuryVoteEventsQuery, +} from "apollo"; +import React from "react"; + +import TreasuryVoteDetail from "../TreasuryVoteDetail"; +import TreasuryVoteHistoryModal from "../TreasuryVoteHistoryModal"; + +interface TreasuryVotePopoverProps { + voter: string; + ensName?: string; + onClose: () => void; + formatWeight: (weight: string) => string; +} + +const Index: React.FC = ({ + voter, + ensName, + onClose, + formatWeight, +}) => { + const { data: votesData, loading: isLoading } = useTreasuryVoteEventsQuery({ + variables: { + where: { + voter: voter, + }, + }, + }); + + const votes = React.useMemo(() => { + return votesData?.treasuryVoteEvents + ? [...votesData.treasuryVoteEvents].sort( + (a, b) => b.transaction.timestamp - a.transaction.timestamp + ) + : []; + }, [votesData]); + + const stats = React.useMemo(() => { + if (!votes.length) return null; + return { + total: votes.length, + for: votes.filter((v) => v.support === TreasuryVoteSupport.For).length, + against: votes.filter((v) => v.support === TreasuryVoteSupport.Against) + .length, + abstain: votes.filter((v) => v.support === TreasuryVoteSupport.Abstain) + .length, + }; + }, [votes]); + + const summaryHeader = React.useMemo(() => { + return ( + + + + {ensName || voter} + + + {stats && ( + + + Total: + + + {stats.total} + + + )} + + {stats && ( + + + + Total: + + + {stats.total} + + + + + For: {stats.for} + + + + Against: {stats.against} + + + + Abstain: {stats.abstain} + + + )} + + ); + }, [stats, voter, ensName]); + + return ( + + {isLoading ? ( + + + + ) : votes.length > 0 ? ( + + {votes.map((vote, idx) => ( + + + + ))} + + ) : ( + + No votes found for this voter. + + )} + + ); +}; + +export default Index; diff --git a/components/Treasury/TreasuryVoteTable/Views/DesktopVoteTable.tsx b/components/Treasury/TreasuryVoteTable/Views/DesktopVoteTable.tsx new file mode 100644 index 00000000..6d512f79 --- /dev/null +++ b/components/Treasury/TreasuryVoteTable/Views/DesktopVoteTable.tsx @@ -0,0 +1,262 @@ +import EthAddressBadge from "@components/EthAddressBadge"; +import { ExplorerTooltip } from "@components/ExplorerTooltip"; +import DataTable from "@components/Table"; +import TransactionBadge from "@components/TransactionBadge"; +import { VOTING_SUPPORT_MAP } from "@lib/api/types/votes"; +import { Badge, Box, Text } from "@livepeer/design-system"; +import { CounterClockwiseClockIcon } from "@radix-ui/react-icons"; +import { TreasuryVote, TreasuryVoteSupport } from "apollo"; +import React, { useMemo } from "react"; +import { Column } from "react-table"; + +import { VoteReasonPopover } from "./VoteReasonPopover"; + +export type Vote = TreasuryVote & { + ensName?: string; + transactionHash?: string; + timestamp?: number; +}; + +export interface TreasuryVoteTableProps { + votes: Vote[]; + formatWeight: (weight: string) => string; + onSelect: (voter: { address: string; ensName?: string }) => void; + pageSize?: number; + totalPages?: number; + currentPage?: number; + onPageChange?: (page: number) => void; +} + +export const DesktopVoteTable: React.FC = ({ + votes, + formatWeight, + onSelect, + pageSize = 10, +}) => { + const columns = useMemo[]>( + () => [ + { + Header: "Voter", + accessor: "ensName", + id: "voter", + Cell: ({ row }) => ( + + + + ), + }, + { + Header: "Support", + accessor: "support", + id: "support", + Cell: ({ row }) => { + const support = + VOTING_SUPPORT_MAP[row.original.support] || + VOTING_SUPPORT_MAP[TreasuryVoteSupport.Abstain]; + + return ( + + + + {support.text} + + + ); + }, + }, + { + Header: "Weight", + accessor: "weight", + id: "weight", + Cell: ({ row }) => ( + + + {formatWeight(row.original.weight)} + + + ), + sortType: (rowA, rowB) => { + return ( + parseFloat(rowA.original.weight) - parseFloat(rowB.original.weight) + ); + }, + }, + { + Header: "Reason", + accessor: "reason", + id: "reason", + Cell: ({ row }) => { + const reason = row.original.reason?.trim(); + const isEmpty = + !reason || reason.toLowerCase() === "no reason provided"; + + const isLongReason = reason && reason.length > 50; + + return ( + + {!isEmpty ? ( + isLongReason ? ( + + + {reason} + + + ) : ( + + {reason} + + ) + ) : ( + + — + + )} + + ); + }, + }, + { + Header: "Transaction", + accessor: "transactionHash", + id: "transaction", + Cell: ({ row }) => ( + + {row.original.transactionHash ? ( + + ) : ( + + N/A + + )} + + ), + }, + { + Header: "", + id: "history", + Cell: ({ row }) => ( + + + { + e.stopPropagation(); + onSelect({ + address: row.original.voter.id, + ensName: row.original.ensName, + }); + }} + css={{ + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + width: 32, + height: 32, + borderRadius: "50%", + cursor: "pointer", + border: "none", + backgroundColor: "transparent", + color: "$neutral10", + "&:hover": { + color: "$primary11", + backgroundColor: "$primary3", + transform: "rotate(-15deg)", + }, + transition: "color .2s, background-color .2s, transform .2s", + }} + > + + + + + ), + disableSortBy: true, + }, + ], + [formatWeight, onSelect] + ); + + return ( + + + + ); +}; diff --git a/components/Treasury/TreasuryVoteTable/Views/MobileVoteTable.tsx b/components/Treasury/TreasuryVoteTable/Views/MobileVoteTable.tsx new file mode 100644 index 00000000..4572fd40 --- /dev/null +++ b/components/Treasury/TreasuryVoteTable/Views/MobileVoteTable.tsx @@ -0,0 +1,58 @@ +import Pagination from "@components/Table/Pagination"; +import { Box, Text } from "@livepeer/design-system"; +import React from "react"; + +import { TreasuryVoteTableProps } from "./DesktopVoteTable"; +import { VoteView } from "./VoteItem"; + +export const MobileVoteCards: React.FC = (props) => { + const { + votes, + formatWeight, + onSelect, + totalPages = 0, + currentPage = 1, + onPageChange, + } = props; + + return ( + + + View a voter's proposal voting history by clicking the history + icon. + + + {votes.map((vote) => { + return ( + + ); + })} + + {/* Pagination */} + {totalPages > 1 && ( + 1} + canNext={currentPage < totalPages} + onPrevious={() => onPageChange?.(currentPage - 1)} + onNext={() => onPageChange?.(currentPage + 1)} + /> + )} + + ); +}; diff --git a/components/Treasury/TreasuryVoteTable/Views/VoteItem.tsx b/components/Treasury/TreasuryVoteTable/Views/VoteItem.tsx new file mode 100644 index 00000000..f94c508e --- /dev/null +++ b/components/Treasury/TreasuryVoteTable/Views/VoteItem.tsx @@ -0,0 +1,531 @@ +import { ExplorerTooltip } from "@components/ExplorerTooltip"; +import { VOTING_SUPPORT_MAP } from "@lib/api/types/votes"; +import dayjs from "@lib/dayjs"; +import { formatTransactionHash } from "@lib/utils"; +import { + Badge, + Box, + Card, + Flex, + Heading, + Link, + Text, +} from "@livepeer/design-system"; +import { + ArrowTopRightIcon, + ChevronDownIcon, + ChevronUpIcon, + CounterClockwiseClockIcon, +} from "@radix-ui/react-icons"; +import { TreasuryVoteSupport } from "apollo/subgraph"; +import { useState } from "react"; + +import { Vote } from "./DesktopVoteTable"; +import { VoteReasonPopover } from "./VoteReasonPopover"; + +interface VoteViewProps { + vote: Vote; + onSelect: (voter: { address: string; ensName?: string }) => void; + formatWeight: (weight: string) => string; + isMobile?: boolean; +} + +export function VoteView({ + vote, + onSelect, + formatWeight, + isMobile, +}: VoteViewProps) { + return isMobile ? ( + + ) : ( + + ); +} + +function MobileVoteView({ vote, onSelect, formatWeight }: VoteViewProps) { + const [isExpanded, setIsExpanded] = useState(false); + const support = + VOTING_SUPPORT_MAP[vote.support] || + VOTING_SUPPORT_MAP[TreasuryVoteSupport.Abstain]; + const hasReason = + vote.reason && vote.reason.toLowerCase() !== "no reason provided"; + const reasonId = `reason-${vote.transactionHash || vote.voter.id}`; + + return ( + + + {/* Hero: Vote badge */} + + + {support.text} + + + {/* Voter name + History */} + + + + {vote.ensName} + + + + onSelect({ address: vote.voter.id, ensName: vote.ensName }) + } + > + + History + + + + + + {/* Weight */} + + {formatWeight(vote.weight)} + + + {/* Collapsible Reason */} + {hasReason && ( + + setIsExpanded(!isExpanded)} + aria-expanded={isExpanded} + aria-controls={reasonId} + css={{ + display: "flex", + alignItems: "center", + gap: "$1", + color: "$primary11", + cursor: "pointer", + border: "none", + backgroundColor: "transparent", + padding: "$2", + margin: "-$2", + minHeight: "44px", + fontSize: "$1", + fontWeight: 600, + }} + > + + {isExpanded ? "Hide reason" : "Show reason"} + + {isExpanded && ( + + + “{vote.reason}” + + + )} + + )} + + {/* Footer: Transaction + Timestamp */} + + {vote.transactionHash ? ( + *": { + border: "1.5px solid $grass7 !important", + backgroundColor: "$grass3 !important", + color: "$grass11 !important", + }, + "&:focus-visible > *": { + outline: "2px solid $primary11", + outlineOffset: "2px", + }, + }} + > + + {formatTransactionHash(vote.transactionHash)} + + + + ) : ( + + N/A + + )} + {vote.timestamp && ( + <> + + · + + + {dayjs.unix(vote.timestamp).format("MMM D, h:mm a")} + + + )} + + + + ); +} + +function DesktopVoteView({ vote, onSelect, formatWeight }: VoteViewProps) { + const support = + VOTING_SUPPORT_MAP[vote.support] || + VOTING_SUPPORT_MAP[TreasuryVoteSupport.Abstain]; + + return ( + td": { padding: "$2 $3" }, + }} + > + + e.stopPropagation()} + > + + {vote.ensName} + + + + + + + {support.text} + + + + + {formatWeight(vote.weight)} + + + + + {!vote.reason || + vote.reason.toLowerCase() === "no reason provided" ? ( + + — + + ) : (vote.reason?.length ?? 0) > 50 ? ( + + + {vote.reason} + + + ) : ( + + {vote.reason} + + )} + + + + {vote.transactionHash ? ( + e.stopPropagation()} + css={{ + display: "inline-flex", + textDecoration: "none", + transition: "transform 0.2s ease", + "&:hover": { + transform: "scale(1.02)", + textDecoration: "none", + }, + "&:hover > *": { + borderColor: "$grass4 !important", + backgroundColor: "$grass3 !important", + color: "$grass11 !important", + }, + }} + > + + {formatTransactionHash(vote.transactionHash)} + + + + ) : ( + N/A + )} + + + + { + e.stopPropagation(); + onSelect({ address: vote.voter.id, ensName: vote.ensName }); + }} + css={{ + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + width: 32, + height: 32, + borderRadius: "50%", + cursor: "pointer", + border: "none", + backgroundColor: "transparent", + color: "$neutral10", + "&:hover": { + color: "$primary11", + backgroundColor: "$primary3", + transform: "rotate(-15deg)", + }, + "&:focus-visible": { + outline: "2px solid $primary11", + outlineOffset: "2px", + color: "$primary11", + backgroundColor: "$primary3", + }, + transition: "all 0.2s", + }} + > + + + + + + ); +} diff --git a/components/Treasury/TreasuryVoteTable/Views/VoteReasonPopover.tsx b/components/Treasury/TreasuryVoteTable/Views/VoteReasonPopover.tsx new file mode 100644 index 00000000..18e787aa --- /dev/null +++ b/components/Treasury/TreasuryVoteTable/Views/VoteReasonPopover.tsx @@ -0,0 +1,117 @@ +import { + Box, + HoverCardArrow, + HoverCardContent, + HoverCardRoot, + HoverCardTrigger, + styled, + Text, +} from "@livepeer/design-system"; +import { ChatBubbleIcon } from "@radix-ui/react-icons"; +import React from "react"; + +const Content = styled(HoverCardContent, { + width: 320, + padding: "$3", + backgroundColor: "$neutral3", + border: "1px solid $neutral5", + borderRadius: "$3", + boxShadow: + "0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)", + zIndex: 100, + outline: "none", + animationDuration: "400ms", + animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)", + willChange: "transform, opacity", +}); + +interface VoteReasonPopoverProps { + reason: string; + children?: React.ReactNode; +} + +export function VoteReasonPopover({ + reason, + children, +}: VoteReasonPopoverProps) { + if (!reason || reason.toLowerCase() === "no reason provided") { + return null; + } + + return ( + + + {children || ( + + + + Reason + + + )} + + + + + Vote Reason + + + + {reason} + + + + + + + ); +} diff --git a/components/Treasury/TreasuryVoteTable/index.tsx b/components/Treasury/TreasuryVoteTable/index.tsx new file mode 100644 index 00000000..9e554393 --- /dev/null +++ b/components/Treasury/TreasuryVoteTable/index.tsx @@ -0,0 +1,203 @@ +import Spinner from "@components/Spinner"; +import TreasuryVotePopover from "@components/Treasury/TreasuryVotePopover"; +import { getEnsForVotes } from "@lib/api/ens"; +import { formatAddress, lptFormatter } from "@lib/utils"; +import { Flex, Text } from "@livepeer/design-system"; +import { useTreasuryVoteEventsQuery, useTreasuryVotesQuery } from "apollo"; +import React, { useEffect, useMemo, useState } from "react"; +import { useWindowSize } from "react-use"; + +import { DesktopVoteTable, Vote } from "./Views/DesktopVoteTable"; +import { MobileVoteCards } from "./Views/MobileVoteTable"; + +interface TreasuryVoteTableProps { + proposalId: string; +} + +const useVotes = (proposalId: string) => { + const { + data: treasuryVotesData, + loading, + error, + } = useTreasuryVotesQuery({ + variables: { + where: { + proposal: proposalId, + }, + }, + }); + + const { + data: treasuryVoteEventsData, + loading: treasuryVoteEventsLoading, + error: treasuryVoteEventsError, + } = useTreasuryVoteEventsQuery({ + variables: { + first: 200, + where: { + proposal: proposalId, + }, + }, + }); + + const [votes, setVotes] = useState([]); + const [votesLoading, setVotesLoading] = useState(false); + useEffect(() => { + if ( + !treasuryVotesData?.treasuryVotes || + !treasuryVoteEventsData?.treasuryVoteEvents + ) { + setVotes([]); + } + const decorateVotes = async () => { + setVotesLoading(true); + const uniqueVoters = Array.from( + new Set(treasuryVotesData?.treasuryVotes?.map((v) => v.voter.id) ?? []) + ); + const localEnsCache: { [address: string]: string } = {}; + + await Promise.all( + uniqueVoters.map(async (address) => { + try { + if (localEnsCache[address]) { + return; + } + const ensAddress = await getEnsForVotes(address); + + if (ensAddress && ensAddress.name) { + localEnsCache[address] = ensAddress.name; + } else { + localEnsCache[address] = formatAddress(address); + } + } catch (e) { + console.warn(`Failed to fetch ENS for ${address}`, e); + } + }) + ); + const votes = + treasuryVotesData?.treasuryVotes?.map((vote) => { + const events = (treasuryVoteEventsData?.treasuryVoteEvents ?? []) + .filter((event) => event.voter.id === vote.voter.id) + .sort((a, b) => b.timestamp - a.timestamp); + + const latestEvent = events[0]; + const ensName = localEnsCache[vote.voter.id] ?? ""; + + return { + ...vote, + reason: latestEvent?.reason || vote.reason || "", + ensName, + transactionHash: latestEvent?.transaction.id ?? "", + timestamp: latestEvent?.timestamp, + }; + }) ?? []; + setVotes(votes as Vote[]); + setVotesLoading(false); + }; + decorateVotes(); + }, [ + treasuryVotesData?.treasuryVotes, + treasuryVoteEventsData?.treasuryVoteEvents, + ]); + + return { + votes, + loading: loading || votesLoading || treasuryVoteEventsLoading, + error: error || treasuryVoteEventsError, + }; +}; + +const Index: React.FC = ({ proposalId }) => { + const { width } = useWindowSize(); + const isDesktop = width >= 900; + + const [selectedVoter, setSelectedVoter] = useState<{ + address: string; + ensName?: string; + } | null>(null); + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 10; + + const { votes, loading, error } = useVotes(proposalId); + const totalWeight = useMemo( + () => votes.reduce((sum, v) => sum + parseFloat(v.weight), 0), + [votes] + ); + + const formatWeight = useMemo( + () => (w: string) => + `${lptFormatter.format(parseFloat(w))} LPT (${ + totalWeight > 0 ? ((parseFloat(w) / totalWeight) * 100).toFixed(2) : "0" + }%)`, + [totalWeight] + ); + + const paginatedVotesForMobile = useMemo(() => { + const sorted = [...votes].sort( + (a, b) => parseFloat(b.weight) - parseFloat(a.weight) + ); + const startIndex = (currentPage - 1) * pageSize; + return sorted.slice(startIndex, startIndex + pageSize); + }, [votes, currentPage, pageSize]); + + const totalPages = Math.ceil(votes.length / pageSize); + + if (loading) { + return ( + + + + ); + } + if (error) + return ( + + Error loading votes: {error.message} + + ); + + if (!votes.length) + return ( + + No votes found for this proposal. + + ); + + return ( + <> + {isDesktop ? ( + + ) : ( + + )} + {selectedVoter && ( + setSelectedVoter(null)} + formatWeight={formatWeight} + /> + )} + + ); +}; + +export default Index; diff --git a/components/TreasuryVotingReason/index.tsx b/components/Treasury/TreasuryVotingReason/index.tsx similarity index 67% rename from components/TreasuryVotingReason/index.tsx rename to components/Treasury/TreasuryVotingReason/index.tsx index 52724984..67b77998 100644 --- a/components/TreasuryVotingReason/index.tsx +++ b/components/Treasury/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)