diff --git a/.env.example b/.env.example index d79f24a0..521dd385 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,6 @@ NEXT_PUBLIC_SUBGRAPH_ID=FE63YgkzcpVocxdCEyEYbvjYqEf2kb1A6daMYRxmejYC NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID= NEXT_PUBLIC_METRICS_SERVER_URL=https://livepeer-leaderboard-serverless.vercel.app NEXT_PUBLIC_AI_METRICS_SERVER_URL=https://leaderboard-api.livepeer.cloud + +# Optional dev overrides (e.g. Graph Studio sandbox; leave empty in prod) +NEXT_PUBLIC_SUBGRAPH_ENDPOINT= diff --git a/apollo/subgraph.ts b/apollo/subgraph.ts index ca344fe7..ec710b10 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -292,18 +292,144 @@ export enum BondEvent_OrderBy { /** Broadcasters pay transcoders to do the work of transcoding in exchange for fees */ export type Broadcaster = { __typename: 'Broadcaster'; + /** Days in which this broadcaster paid out fees */ + broadcasterDays: Array; /** Amount of funds deposited */ deposit: Scalars['BigDecimal']; + /** The date this broadcaster first funded a deposit or reserve, beginning at 12:00am UTC */ + firstActiveDay: Scalars['Int']; /** ETH address of a broadcaster */ id: Scalars['ID']; + /** The date this broadcaster last paid fees, beginning at 12:00am UTC */ + lastActiveDay: Scalars['Int']; + /** Fees paid out by this broadcaster in ETH during the last 90 days */ + ninetyDayVolumeETH: Scalars['BigDecimal']; /** Amount of funds in reserve */ reserve: Scalars['BigDecimal']; + /** Fees paid out by this broadcaster in ETH during the last 60 days */ + sixtyDayVolumeETH: Scalars['BigDecimal']; + /** Fees paid out by this broadcaster in ETH during the last 30 days */ + thirtyDayVolumeETH: Scalars['BigDecimal']; + /** Total fees paid out by this broadcaster in ETH */ + totalVolumeETH: Scalars['BigDecimal']; + /** Total fees paid out by this broadcaster in USD */ + totalVolumeUSD: Scalars['BigDecimal']; +}; + + +/** Broadcasters pay transcoders to do the work of transcoding in exchange for fees */ +export type BroadcasterBroadcasterDaysArgs = { + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; + skip?: InputMaybe; + where?: InputMaybe; +}; + +/** Broadcaster data accumulated and condensed into day stats */ +export type BroadcasterDay = { + __typename: 'BroadcasterDay'; + /** Broadcaster associated with the day */ + broadcaster: Broadcaster; + /** The date beginning at 12:00am UTC */ + date: Scalars['Int']; + /** Combination of the broadcaster address and the timestamp rounded to the current day by dividing by 86400 */ + id: Scalars['ID']; + /** Fees paid this day in ETH */ + volumeETH: Scalars['BigDecimal']; + /** Fees paid this day in USD */ + volumeUSD: Scalars['BigDecimal']; +}; + +export type BroadcasterDay_Filter = { + /** Filter for the block changed event. */ + _change_block?: InputMaybe; + and?: InputMaybe>>; + broadcaster?: InputMaybe; + broadcaster_?: InputMaybe; + broadcaster_contains?: InputMaybe; + broadcaster_contains_nocase?: InputMaybe; + broadcaster_ends_with?: InputMaybe; + broadcaster_ends_with_nocase?: InputMaybe; + broadcaster_gt?: InputMaybe; + broadcaster_gte?: InputMaybe; + broadcaster_in?: InputMaybe>; + broadcaster_lt?: InputMaybe; + broadcaster_lte?: InputMaybe; + broadcaster_not?: InputMaybe; + broadcaster_not_contains?: InputMaybe; + broadcaster_not_contains_nocase?: InputMaybe; + broadcaster_not_ends_with?: InputMaybe; + broadcaster_not_ends_with_nocase?: InputMaybe; + broadcaster_not_in?: InputMaybe>; + broadcaster_not_starts_with?: InputMaybe; + broadcaster_not_starts_with_nocase?: InputMaybe; + broadcaster_starts_with?: InputMaybe; + broadcaster_starts_with_nocase?: InputMaybe; + date?: InputMaybe; + date_gt?: InputMaybe; + date_gte?: InputMaybe; + date_in?: InputMaybe>; + date_lt?: InputMaybe; + date_lte?: InputMaybe; + date_not?: InputMaybe; + date_not_in?: InputMaybe>; + id?: InputMaybe; + id_gt?: InputMaybe; + id_gte?: InputMaybe; + id_in?: InputMaybe>; + id_lt?: InputMaybe; + id_lte?: InputMaybe; + id_not?: InputMaybe; + id_not_in?: InputMaybe>; + or?: InputMaybe>>; + volumeETH?: InputMaybe; + volumeETH_gt?: InputMaybe; + volumeETH_gte?: InputMaybe; + volumeETH_in?: InputMaybe>; + volumeETH_lt?: InputMaybe; + volumeETH_lte?: InputMaybe; + volumeETH_not?: InputMaybe; + volumeETH_not_in?: InputMaybe>; + volumeUSD?: InputMaybe; + volumeUSD_gt?: InputMaybe; + volumeUSD_gte?: InputMaybe; + volumeUSD_in?: InputMaybe>; + volumeUSD_lt?: InputMaybe; + volumeUSD_lte?: InputMaybe; + volumeUSD_not?: InputMaybe; + volumeUSD_not_in?: InputMaybe>; }; +export enum BroadcasterDay_OrderBy { + Broadcaster = 'broadcaster', + BroadcasterDeposit = 'broadcaster__deposit', + BroadcasterFirstActiveDay = 'broadcaster__firstActiveDay', + BroadcasterId = 'broadcaster__id', + BroadcasterLastActiveDay = 'broadcaster__lastActiveDay', + BroadcasterNinetyDayVolumeEth = 'broadcaster__ninetyDayVolumeETH', + BroadcasterReserve = 'broadcaster__reserve', + BroadcasterSixtyDayVolumeEth = 'broadcaster__sixtyDayVolumeETH', + BroadcasterThirtyDayVolumeEth = 'broadcaster__thirtyDayVolumeETH', + BroadcasterTotalVolumeEth = 'broadcaster__totalVolumeETH', + BroadcasterTotalVolumeUsd = 'broadcaster__totalVolumeUSD', + Date = 'date', + Id = 'id', + VolumeEth = 'volumeETH', + VolumeUsd = 'volumeUSD' +} + export type Broadcaster_Filter = { /** Filter for the block changed event. */ _change_block?: InputMaybe; and?: InputMaybe>>; + broadcasterDays?: InputMaybe>; + broadcasterDays_?: InputMaybe; + broadcasterDays_contains?: InputMaybe>; + broadcasterDays_contains_nocase?: InputMaybe>; + broadcasterDays_not?: InputMaybe>; + broadcasterDays_not_contains?: InputMaybe>; + broadcasterDays_not_contains_nocase?: InputMaybe>; deposit?: InputMaybe; deposit_gt?: InputMaybe; deposit_gte?: InputMaybe; @@ -312,6 +438,14 @@ export type Broadcaster_Filter = { deposit_lte?: InputMaybe; deposit_not?: InputMaybe; deposit_not_in?: InputMaybe>; + firstActiveDay?: InputMaybe; + firstActiveDay_gt?: InputMaybe; + firstActiveDay_gte?: InputMaybe; + firstActiveDay_in?: InputMaybe>; + firstActiveDay_lt?: InputMaybe; + firstActiveDay_lte?: InputMaybe; + firstActiveDay_not?: InputMaybe; + firstActiveDay_not_in?: InputMaybe>; id?: InputMaybe; id_gt?: InputMaybe; id_gte?: InputMaybe; @@ -320,6 +454,22 @@ export type Broadcaster_Filter = { id_lte?: InputMaybe; id_not?: InputMaybe; id_not_in?: InputMaybe>; + lastActiveDay?: InputMaybe; + lastActiveDay_gt?: InputMaybe; + lastActiveDay_gte?: InputMaybe; + lastActiveDay_in?: InputMaybe>; + lastActiveDay_lt?: InputMaybe; + lastActiveDay_lte?: InputMaybe; + lastActiveDay_not?: InputMaybe; + lastActiveDay_not_in?: InputMaybe>; + ninetyDayVolumeETH?: InputMaybe; + ninetyDayVolumeETH_gt?: InputMaybe; + ninetyDayVolumeETH_gte?: InputMaybe; + ninetyDayVolumeETH_in?: InputMaybe>; + ninetyDayVolumeETH_lt?: InputMaybe; + ninetyDayVolumeETH_lte?: InputMaybe; + ninetyDayVolumeETH_not?: InputMaybe; + ninetyDayVolumeETH_not_in?: InputMaybe>; or?: InputMaybe>>; reserve?: InputMaybe; reserve_gt?: InputMaybe; @@ -329,12 +479,52 @@ export type Broadcaster_Filter = { reserve_lte?: InputMaybe; reserve_not?: InputMaybe; reserve_not_in?: InputMaybe>; + sixtyDayVolumeETH?: InputMaybe; + sixtyDayVolumeETH_gt?: InputMaybe; + sixtyDayVolumeETH_gte?: InputMaybe; + sixtyDayVolumeETH_in?: InputMaybe>; + sixtyDayVolumeETH_lt?: InputMaybe; + sixtyDayVolumeETH_lte?: InputMaybe; + sixtyDayVolumeETH_not?: InputMaybe; + sixtyDayVolumeETH_not_in?: InputMaybe>; + thirtyDayVolumeETH?: InputMaybe; + thirtyDayVolumeETH_gt?: InputMaybe; + thirtyDayVolumeETH_gte?: InputMaybe; + thirtyDayVolumeETH_in?: InputMaybe>; + thirtyDayVolumeETH_lt?: InputMaybe; + thirtyDayVolumeETH_lte?: InputMaybe; + thirtyDayVolumeETH_not?: InputMaybe; + thirtyDayVolumeETH_not_in?: InputMaybe>; + totalVolumeETH?: InputMaybe; + totalVolumeETH_gt?: InputMaybe; + totalVolumeETH_gte?: InputMaybe; + totalVolumeETH_in?: InputMaybe>; + totalVolumeETH_lt?: InputMaybe; + totalVolumeETH_lte?: InputMaybe; + totalVolumeETH_not?: InputMaybe; + totalVolumeETH_not_in?: InputMaybe>; + totalVolumeUSD?: InputMaybe; + totalVolumeUSD_gt?: InputMaybe; + totalVolumeUSD_gte?: InputMaybe; + totalVolumeUSD_in?: InputMaybe>; + totalVolumeUSD_lt?: InputMaybe; + totalVolumeUSD_lte?: InputMaybe; + totalVolumeUSD_not?: InputMaybe; + totalVolumeUSD_not_in?: InputMaybe>; }; export enum Broadcaster_OrderBy { + BroadcasterDays = 'broadcasterDays', Deposit = 'deposit', + FirstActiveDay = 'firstActiveDay', Id = 'id', - Reserve = 'reserve' + LastActiveDay = 'lastActiveDay', + NinetyDayVolumeEth = 'ninetyDayVolumeETH', + Reserve = 'reserve', + SixtyDayVolumeEth = 'sixtyDayVolumeETH', + ThirtyDayVolumeEth = 'thirtyDayVolumeETH', + TotalVolumeEth = 'totalVolumeETH', + TotalVolumeUsd = 'totalVolumeUSD' } /** BurnEvent entities are created for every emitted Burn event. */ @@ -930,8 +1120,15 @@ export enum DepositFundedEvent_OrderBy { RoundVolumeUsd = 'round__volumeUSD', Sender = 'sender', SenderDeposit = 'sender__deposit', + SenderFirstActiveDay = 'sender__firstActiveDay', SenderId = 'sender__id', + SenderLastActiveDay = 'sender__lastActiveDay', + SenderNinetyDayVolumeEth = 'sender__ninetyDayVolumeETH', SenderReserve = 'sender__reserve', + SenderSixtyDayVolumeEth = 'sender__sixtyDayVolumeETH', + SenderThirtyDayVolumeEth = 'sender__thirtyDayVolumeETH', + SenderTotalVolumeEth = 'sender__totalVolumeETH', + SenderTotalVolumeUsd = 'sender__totalVolumeUSD', Timestamp = 'timestamp', Transaction = 'transaction', TransactionBlockNumber = 'transaction__blockNumber', @@ -2684,6 +2881,8 @@ export enum Pool_OrderBy { /** Livepeer protocol global parameters */ export type Protocol = { __typename: 'Protocol'; + /** Broadcasters active within the current 90 day fee window */ + activeBroadcasters: Array; /** Total active transcoders (up to the limit) */ activeTranscoderCount: Scalars['BigInt']; /** Current round the protocol is in */ @@ -2761,6 +2960,12 @@ export type ProtocolPendingDeactivationArgs = { export type Protocol_Filter = { /** Filter for the block changed event. */ _change_block?: InputMaybe; + activeBroadcasters?: InputMaybe>; + activeBroadcasters_contains?: InputMaybe>; + activeBroadcasters_contains_nocase?: InputMaybe>; + activeBroadcasters_not?: InputMaybe>; + activeBroadcasters_not_contains?: InputMaybe>; + activeBroadcasters_not_contains_nocase?: InputMaybe>; activeTranscoderCount?: InputMaybe; activeTranscoderCount_gt?: InputMaybe; activeTranscoderCount_gte?: InputMaybe; @@ -3007,6 +3212,7 @@ export type Protocol_Filter = { }; export enum Protocol_OrderBy { + ActiveBroadcasters = 'activeBroadcasters', ActiveTranscoderCount = 'activeTranscoderCount', CurrentRound = 'currentRound', CurrentRoundActiveTranscoderCount = 'currentRound__activeTranscoderCount', @@ -3096,6 +3302,8 @@ export type Query = { bondEvent?: Maybe; bondEvents: Array; broadcaster?: Maybe; + broadcasterDay?: Maybe; + broadcasterDays: Array; broadcasters: Array; burnEvent?: Maybe; burnEvents: Array; @@ -3220,6 +3428,24 @@ export type QueryBroadcasterArgs = { }; +export type QueryBroadcasterDayArgs = { + block?: InputMaybe; + id: Scalars['ID']; + subgraphError?: _SubgraphErrorPolicy_; +}; + + +export type QueryBroadcasterDaysArgs = { + block?: InputMaybe; + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; + skip?: InputMaybe; + subgraphError?: _SubgraphErrorPolicy_; + where?: InputMaybe; +}; + + export type QueryBroadcastersArgs = { block?: InputMaybe; first?: InputMaybe; @@ -4401,8 +4627,15 @@ export enum ReserveClaimedEvent_OrderBy { Id = 'id', ReserveHolder = 'reserveHolder', ReserveHolderDeposit = 'reserveHolder__deposit', + ReserveHolderFirstActiveDay = 'reserveHolder__firstActiveDay', ReserveHolderId = 'reserveHolder__id', + ReserveHolderLastActiveDay = 'reserveHolder__lastActiveDay', + ReserveHolderNinetyDayVolumeEth = 'reserveHolder__ninetyDayVolumeETH', ReserveHolderReserve = 'reserveHolder__reserve', + ReserveHolderSixtyDayVolumeEth = 'reserveHolder__sixtyDayVolumeETH', + ReserveHolderThirtyDayVolumeEth = 'reserveHolder__thirtyDayVolumeETH', + ReserveHolderTotalVolumeEth = 'reserveHolder__totalVolumeETH', + ReserveHolderTotalVolumeUsd = 'reserveHolder__totalVolumeUSD', Round = 'round', RoundActiveTranscoderCount = 'round__activeTranscoderCount', RoundDelegatorsCount = 'round__delegatorsCount', @@ -4549,8 +4782,15 @@ export enum ReserveFundedEvent_OrderBy { Id = 'id', ReserveHolder = 'reserveHolder', ReserveHolderDeposit = 'reserveHolder__deposit', + ReserveHolderFirstActiveDay = 'reserveHolder__firstActiveDay', ReserveHolderId = 'reserveHolder__id', + ReserveHolderLastActiveDay = 'reserveHolder__lastActiveDay', + ReserveHolderNinetyDayVolumeEth = 'reserveHolder__ninetyDayVolumeETH', ReserveHolderReserve = 'reserveHolder__reserve', + ReserveHolderSixtyDayVolumeEth = 'reserveHolder__sixtyDayVolumeETH', + ReserveHolderThirtyDayVolumeEth = 'reserveHolder__thirtyDayVolumeETH', + ReserveHolderTotalVolumeEth = 'reserveHolder__totalVolumeETH', + ReserveHolderTotalVolumeUsd = 'reserveHolder__totalVolumeUSD', Round = 'round', RoundActiveTranscoderCount = 'round__activeTranscoderCount', RoundDelegatorsCount = 'round__delegatorsCount', @@ -8323,8 +8563,15 @@ export enum WinningTicketRedeemedEvent_OrderBy { Sender = 'sender', SenderNonce = 'senderNonce', SenderDeposit = 'sender__deposit', + SenderFirstActiveDay = 'sender__firstActiveDay', SenderId = 'sender__id', + SenderLastActiveDay = 'sender__lastActiveDay', + SenderNinetyDayVolumeEth = 'sender__ninetyDayVolumeETH', SenderReserve = 'sender__reserve', + SenderSixtyDayVolumeEth = 'sender__sixtyDayVolumeETH', + SenderThirtyDayVolumeEth = 'sender__thirtyDayVolumeETH', + SenderTotalVolumeEth = 'sender__totalVolumeETH', + SenderTotalVolumeUsd = 'sender__totalVolumeUSD', Timestamp = 'timestamp', Transaction = 'transaction', TransactionBlockNumber = 'transaction__blockNumber', @@ -8823,8 +9070,15 @@ export enum WithdrawalEvent_OrderBy { RoundVolumeUsd = 'round__volumeUSD', Sender = 'sender', SenderDeposit = 'sender__deposit', + SenderFirstActiveDay = 'sender__firstActiveDay', SenderId = 'sender__id', + SenderLastActiveDay = 'sender__lastActiveDay', + SenderNinetyDayVolumeEth = 'sender__ninetyDayVolumeETH', SenderReserve = 'sender__reserve', + SenderSixtyDayVolumeEth = 'sender__sixtyDayVolumeETH', + SenderThirtyDayVolumeEth = 'sender__thirtyDayVolumeETH', + SenderTotalVolumeEth = 'sender__totalVolumeETH', + SenderTotalVolumeUsd = 'sender__totalVolumeUSD', Timestamp = 'timestamp', Transaction = 'transaction', TransactionBlockNumber = 'transaction__blockNumber', @@ -8876,7 +9130,7 @@ export type AccountQueryVariables = Exact<{ }>; -export type AccountQuery = { __typename: 'Query', delegator?: { __typename: 'Delegator', id: string, bondedAmount: string, principal: string, unbonded: string, withdrawnFees: string, startRound: string, lastClaimRound?: { __typename: 'Round', id: string } | null, unbondingLocks?: Array<{ __typename: 'UnbondingLock', id: string, amount: string, unbondingLockId: number, withdrawRound: string, delegate: { __typename: 'Transcoder', id: string } }> | null, delegate?: { __typename: 'Transcoder', id: string, active: boolean, status: TranscoderStatus, totalStake: string } | null } | null, transcoder?: { __typename: 'Transcoder', id: string, active: boolean, feeShare: string, rewardCut: string, status: TranscoderStatus, totalStake: string, totalVolumeETH: string, activationTimestamp: number, activationRound: string, deactivationRound: string, thirtyDayVolumeETH: string, ninetyDayVolumeETH: string, lastRewardRound?: { __typename: 'Round', id: string } | null, pools?: Array<{ __typename: 'Pool', rewardTokens?: string | null }> | null, delegators?: Array<{ __typename: 'Delegator', id: string }> | null } | null, protocol?: { __typename: 'Protocol', id: string, totalSupply: string, totalActiveStake: string, participationRate: string, inflation: string, inflationChange: string, lptPriceEth: string, roundLength: string, currentRound: { __typename: 'Round', id: string } } | null }; +export type AccountQuery = { __typename: 'Query', delegator?: { __typename: 'Delegator', id: string, bondedAmount: string, principal: string, unbonded: string, withdrawnFees: string, startRound: string, lastClaimRound?: { __typename: 'Round', id: string } | null, unbondingLocks?: Array<{ __typename: 'UnbondingLock', id: string, amount: string, unbondingLockId: number, withdrawRound: string, delegate: { __typename: 'Transcoder', id: string } }> | null, delegate?: { __typename: 'Transcoder', id: string, active: boolean, status: TranscoderStatus, totalStake: string } | null } | null, transcoder?: { __typename: 'Transcoder', id: string, active: boolean, feeShare: string, rewardCut: string, status: TranscoderStatus, totalStake: string, totalVolumeETH: string, activationTimestamp: number, activationRound: string, deactivationRound: string, thirtyDayVolumeETH: string, ninetyDayVolumeETH: string, lastRewardRound?: { __typename: 'Round', id: string } | null, pools?: Array<{ __typename: 'Pool', rewardTokens?: string | null }> | null, delegators?: Array<{ __typename: 'Delegator', id: string }> | null } | null, gateway?: { __typename: 'Broadcaster', id: string, deposit: string, reserve: string, totalVolumeETH: string, ninetyDayVolumeETH: string, firstActiveDay: number, lastActiveDay: number } | null, protocol?: { __typename: 'Protocol', id: string, totalSupply: string, totalActiveStake: string, participationRate: string, inflation: string, inflationChange: string, lptPriceEth: string, roundLength: string, currentRound: { __typename: 'Round', id: string } } | null }; export type AccountInactiveQueryVariables = Exact<{ id: Scalars['ID']; @@ -8906,6 +9160,22 @@ 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: '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 GatewaySelfRedeemQueryVariables = Exact<{ + account: Scalars['String']; +}>; + + +export type GatewaySelfRedeemQuery = { __typename: 'Query', winningTicketRedeemedEvents: Array<{ __typename: 'WinningTicketRedeemedEvent', transaction: { __typename: 'Transaction', timestamp: number } }> }; + +export type GatewaysQueryVariables = Exact<{ + first: Scalars['Int']; + skip: Scalars['Int']; + minActiveDay: Scalars['Int']; +}>; + + +export type GatewaysQuery = { __typename: 'Query', protocol?: { __typename: 'Protocol', id: string, activeBroadcasters: Array } | null, gateways: Array<{ __typename: 'Broadcaster', id: string, deposit: string, reserve: string, totalVolumeETH: string, ninetyDayVolumeETH: string, firstActiveDay: number, lastActiveDay: number }> }; + export type OrchestratorsQueryVariables = Exact<{ currentRound?: InputMaybe; currentRoundString?: InputMaybe; @@ -8955,7 +9225,7 @@ 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: '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: '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, sender: { __typename: 'Broadcaster', id: string }, recipient: { __typename: 'Transcoder', id: 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 }, sender: { __typename: 'Broadcaster', id: string }, recipient: { __typename: 'Transcoder', id: string } }> }; export type TreasuryProposalQueryVariables = Exact<{ id: Scalars['ID']; @@ -9029,6 +9299,15 @@ export const AccountDocument = gql` id } } + gateway: broadcaster(id: $account) { + id + deposit + reserve + totalVolumeETH + ninetyDayVolumeETH + firstActiveDay + lastActiveDay + } protocol(id: "0") { id totalSupply @@ -9403,6 +9682,101 @@ export function useEventsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions; export type EventsLazyQueryHookResult = ReturnType; export type EventsQueryResult = Apollo.QueryResult; +export const GatewaySelfRedeemDocument = gql` + query gatewaySelfRedeem($account: String!) { + winningTicketRedeemedEvents( + first: 1 + orderBy: timestamp + orderDirection: desc + where: {sender: $account, recipient: $account} + ) { + transaction { + timestamp + } + } +} + `; + +/** + * __useGatewaySelfRedeemQuery__ + * + * To run a query within a React component, call `useGatewaySelfRedeemQuery` and pass it any options that fit your needs. + * When your component renders, `useGatewaySelfRedeemQuery` 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 } = useGatewaySelfRedeemQuery({ + * variables: { + * account: // value for 'account' + * }, + * }); + */ +export function useGatewaySelfRedeemQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GatewaySelfRedeemDocument, options); + } +export function useGatewaySelfRedeemLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GatewaySelfRedeemDocument, options); + } +export type GatewaySelfRedeemQueryHookResult = ReturnType; +export type GatewaySelfRedeemLazyQueryHookResult = ReturnType; +export type GatewaySelfRedeemQueryResult = Apollo.QueryResult; +export const GatewaysDocument = gql` + query gateways($first: Int!, $skip: Int!, $minActiveDay: Int!) { + protocol(id: "0") { + id + activeBroadcasters + } + gateways: broadcasters( + first: $first + skip: $skip + orderBy: ninetyDayVolumeETH + orderDirection: desc + where: {or: [{ninetyDayVolumeETH_gt: "0"}, {lastActiveDay_gte: $minActiveDay}]} + ) { + id + deposit + reserve + totalVolumeETH + ninetyDayVolumeETH + firstActiveDay + lastActiveDay + } +} + `; + +/** + * __useGatewaysQuery__ + * + * To run a query within a React component, call `useGatewaysQuery` and pass it any options that fit your needs. + * When your component renders, `useGatewaysQuery` 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 } = useGatewaysQuery({ + * variables: { + * first: // value for 'first' + * skip: // value for 'skip' + * minActiveDay: // value for 'minActiveDay' + * }, + * }); + */ +export function useGatewaysQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GatewaysDocument, options); + } +export function useGatewaysLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GatewaysDocument, options); + } +export type GatewaysQueryHookResult = ReturnType; +export type GatewaysLazyQueryHookResult = ReturnType; +export type GatewaysQueryResult = Apollo.QueryResult; export const OrchestratorsDocument = gql` query orchestrators($currentRound: BigInt, $currentRoundString: String, $where: Transcoder_filter, $first: Int, $skip: Int, $orderBy: Transcoder_orderBy, $orderDirection: OrderDirection) { transcoders( @@ -9762,6 +10136,12 @@ export const TransactionsDocument = gql` } ... on WinningTicketRedeemedEvent { faceValue + sender { + id + } + recipient { + id + } } ... on DepositFundedEvent { sender { @@ -9780,7 +10160,7 @@ export const TransactionsDocument = gql` winningTicketRedeemedEvents( orderBy: timestamp orderDirection: desc - where: {recipient: $account} + where: {or: [{recipient: $account}, {sender: $account}]} ) { __typename id @@ -9792,6 +10172,12 @@ export const TransactionsDocument = gql` timestamp } faceValue + sender { + id + } + recipient { + id + } } } `; diff --git a/codegen.yml b/codegen.yml index 143c8c99..817ff9e0 100644 --- a/codegen.yml +++ b/codegen.yml @@ -1,5 +1,6 @@ overwrite: true -schema: https://gateway.thegraph.com/api/${NEXT_PUBLIC_SUBGRAPH_API_KEY}/subgraphs/id/${NEXT_PUBLIC_SUBGRAPH_ID} +# schema: https://gateway.thegraph.com/api/${NEXT_PUBLIC_SUBGRAPH_API_KEY}/subgraphs/id/${NEXT_PUBLIC_SUBGRAPH_ID} +schema: https://api.studio.thegraph.com/query/31909/livepeer-ci/pr-168-b602361-19536247588 # TODO: Merge https://github.com/livepeer/subgraph/pull/168 upstream first and then update this to real graph. documents: ./queries/**/*.graphql generates: ./apollo/subgraph.ts: diff --git a/components/BroadcastingView/index.tsx b/components/BroadcastingView/index.tsx new file mode 100644 index 00000000..ff4ba506 --- /dev/null +++ b/components/BroadcastingView/index.tsx @@ -0,0 +1,131 @@ +import { ExplorerTooltip } from "@components/ExplorerTooltip"; +import Stat from "@components/Stat"; +import dayjs from "@lib/dayjs"; +import { Box, Grid } from "@livepeer/design-system"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { GatewaysQuery } from "apollo"; +import { useGatewaySelfRedeemStatus } from "hooks"; +import numbro from "numbro"; +import { useMemo } from "react"; + +// TODO: replace with common formatting util. +const formatEth = (value?: string | number | null) => { + const amount = Number(value ?? 0) || 0; + return `${numbro(amount).format( + amount > 0 && amount < 0.01 + ? { mantissa: 4, trimMantissa: true } + : { mantissa: 2, average: true, lowPrecision: false } + )} ETH`; +}; + +const SelfRedeemIndicator = () => ( + Self-redeemed winning tickets detected in the last 90 days. + } + > + + +); + +const BroadcastingView = ({ + gateway, +}: { + gateway?: GatewaysQuery["gateways"] extends Array ? G | null : null; +}) => { + const isSelfRedeeming = useGatewaySelfRedeemStatus(gateway?.id); + + const activeSince = useMemo( + () => + gateway?.firstActiveDay + ? dayjs.unix(gateway.firstActiveDay).format("MMM D, YYYY") + : "Pending graph update", + [gateway] + ); + const statusText = useMemo(() => { + const active = Number(gateway?.ninetyDayVolumeETH ?? 0) > 0; + return active ? "Active" : "Inactive"; + }, [gateway]); + const statItems = useMemo( + () => [ + { + id: "total-fees-distributed", + label: "Total fees distributed", + value: ( + + {formatEth(gateway?.totalVolumeETH)} + {isSelfRedeeming && } + + ), + tooltip: "Lifetime fees this gateway has distributed on-chain.", + }, + { + id: "status", + label: "Status", + value: statusText, + tooltip: "Active if this gateway distributed fees in the last 90 days.", + }, + { + id: "ninety-day-fees", + label: "90d fees distributed", + value: formatEth(gateway?.ninetyDayVolumeETH), + tooltip: + "Total fees distributed to orchestrators over the last 90 days.", + }, + { + id: "activation-date", + label: "Activation date", + value: activeSince, + tooltip: "First day this gateway funded deposit/reserve on-chain.", + }, + { + id: "deposit-balance", + label: "Deposit balance", + value: formatEth(gateway?.deposit), + tooltip: "Current deposit balance funded for gateway job payouts.", + }, + { + id: "reserve-balance", + label: "Reserve balance", + value: formatEth(gateway?.reserve), + tooltip: "Reserve funds available for winning ticket payouts.", + }, + ], + [gateway, activeSince, statusText, isSelfRedeeming] + ); + + return ( + + + {statItems.map((stat) => ( + + ))} + + + ); +}; + +export default BroadcastingView; diff --git a/components/GatewayList/index.tsx b/components/GatewayList/index.tsx new file mode 100644 index 00000000..4f4ec6f1 --- /dev/null +++ b/components/GatewayList/index.tsx @@ -0,0 +1,346 @@ +import { ExplorerTooltip } from "@components/ExplorerTooltip"; +import IdentityAvatar from "@components/IdentityAvatar"; +import PopoverLink from "@components/PopoverLink"; +import Table from "@components/Table"; +import { textTruncate } from "@lib/utils"; +import { + Badge, + Box, + Flex, + IconButton, + Link as A, + Popover, + PopoverContent, + PopoverTrigger, + Text, +} from "@livepeer/design-system"; +import { DotsHorizontalIcon } from "@radix-ui/react-icons"; +import { GatewaysQuery } from "apollo"; +import { useEnsData } from "hooks"; +import Link from "next/link"; +import numbro from "numbro"; +import { useMemo } from "react"; +import { Column } from "react-table"; + +type GatewayRow = NonNullable[number] & { + depositNumber: number; + reserveNumber: number; + ninetyDayVolumeNumber: number; + totalVolumeNumber: number; + lastActiveDayNumber: number; +}; + +// TODO: replace with common formatting util. +const formatEth = (value: number) => { + const amount = Number(value ?? 0) || 0; + return `${numbro(amount).format( + amount > 0 && amount < 0.01 + ? { mantissa: 4, trimMantissa: true } + : { mantissa: 2, average: true, lowPrecision: false } + )} ETH`; +}; + +const GatewayList = ({ + data, + pageSize = 10, +}: { + pageSize?: number; + data: GatewaysQuery["gateways"] | undefined; +}) => { + const rows: GatewayRow[] = useMemo( + () => + (data ?? []).map((gateway) => ({ + ...gateway, + depositNumber: Number(gateway?.deposit ?? 0), + reserveNumber: Number(gateway?.reserve ?? 0), + ninetyDayVolumeNumber: Number(gateway?.ninetyDayVolumeETH ?? 0), + totalVolumeNumber: Number(gateway?.totalVolumeETH ?? 0), + lastActiveDayNumber: Number(gateway?.lastActiveDay ?? 0), + })), + [data] + ); + + const columns: Column[] = useMemo( + () => [ + { + Header: ( + + The account routing jobs to orchestrators and paying fees. + + } + > + Gateway + + ), + id: "gateway", + accessor: "id", + Cell: ({ row, value }) => { + const address = value as string; + const identity = useEnsData(address); + const ensName = identity?.name; + const shortAddress = address.replace(address.slice(6, 38), "…"); + + return ( + + + + {row.index + 1} + + + + {ensName ? ( + + + {textTruncate(ensName, 20, "…")} + + + {address.substring(0, 6)} + + + ) : ( + {shortAddress} + )} + + + + ); + }, + }, + { + Header: ( + Current deposit balance funded for payouts.} + > + Deposit + + ), + id: "depositNumber", + accessor: "depositNumber", + Cell: ({ value }) => ( + + {formatEth(Number(value ?? 0))} + + ), + }, + { + Header: ( + Reserve funds available for winning tickets.} + > + Reserve + + ), + id: "reserveNumber", + accessor: "reserveNumber", + Cell: ({ value }) => ( + + {formatEth(Number(value ?? 0))} + + ), + }, + { + Header: ( + Fees distributed over the last 90 days.} + > + 90d Fees + + ), + id: "ninetyDayVolumeNumber", + accessor: "ninetyDayVolumeNumber", + Cell: ({ value }) => ( + + {formatEth(Number(value ?? 0))} + + ), + sortType: "number", + }, + { + Header: ( + Lifetime fees distributed on-chain.} + > + Total Fees + + ), + id: "totalVolumeNumber", + accessor: "totalVolumeNumber", + Cell: ({ value }) => ( + + {formatEth(Number(value ?? 0))} + + ), + sortType: "number", + }, + { + Header: <>, + id: "actions", + Cell: ({ row }) => ( + + { + e.stopPropagation(); + }} + asChild + > + + + + + + + { + e.stopPropagation(); + }} + onPointerEnterCapture={undefined} + onPointerLeaveCapture={undefined} + placeholder={undefined} + > + + + Account Details + + + + Profile + + + History + + + + + ), + }, + ], + [] + ); + + if (!rows?.length) { + return ( + + No gateways found. + + ); + } + + return ( + + ); +}; + +export default GatewayList; diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index 82e37c65..146e3d67 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -55,20 +55,39 @@ const Index = () => { [events] ); + // Tag winning tickets (in/out/self) within the current window + const ticketEvents = useMemo(() => { + const tickets = + data?.winningTicketRedeemedEvents?.filter( + (e) => (e?.transaction?.timestamp ?? 0) > lastEventTimestamp + ) ?? []; + const accountLower = account.toLowerCase(); + return tickets + .filter((e) => (e?.transaction?.timestamp ?? 0) > lastEventTimestamp) + .map((e) => ({ + ...e, + direction: + e?.sender?.id?.toLowerCase() === accountLower && + e?.recipient?.id?.toLowerCase() === accountLower + ? ("self" as const) + : e?.sender?.id?.toLowerCase() === accountLower + ? ("out" as const) + : ("in" as const), + })); + }, [data?.winningTicketRedeemedEvents, account, lastEventTimestamp]); + // 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"), - ...(data?.winningTicketRedeemedEvents?.filter( - (e) => (e?.transaction?.timestamp ?? 0) > lastEventTimestamp - ) ?? []), + ...ticketEvents, ].sort( (a, b) => (b?.transaction?.timestamp ?? 0) - (a?.transaction?.timestamp ?? 0) ), - [events, data, lastEventTimestamp] + [events, ticketEvents] ); if (error) { @@ -676,7 +695,16 @@ function renderSwitch(event, i: number) { ); - case "WinningTicketRedeemedEvent": + case "WinningTicketRedeemedEvent": { + const direction = event.direction; + const title = + direction === "out" + ? "Paid winning ticket" + : direction === "self" + ? "Self-redeemed winning ticket" + : "Redeemed winning ticket"; + const amountPrefix = + direction === "out" ? "-" : direction === "self" ? "±" : "+"; return ( - Redeemed winning ticket + {title} @@ -728,7 +756,7 @@ function renderSwitch(event, i: number) { {" "} - + + {amountPrefix} {numbro(event.faceValue).format({ mantissa: 3, average: true, @@ -739,6 +767,7 @@ function renderSwitch(event, i: number) { ); + } case "DepositFundedEvent": return ( (({ children, ariaLabel, role }, ref) => { + const innerRef = useRef(null); + const [hasOverflow, setHasOverflow] = useState(false); + + useLayoutEffect(() => { + const el = innerRef.current; + if (!el) return; + const updateState = () => { + const scrollBuffer = 1; + const overflow = el.scrollWidth > el.clientWidth + scrollBuffer; + const maxVisibleRight = el.scrollLeft + el.clientWidth; + setHasOverflow( + overflow && maxVisibleRight < el.scrollWidth - scrollBuffer + ); + }; + updateState(); + const observer = new ResizeObserver(updateState); + observer.observe(el); + const handleScroll = () => updateState(); + el.addEventListener("scroll", handleScroll, { passive: true }); + return () => { + el.removeEventListener("scroll", handleScroll); + observer.disconnect(); + }; + }, [children]); + + useLayoutEffect(() => { + const el = innerRef.current; + if (!el) return; + const activeTab = el.querySelector( + '[data-active="true"]' + ) as HTMLElement | null; + activeTab?.scrollIntoView({ block: "nearest", inline: "nearest" }); + }, [children]); + + const setRefs = (node: HTMLDivElement | null) => { + innerRef.current = node; + if (typeof ref === "function") { + ref(node); + } else if (ref) { + ref.current = node; + } + }; + + return ( + + + {children} + + + + ); +}); + +HorizontalScrollContainer.displayName = "HorizontalScrollContainer"; + +export default HorizontalScrollContainer; diff --git a/hooks/index.tsx b/hooks/index.tsx index 9d37f82c..e6a5f089 100644 --- a/hooks/index.tsx +++ b/hooks/index.tsx @@ -2,6 +2,7 @@ import { useEffect } from "react"; // DO NOT IMPORT useHandleTransaction due to @rainbow-me/rainbowkit issues with SSR export * from "./useExplorerStore"; +export * from "./useGatewaySelfRedeemStatus"; export * from "./useSwr"; export * from "./wallet"; diff --git a/hooks/useGatewaySelfRedeemStatus.ts b/hooks/useGatewaySelfRedeemStatus.ts new file mode 100644 index 00000000..127715c3 --- /dev/null +++ b/hooks/useGatewaySelfRedeemStatus.ts @@ -0,0 +1,20 @@ +import dayjs from "@lib/dayjs"; +import { useGatewaySelfRedeemQuery } from "apollo"; + +export const useGatewaySelfRedeemStatus = ( + gatewayId?: string | null, + windowDays = 90 +): boolean => { + const account = gatewayId?.toLowerCase(); + const { data } = useGatewaySelfRedeemQuery({ + variables: { account: account ?? "" }, + skip: !account, + }); + + const cutoff = dayjs().subtract(windowDays, "day").unix(); + const lastTimestamp = Number( + data?.winningTicketRedeemedEvents?.[0]?.transaction?.timestamp ?? 0 + ); + + return lastTimestamp >= cutoff; +}; diff --git a/layouts/account.tsx b/layouts/account.tsx index 508b8f29..3d889f54 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -1,7 +1,9 @@ import BottomDrawer from "@components/BottomDrawer"; +import BroadcastingView from "@components/BroadcastingView"; import DelegatingView from "@components/DelegatingView"; import DelegatingWidget from "@components/DelegatingWidget"; import HistoryView from "@components/HistoryView"; +import HorizontalScrollContainer from "@components/HorizontalScrollContainer"; import OrchestratingView from "@components/OrchestratingView"; import Profile from "@components/Profile"; import Spinner from "@components/Spinner"; @@ -11,7 +13,6 @@ import { bondingManager } from "@lib/api/abis/main/BondingManager"; import { getAccount, getSortedOrchestrators } from "@lib/api/ssr"; import { checkAddressEquality } from "@lib/utils"; import { - Box, Button, Container, Flex, @@ -43,9 +44,14 @@ export interface TabType { isActive?: boolean; } -type TabTypeEnum = "delegating" | "orchestrating" | "history"; +type TabTypeEnum = "delegating" | "orchestrating" | "history" | "broadcasting"; -const ACCOUNT_VIEWS: TabTypeEnum[] = ["delegating", "orchestrating", "history"]; +const ACCOUNT_VIEWS: TabTypeEnum[] = [ + "delegating", + "orchestrating", + "broadcasting", + "history", +]; const AccountLayout = () => { /* PART OF https://github.com/livepeer/explorer/pull/427 - TODO: REMOVE ONCE SERVER-SIDE ISSUE IS FIXED */ @@ -140,6 +146,7 @@ const AccountLayout = () => { [accountAddress, accountId] ); const isOrchestrator = useMemo(() => Boolean(account?.transcoder), [account]); + const isGateway = useMemo(() => Boolean(account?.gateway), [account]); const isMyDelegate = useMemo( () => accountId === dataMyAccount?.delegator?.delegate?.id.toLowerCase(), [accountId, dataMyAccount] @@ -158,9 +165,20 @@ const AccountLayout = () => { isOrchestrator, accountId ?? "", view ?? "delegating", - isMyDelegate + isMyDelegate, + isGateway, + isMyAccount, + Boolean(account?.delegator) ), - [isOrchestrator, accountId, view, isMyDelegate] + [ + isOrchestrator, + accountId, + view, + isMyDelegate, + isGateway, + isMyAccount, + account?.delegator, + ] ); useEffect(() => { @@ -323,15 +341,9 @@ const AccountLayout = () => { ))} - {tabs.map((tab: TabType, i: number) => ( { href={tab.href} passHref variant="subtle" + data-active={tab.isActive ? "true" : undefined} + aria-current={tab.isActive ? "page" : undefined} css={{ color: tab.isActive ? "$hiContrast" : "$neutral11", marginRight: "$4", paddingBottom: "$2", fontSize: "$3", fontWeight: 500, + flex: "0 0 auto", + whiteSpace: "nowrap", borderBottom: "2px solid", borderColor: tab.isActive ? "$primary11" : "transparent", "&:hover": { @@ -357,7 +373,7 @@ const AccountLayout = () => { {tab.name} ))} - + {view === "orchestrating" && ( { /> )} {view === "history" && } + {view === "broadcasting" && ( + + )} {(isOrchestrator || isMyDelegate || isDelegatingAndIsMyAccountView) && (width > 1020 ? ( @@ -434,27 +453,38 @@ function getTabs( isOrchestrator: boolean, account: string, view: TabTypeEnum, - isMyDelegate: boolean + isMyDelegate: boolean, + hasGateway: boolean, + isMyAccount: boolean, + hasDelegator: boolean ): Array { - const tabs: Array = [ - { - name: "Delegating", - href: `/accounts/${account}/delegating`, - isActive: view === "delegating", - }, - { - name: "History", - href: `/accounts/${account}/history`, - isActive: view === "history", - }, - ]; + const tabs: Array = []; if (isOrchestrator || isMyDelegate) { - tabs.unshift({ + tabs.push({ name: "Orchestrating", href: `/accounts/${account}/orchestrating`, isActive: view === "orchestrating", }); } + if (hasGateway) { + tabs.push({ + name: "Broadcasting", + href: `/accounts/${account}/broadcasting`, + isActive: view === "broadcasting", + }); + } + if (isMyAccount || hasDelegator) { + tabs.push({ + name: "Delegating", + href: `/accounts/${account}/delegating`, + isActive: view === "delegating", + }); + } + tabs.push({ + name: "History", + href: `/accounts/${account}/history`, + isActive: view === "history", + }); return tabs; } diff --git a/layouts/main.tsx b/layouts/main.tsx index 24ed02b0..226283a7 100644 --- a/layouts/main.tsx +++ b/layouts/main.tsx @@ -60,6 +60,7 @@ import React, { } from "react"; import { isMobile } from "react-device-detect"; import ReactGA from "react-ga"; +import { LuRadioTower } from "react-icons/lu"; import { useWindowSize } from "react-use"; import { Chain } from "viem"; @@ -125,6 +126,14 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { const currentRound = useCurrentRoundData(); const pendingFeesAndStake = usePendingFeesAndStakeData(accountAddress); const isBannerDisabledByQuery = query.disableUrlVerificationBanner === "true"; + const isOrchestratorsNavActive = + (!accountAddress || !asPath.includes(accountAddress)) && + (asPath.includes("/orchestrators") || + asPath.includes("/orchestrating") || + asPath.includes("/delegating")); + const isGatewaysNavActive = + (!accountAddress || !asPath.includes(accountAddress)) && + (asPath.includes("/gateways") || asPath.includes("/broadcasting")); const totalActivePolls = useMemo( () => @@ -230,6 +239,13 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { icon: DNS, className: "orchestrators", }, + { + name: "Gateways", + href: "/gateways", + as: "/gateways", + icon: LuRadioTower, + className: "gateways", + }, { name: ( @@ -447,13 +463,9 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { size="3" css={{ marginLeft: "$2", - backgroundColor: - (!accountAddress || - !asPath.includes(accountAddress)) && - (asPath.includes("/accounts") || - asPath.includes("/orchestrators")) - ? "hsla(0,100%,100%,.05)" - : "transparent", + backgroundColor: isOrchestratorsNavActive + ? "hsla(0,100%,100%,.05)" + : "transparent", color: "white", "&:hover": { backgroundColor: "hsla(0,100%,100%,.1)", @@ -469,6 +481,29 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { Orchestrators + + + + + + + {!gateways?.gateways ? ( + + + + ) : ( + + + + )} + { const errorProps: PageProps = { hadError: true, orchestrators: null, + gateways: null, events: null, protocol: null, fallback: {}, @@ -493,8 +551,14 @@ export const getStaticProps = async () => { const { orchestrators } = await getOrchestrators(client); const { events } = await getEvents(client); const protocol = await getProtocol(client); + const { gateways } = await getGateways(); - if (!orchestrators.data || !events.data || !protocol.data) { + if ( + !orchestrators.data || + !events.data || + !protocol.data || + !gateways.data + ) { return { props: errorProps, revalidate: 60, @@ -504,6 +568,7 @@ export const getStaticProps = async () => { const props: PageProps = { hadError: false, orchestrators: orchestrators.data, + gateways: gateways.data, events: events.data, protocol: protocol.data, fallback: {}, diff --git a/queries/account.graphql b/queries/account.graphql index 39104fd2..010b70fb 100644 --- a/queries/account.graphql +++ b/queries/account.graphql @@ -49,6 +49,15 @@ query account($account: ID!) { id } } + gateway: broadcaster(id: $account) { + id + deposit + reserve + totalVolumeETH + ninetyDayVolumeETH + firstActiveDay + lastActiveDay + } protocol(id: "0") { id totalSupply diff --git a/queries/gatewaySelfRedeem.graphql b/queries/gatewaySelfRedeem.graphql new file mode 100644 index 00000000..3add14c6 --- /dev/null +++ b/queries/gatewaySelfRedeem.graphql @@ -0,0 +1,12 @@ +query gatewaySelfRedeem($account: String!) { + winningTicketRedeemedEvents( + first: 1 + orderBy: timestamp + orderDirection: desc + where: { sender: $account, recipient: $account } + ) { + transaction { + timestamp + } + } +} diff --git a/queries/gateways.graphql b/queries/gateways.graphql new file mode 100644 index 00000000..5cb3f44d --- /dev/null +++ b/queries/gateways.graphql @@ -0,0 +1,23 @@ +query gateways($first: Int!, $skip: Int!, $minActiveDay: Int!) { + protocol(id: "0") { + id + activeBroadcasters + } + gateways: broadcasters( + first: $first + skip: $skip + orderBy: ninetyDayVolumeETH + orderDirection: desc + where: { + or: [{ ninetyDayVolumeETH_gt: "0" }, { lastActiveDay_gte: $minActiveDay }] + } + ) { + id + deposit + reserve + totalVolumeETH + ninetyDayVolumeETH + firstActiveDay + lastActiveDay + } +} diff --git a/queries/transactions.graphql b/queries/transactions.graphql index aa976a98..07f82692 100644 --- a/queries/transactions.graphql +++ b/queries/transactions.graphql @@ -54,6 +54,12 @@ query transactions($account: String!, $first: Int!, $skip: Int!) { } ... on WinningTicketRedeemedEvent { faceValue + sender { + id + } + recipient { + id + } } ... on DepositFundedEvent { sender { @@ -72,7 +78,7 @@ query transactions($account: String!, $first: Int!, $skip: Int!) { winningTicketRedeemedEvents( orderBy: timestamp orderDirection: desc - where: { recipient: $account } + where: { or: [{ recipient: $account }, { sender: $account }] } ) { __typename id @@ -84,5 +90,11 @@ query transactions($account: String!, $first: Int!, $skip: Int!) { timestamp } faceValue + sender { + id + } + recipient { + id + } } }