From 671d3c78812a06da6f6015ee5accb3a8a5144d2e Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sat, 22 Nov 2025 13:11:04 +0100 Subject: [PATCH 01/20] feat(gateways): add gateway stats widgets and new gateway pages Introduce new gateway widgets and pages to display stats for active gateways that have distributed fees in the last 90 days. Adds a gateways widget to the main page, a dedicated gateway tab, and a gateway account page for detailed gateway stats and activity. --- .env.example | 4 + apollo/subgraph.ts | 1505 ++++++--------------- codegen.yml | 13 +- components/GatewayList/index.tsx | 349 +++++ components/GatewayProfileView/index.tsx | 114 ++ components/PerformanceList/index.tsx | 1 - layouts/account.tsx | 54 +- layouts/main.tsx | 34 + lib/api/ssr.ts | 22 + lib/chains.ts | 15 +- pages/accounts/[account]/broadcasting.tsx | 81 ++ pages/gateways.tsx | 75 + pages/index.tsx | 62 +- queries/account.graphql | 9 + queries/gateways.graphql | 26 + 15 files changed, 1262 insertions(+), 1102 deletions(-) create mode 100644 components/GatewayList/index.tsx create mode 100644 components/GatewayProfileView/index.tsx create mode 100644 pages/accounts/[account]/broadcasting.tsx create mode 100644 pages/gateways.tsx create mode 100644 queries/gateways.graphql diff --git a/.env.example b/.env.example index d79f24a0..e3ea91e8 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,10 @@ NEXT_PUBLIC_L2_RPC_URL= NEXT_PUBLIC_GITHUB_LIP_NAMESPACE=adamsoffer NEXT_PUBLIC_SUBGRAPH_API_KEY= NEXT_PUBLIC_SUBGRAPH_ID=FE63YgkzcpVocxdCEyEYbvjYqEf2kb1A6daMYRxmejYC +NEXT_PUBLIC_SUBGRAPH_ENDPOINT= 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 565a56df..f8fdb04b 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -1,3 +1,10 @@ +/* eslint-disable */ +/** + * @generated + * This file was automatically generated and should not be edited. + * To add new queries, mutations, or subscriptions, create or update files in the queries folder. + */ + import { gql } from "@apollo/client"; import * as Apollo from "@apollo/client"; export type Maybe = T | null; @@ -41,10 +48,7 @@ export type Block_Height = { number_gte?: InputMaybe; }; -/** - * BondEvent entities are created for every emitted Bond event. - * - */ +/** BondEvent entities are created for every emitted Bond event. */ export type BondEvent = Event & { __typename: "BondEvent"; /** Additional amount added to bonded amount */ @@ -292,24 +296,146 @@ export enum BondEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * Broadcasters pay transcoders to do the work of transcoding in exchange for fees - * - */ +/** 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; @@ -318,6 +444,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; @@ -326,6 +460,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; @@ -335,18 +485,55 @@ 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", + LastActiveDay = "lastActiveDay", + NinetyDayVolumeEth = "ninetyDayVolumeETH", Reserve = "reserve", + SixtyDayVolumeEth = "sixtyDayVolumeETH", + ThirtyDayVolumeEth = "thirtyDayVolumeETH", + TotalVolumeEth = "totalVolumeETH", + TotalVolumeUsd = "totalVolumeUSD", } -/** - * BurnEvent entities are created for every emitted Burn event. - * - */ +/** BurnEvent entities are created for every emitted Burn event. */ export type BurnEvent = Event & { __typename: "BurnEvent"; /** Ethereum transaction hash + event log index */ @@ -467,10 +654,7 @@ export enum BurnEvent_OrderBy { Value = "value", } -/** - * Protocol data accumulated and condensed into day stats - * - */ +/** Protocol data accumulated and condensed into day stats */ export type Day = { __typename: "Day"; /** Total active transcoders (up to the limit) */ @@ -606,10 +790,7 @@ export enum Day_OrderBy { VolumeUsd = "volumeUSD", } -/** - * Bonded accounts who have delegated their stake towards a transcoder candidate - * - */ +/** Bonded accounts who have delegated their stake towards a transcoder candidate */ export type Delegator = { __typename: "Delegator"; /** Amount of Livepeer Token a delegator currently has bonded */ @@ -636,10 +817,7 @@ export type Delegator = { withdrawnFees: Scalars["BigDecimal"]; }; -/** - * Bonded accounts who have delegated their stake towards a transcoder candidate - * - */ +/** Bonded accounts who have delegated their stake towards a transcoder candidate */ export type DelegatorUnbondingLocksArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -812,10 +990,7 @@ export enum Delegator_OrderBy { WithdrawnFees = "withdrawnFees", } -/** - * DepositFundedEvent entities are created for every emitted DepositFunded event. - * - */ +/** DepositFundedEvent entities are created for every emitted DepositFunded event. */ export type DepositFundedEvent = Event & { __typename: "DepositFundedEvent"; /** Amount of broadcasting fees deposited */ @@ -950,8 +1125,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", @@ -963,10 +1145,7 @@ export enum DepositFundedEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * EarningsClaimedEvent entities are created for every emitted EarningsClaimed event. - * - */ +/** EarningsClaimedEvent entities are created for every emitted EarningsClaimed event. */ export type EarningsClaimedEvent = Event & { __typename: "EarningsClaimedEvent"; /** Reference to the delegator's delegate */ @@ -1329,10 +1508,7 @@ export enum Event_OrderBy { TransactionTo = "transaction__to", } -/** - * Abstraction for accounts/delegators bonded with the protocol - * - */ +/** Abstraction for accounts/delegators bonded with the protocol */ export type LivepeerAccount = { __typename: "LivepeerAccount"; /** Reference to the Delegate this address is bonded to */ @@ -1443,10 +1619,7 @@ export enum LivepeerAccount_OrderBy { LastUpdatedTimestamp = "lastUpdatedTimestamp", } -/** - * MigrateDelegatorFinalizedEvent entities are created for every emitted WithdrawStake event. - * - */ +/** MigrateDelegatorFinalizedEvent entities are created for every emitted WithdrawStake event. */ export type MigrateDelegatorFinalizedEvent = Event & { __typename: "MigrateDelegatorFinalizedEvent"; delegate: Scalars["String"]; @@ -1652,10 +1825,7 @@ export enum MigrateDelegatorFinalizedEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * MintEvent entities are created for every emitted Mint event. - * - */ +/** MintEvent entities are created for every emitted Mint event. */ export type MintEvent = Event & { __typename: "MintEvent"; /** Amount of tokens minted */ @@ -1799,10 +1969,7 @@ export enum MintEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * NewRoundEvent entities are created for every emitted NewRound event. - * - */ +/** NewRoundEvent entities are created for every emitted NewRound event. */ export type NewRoundEvent = Event & { __typename: "NewRoundEvent"; /** Block hash for the round */ @@ -1941,10 +2108,7 @@ export enum OrderDirection { Desc = "desc", } -/** - * ParameterUpdateEvent entities are created for every emitted ParameterUpdate event. - * - */ +/** ParameterUpdateEvent entities are created for every emitted ParameterUpdate event. */ export type ParameterUpdateEvent = Event & { __typename: "ParameterUpdateEvent"; /** Ethereum transaction hash + event log index */ @@ -2077,10 +2241,7 @@ export enum ParameterUpdateEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * PauseEvent entities are created for every emitted Pause event. - * - */ +/** PauseEvent entities are created for every emitted Pause event. */ export type PauseEvent = Event & { __typename: "PauseEvent"; /** Ethereum transaction hash + event log index */ @@ -2190,10 +2351,7 @@ export enum PauseEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * Stake weighted poll - * - */ +/** Stake weighted poll */ export type Poll = { __typename: "Poll"; /** Block at which the poll ends and votes can no longer be submitted */ @@ -2212,10 +2370,7 @@ export type Poll = { votes: Array; }; -/** - * Stake weighted poll - * - */ +/** Stake weighted poll */ export type PollVotesArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -2229,10 +2384,7 @@ export enum PollChoice { Yes = "Yes", } -/** - * PollCreatedEvent entities are created for every emitted PollCreated event. - * - */ +/** PollCreatedEvent entities are created for every emitted PollCreated event. */ export type PollCreatedEvent = Event & { __typename: "PollCreatedEvent"; /** Ethereum block in which this poll ends */ @@ -2417,10 +2569,7 @@ export enum PollCreatedEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * Stake weighted tally associated with a poll - * - */ +/** Stake weighted tally associated with a poll */ export type PollTally = { __typename: "PollTally"; /** Poll address */ @@ -2568,10 +2717,7 @@ export enum Poll_OrderBy { Votes = "votes", } -/** - * Represents a transcoder's rewards and fees to be distributed to delegators - * - */ +/** Represents a transcoder's rewards and fees to be distributed to delegators */ export type Pool = { __typename: "Pool"; /** Transcoder associated with the pool */ @@ -2736,12 +2882,11 @@ export enum Pool_OrderBy { TotalStake = "totalStake", } -/** - * Livepeer protocol global parameters - * - */ +/** 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 */ @@ -2796,10 +2941,7 @@ export type Protocol = { winningTicketCount: Scalars["Int"]; }; -/** - * Livepeer protocol global parameters - * - */ +/** Livepeer protocol global parameters */ export type ProtocolPendingActivationArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -2808,10 +2950,7 @@ export type ProtocolPendingActivationArgs = { where?: InputMaybe; }; -/** - * Livepeer protocol global parameters - * - */ +/** Livepeer protocol global parameters */ export type ProtocolPendingDeactivationArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -2823,6 +2962,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; @@ -3077,6 +3222,7 @@ export type Protocol_Filter = { }; export enum Protocol_OrderBy { + ActiveBroadcasters = "activeBroadcasters", ActiveTranscoderCount = "activeTranscoderCount", CurrentRound = "currentRound", CurrentRoundActiveTranscoderCount = "currentRound__activeTranscoderCount", @@ -3166,6 +3312,8 @@ export type Query = { bondEvent?: Maybe; bondEvents: Array; broadcaster?: Maybe; + broadcasterDay?: Maybe; + broadcasterDays: Array; broadcasters: Array; burnEvent?: Maybe; burnEvents: Array; @@ -3285,6 +3433,22 @@ export type QueryBroadcasterArgs = { subgraphError?: _SubgraphErrorPolicy_; }; +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; @@ -4015,10 +4179,7 @@ export type QueryWithdrawalEventsArgs = { where?: InputMaybe; }; -/** - * RebondEvent entities are created for every emitted Rebond event. - * - */ +/** RebondEvent entities are created for every emitted Rebond event. */ export type RebondEvent = Event & { __typename: "RebondEvent"; amount: Scalars["BigDecimal"]; @@ -4221,10 +4382,7 @@ export enum RebondEvent_OrderBy { UnbondingLockId = "unbondingLockId", } -/** - * ReserveClaimedEvent entities are created for every emitted ReserveClaimed event. - * - */ +/** ReserveClaimedEvent entities are created for every emitted ReserveClaimed event. */ export type ReserveClaimedEvent = Event & { __typename: "ReserveClaimedEvent"; /** Amount of funds claimed by claimant from the reserve for the reserve holder */ @@ -4382,8 +4540,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", @@ -4414,10 +4579,7 @@ export enum ReserveClaimedEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * ReserveFundedEvent entities are created for every emitted ReserveFunded event. - * - */ +/** ReserveFundedEvent entities are created for every emitted ReserveFunded event. */ export type ReserveFundedEvent = Event & { __typename: "ReserveFundedEvent"; /** Amount of funds added to reserve */ @@ -4533,8 +4695,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", @@ -4565,10 +4734,7 @@ export enum ReserveFundedEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * RewardEvent entities are created for every emitted Reward event. - * - */ +/** RewardEvent entities are created for every emitted Reward event. */ export type RewardEvent = Event & { __typename: "RewardEvent"; /** Reference to the delegate that claimed its inflationary token reward */ @@ -4731,10 +4897,7 @@ export enum RewardEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * The Livepeer protocol is round based and each round is represented by some number of Ethereum blocks. - * - */ +/** The Livepeer protocol is round based and each round is represented by some number of Ethereum blocks. */ export type Round = { __typename: "Round"; /** Total active transcoders (up to the limit) */ @@ -4777,10 +4940,7 @@ export type Round = { volumeUSD: Scalars["BigDecimal"]; }; -/** - * The Livepeer protocol is round based and each round is represented by some number of Ethereum blocks. - * - */ +/** The Livepeer protocol is round based and each round is represented by some number of Ethereum blocks. */ export type RoundPoolsArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -4959,10 +5119,7 @@ export enum Round_OrderBy { VolumeUsd = "volumeUSD", } -/** - * ServiceURIUpdateEvent entities are created for every emitted ServiceURIUpdate event. - * - */ +/** ServiceURIUpdateEvent entities are created for every emitted ServiceURIUpdate event. */ export type ServiceUriUpdateEvent = Event & { __typename: "ServiceURIUpdateEvent"; /** Address of sender */ @@ -5118,10 +5275,7 @@ export enum ServiceUriUpdateEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * SetCurrentRewardTokensEvent entities are created for every emitted SetCurrentRewardTokens event. - * - */ +/** SetCurrentRewardTokensEvent entities are created for every emitted SetCurrentRewardTokens event. */ export type SetCurrentRewardTokensEvent = Event & { __typename: "SetCurrentRewardTokensEvent"; /** Current inflation during the round */ @@ -5253,10 +5407,7 @@ export enum SetCurrentRewardTokensEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * StakeClaimedEvent entities are created for every emitted StakeClaimed event. - * - */ +/** StakeClaimedEvent entities are created for every emitted StakeClaimed event. */ export type StakeClaimedEvent = Event & { __typename: "StakeClaimedEvent"; delegate: Scalars["String"]; @@ -5430,866 +5581,7 @@ export enum StakeClaimedEvent_OrderBy { TransactionTo = "transaction__to", } -export type Subscription = { - __typename: "Subscription"; - /** Access to subgraph metadata */ - _meta?: Maybe<_Meta_>; - bondEvent?: Maybe; - bondEvents: Array; - broadcaster?: Maybe; - broadcasters: Array; - burnEvent?: Maybe; - burnEvents: Array; - day?: Maybe; - days: Array; - delegator?: Maybe; - delegators: Array; - depositFundedEvent?: Maybe; - depositFundedEvents: Array; - earningsClaimedEvent?: Maybe; - earningsClaimedEvents: Array; - event?: Maybe; - events: Array; - livepeerAccount?: Maybe; - livepeerAccounts: Array; - migrateDelegatorFinalizedEvent?: Maybe; - migrateDelegatorFinalizedEvents: Array; - mintEvent?: Maybe; - mintEvents: Array; - newRoundEvent?: Maybe; - newRoundEvents: Array; - parameterUpdateEvent?: Maybe; - parameterUpdateEvents: Array; - pauseEvent?: Maybe; - pauseEvents: Array; - poll?: Maybe; - pollCreatedEvent?: Maybe; - pollCreatedEvents: Array; - pollTallies: Array; - pollTally?: Maybe; - polls: Array; - pool?: Maybe; - pools: Array; - protocol?: Maybe; - protocols: Array; - rebondEvent?: Maybe; - rebondEvents: Array; - reserveClaimedEvent?: Maybe; - reserveClaimedEvents: Array; - reserveFundedEvent?: Maybe; - reserveFundedEvents: Array; - rewardEvent?: Maybe; - rewardEvents: Array; - round?: Maybe; - rounds: Array; - serviceURIUpdateEvent?: Maybe; - serviceURIUpdateEvents: Array; - setCurrentRewardTokensEvent?: Maybe; - setCurrentRewardTokensEvents: Array; - stakeClaimedEvent?: Maybe; - stakeClaimedEvents: Array; - transaction?: Maybe; - transactions: Array; - transcoder?: Maybe; - transcoderActivatedEvent?: Maybe; - transcoderActivatedEvents: Array; - transcoderDay?: Maybe; - transcoderDays: Array; - transcoderDeactivatedEvent?: Maybe; - transcoderDeactivatedEvents: Array; - transcoderEvictedEvent?: Maybe; - transcoderEvictedEvents: Array; - transcoderResignedEvent?: Maybe; - transcoderResignedEvents: Array; - transcoderSlashedEvent?: Maybe; - transcoderSlashedEvents: Array; - transcoderUpdateEvent?: Maybe; - transcoderUpdateEvents: Array; - transcoders: Array; - transferBondEvent?: Maybe; - transferBondEvents: Array; - treasuryProposal?: Maybe; - treasuryProposals: Array; - unbondEvent?: Maybe; - unbondEvents: Array; - unbondingLock?: Maybe; - unbondingLocks: Array; - unpauseEvent?: Maybe; - unpauseEvents: Array; - vote?: Maybe; - voteEvent?: Maybe; - voteEvents: Array; - votes: Array; - winningTicketRedeemedEvent?: Maybe; - winningTicketRedeemedEvents: Array; - withdrawFeesEvent?: Maybe; - withdrawFeesEvents: Array; - withdrawStakeEvent?: Maybe; - withdrawStakeEvents: Array; - withdrawalEvent?: Maybe; - withdrawalEvents: Array; -}; - -export type Subscription_MetaArgs = { - block?: InputMaybe; -}; - -export type SubscriptionBondEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionBondEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionBroadcasterArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionBroadcastersArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionBurnEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionBurnEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionDayArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionDaysArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionDelegatorArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionDelegatorsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionDepositFundedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionDepositFundedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionEarningsClaimedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionEarningsClaimedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionLivepeerAccountArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionLivepeerAccountsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionMigrateDelegatorFinalizedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionMigrateDelegatorFinalizedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionMintEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionMintEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionNewRoundEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionNewRoundEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionParameterUpdateEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionParameterUpdateEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionPauseEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionPauseEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionPollArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionPollCreatedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionPollCreatedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionPollTalliesArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionPollTallyArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionPollsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionPoolArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionPoolsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionProtocolArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionProtocolsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionRebondEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionRebondEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionReserveClaimedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionReserveClaimedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionReserveFundedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionReserveFundedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionRewardEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionRewardEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionRoundArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionRoundsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionServiceUriUpdateEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionServiceUriUpdateEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionSetCurrentRewardTokensEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionSetCurrentRewardTokensEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionStakeClaimedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionStakeClaimedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTransactionArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTransactionsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTranscoderArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTranscoderActivatedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTranscoderActivatedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTranscoderDayArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTranscoderDaysArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTranscoderDeactivatedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTranscoderDeactivatedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTranscoderEvictedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTranscoderEvictedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTranscoderResignedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTranscoderResignedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTranscoderSlashedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTranscoderSlashedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTranscoderUpdateEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTranscoderUpdateEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTranscodersArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTransferBondEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTransferBondEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionTreasuryProposalArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionTreasuryProposalsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionUnbondEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionUnbondEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionUnbondingLockArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionUnbondingLocksArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionUnpauseEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionUnpauseEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionVoteArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionVoteEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionVoteEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionVotesArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionWinningTicketRedeemedEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionWinningTicketRedeemedEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionWithdrawFeesEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionWithdrawFeesEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionWithdrawStakeEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionWithdrawStakeEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -export type SubscriptionWithdrawalEventArgs = { - block?: InputMaybe; - id: Scalars["ID"]; - subgraphError?: _SubgraphErrorPolicy_; -}; - -export type SubscriptionWithdrawalEventsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: _SubgraphErrorPolicy_; - where?: InputMaybe; -}; - -/** - * Transaction entities are created for each Ethereum transaction that contains an interaction within Livepeer contracts. - * - */ +/** Transaction entities are created for each Ethereum transaction that contains an interaction within Livepeer contracts. */ export type Transaction = { __typename: "Transaction"; /** Block transaction was mined in */ @@ -6310,10 +5602,7 @@ export type Transaction = { to: Scalars["String"]; }; -/** - * Transaction entities are created for each Ethereum transaction that contains an interaction within Livepeer contracts. - * - */ +/** Transaction entities are created for each Ethereum transaction that contains an interaction within Livepeer contracts. */ export type TransactionEventsArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -6421,10 +5710,7 @@ export enum Transaction_OrderBy { To = "to", } -/** - * Perform transcoding work for the network. The transcoders with the most delegated stake are elected as active transcoders that process transcode jobs for the network. - * - */ +/** Perform transcoding work for the network. The transcoders with the most delegated stake are elected as active transcoders that process transcode jobs for the network. */ export type Transcoder = { __typename: "Transcoder"; /** Round in which the transcoder became active - 0 if inactive */ @@ -6475,10 +5761,7 @@ export type Transcoder = { transcoderDays: Array; }; -/** - * Perform transcoding work for the network. The transcoders with the most delegated stake are elected as active transcoders that process transcode jobs for the network. - * - */ +/** Perform transcoding work for the network. The transcoders with the most delegated stake are elected as active transcoders that process transcode jobs for the network. */ export type TranscoderDelegatorsArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -6487,10 +5770,7 @@ export type TranscoderDelegatorsArgs = { where?: InputMaybe; }; -/** - * Perform transcoding work for the network. The transcoders with the most delegated stake are elected as active transcoders that process transcode jobs for the network. - * - */ +/** Perform transcoding work for the network. The transcoders with the most delegated stake are elected as active transcoders that process transcode jobs for the network. */ export type TranscoderPoolsArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -6499,10 +5779,7 @@ export type TranscoderPoolsArgs = { where?: InputMaybe; }; -/** - * Perform transcoding work for the network. The transcoders with the most delegated stake are elected as active transcoders that process transcode jobs for the network. - * - */ +/** Perform transcoding work for the network. The transcoders with the most delegated stake are elected as active transcoders that process transcode jobs for the network. */ export type TranscoderTranscoderDaysArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -6511,10 +5788,7 @@ export type TranscoderTranscoderDaysArgs = { where?: InputMaybe; }; -/** - * TranscoderActivatedEvent entities are created for every emitted TranscoderActivated event. - * - */ +/** TranscoderActivatedEvent entities are created for every emitted TranscoderActivated event. */ export type TranscoderActivatedEvent = Event & { __typename: "TranscoderActivatedEvent"; /** Future round in which the delegate will become active */ @@ -6677,10 +5951,7 @@ export enum TranscoderActivatedEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * Transcoder data accumulated and condensed into day stats - * - */ +/** Transcoder data accumulated and condensed into day stats */ export type TranscoderDay = { __typename: "TranscoderDay"; /** The date beginning at 12:00am UTC */ @@ -6781,10 +6052,7 @@ export enum TranscoderDay_OrderBy { VolumeUsd = "volumeUSD", } -/** - * TranscoderDeactivatedEvent entities are created for every emitted TranscoderDeactivated event. - * - */ +/** TranscoderDeactivatedEvent entities are created for every emitted TranscoderDeactivated event. */ export type TranscoderDeactivatedEvent = Event & { __typename: "TranscoderDeactivatedEvent"; /** Future round in which the delegate will become deactive */ @@ -6947,10 +6215,7 @@ export enum TranscoderDeactivatedEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * TranscoderEvictedEvent entities are created for every emitted TranscoderEvicted event. - * - */ +/** TranscoderEvictedEvent entities are created for every emitted TranscoderEvicted event. */ export type TranscoderEvictedEvent = Event & { __typename: "TranscoderEvictedEvent"; /** Reference to the delegate that was evicted */ @@ -7102,10 +6367,7 @@ export enum TranscoderEvictedEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * TranscoderResignedEvent entities are created for every emitted TranscoderResigned event. - * - */ +/** TranscoderResignedEvent entities are created for every emitted TranscoderResigned event. */ export type TranscoderResignedEvent = Event & { __typename: "TranscoderResignedEvent"; /** Reference to the delegate that resigned */ @@ -7257,10 +6519,7 @@ export enum TranscoderResignedEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * TranscoderSlashedEvent entities are created for every emitted TranscoderSlashed event. - * - */ +/** TranscoderSlashedEvent entities are created for every emitted TranscoderSlashed event. */ export type TranscoderSlashedEvent = Event & { __typename: "TranscoderSlashedEvent"; /** Reference to the delegate that was slashed */ @@ -7452,10 +6711,7 @@ export enum TranscoderStatus { Registered = "Registered", } -/** - * TranscoderUpdateEvent entities are created for every emitted TranscoderUpdate event. - * - */ +/** TranscoderUpdateEvent entities are created for every emitted TranscoderUpdate event. */ export type TranscoderUpdateEvent = Event & { __typename: "TranscoderUpdateEvent"; /** Reference to the delegate that was updated */ @@ -7887,10 +7143,7 @@ export enum Transcoder_OrderBy { TranscoderDays = "transcoderDays", } -/** - * TransferBond entities are created for every emitted TransferBond event. - * - */ +/** TransferBond entities are created for every emitted TransferBond event. */ export type TransferBondEvent = Event & { __typename: "TransferBondEvent"; amount: Scalars["BigDecimal"]; @@ -8215,10 +7468,7 @@ export enum TreasuryProposal_OrderBy { VoteStart = "voteStart", } -/** - * UnbondEvent entities are created for every emitted Unbond event. - * - */ +/** UnbondEvent entities are created for every emitted Unbond event. */ export type UnbondEvent = Event & { __typename: "UnbondEvent"; /** Amount unbonded in the transaction */ @@ -8435,10 +7685,7 @@ export enum UnbondEvent_OrderBy { WithdrawRound = "withdrawRound", } -/** - * Get an unbonding lock for a delegator - * - */ +/** Get an unbonding lock for a delegator */ export type UnbondingLock = { __typename: "UnbondingLock"; /** Amount being unbonded */ @@ -8594,10 +7841,7 @@ export enum UnbondingLock_OrderBy { WithdrawRound = "withdrawRound", } -/** - * UnpauseEvent entities are created for every emitted Unpause event. - * - */ +/** UnpauseEvent entities are created for every emitted Unpause event. */ export type UnpauseEvent = Event & { __typename: "UnpauseEvent"; /** Ethereum transaction hash + event log index */ @@ -8707,10 +7951,7 @@ export enum UnpauseEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * Vote data - * - */ +/** Vote data */ export type Vote = { __typename: "Vote"; /** Vote choice */ @@ -8729,10 +7970,7 @@ export type Vote = { voter: Scalars["String"]; }; -/** - * VoteEvent entities are created for every emitted Vote event. - * - */ +/** VoteEvent entities are created for every emitted Vote event. */ export type VoteEvent = Event & { __typename: "VoteEvent"; /** Voter choice. Zero means yes and one means no */ @@ -9000,10 +8238,7 @@ export enum Vote_OrderBy { Voter = "voter", } -/** - * WinningTicketRedeemedEvent entities are created for every emitted WinningTicketRedeemed event. - * - */ +/** WinningTicketRedeemedEvent entities are created for every emitted WinningTicketRedeemed event. */ export type WinningTicketRedeemedEvent = Event & { __typename: "WinningTicketRedeemedEvent"; /** Auxilary data included in ticket used for additional validation */ @@ -9236,8 +8471,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", @@ -9250,10 +8492,7 @@ export enum WinningTicketRedeemedEvent_OrderBy { WinProb = "winProb", } -/** - * WithdrawFeesEvent entities are created for every emitted WithdrawFees event. - * - */ +/** WithdrawFeesEvent entities are created for every emitted WithdrawFees event. */ export type WithdrawFeesEvent = Event & { __typename: "WithdrawFeesEvent"; /** Amount of fees withdrawn */ @@ -9429,10 +8668,7 @@ export enum WithdrawFeesEvent_OrderBy { TransactionTo = "transaction__to", } -/** - * WithdrawStakeEvent entities are created for every emitted WithdrawStake event. - * - */ +/** WithdrawStakeEvent entities are created for every emitted WithdrawStake event. */ export type WithdrawStakeEvent = Event & { __typename: "WithdrawStakeEvent"; /** Amount of stake withdrawn */ @@ -9596,10 +8832,7 @@ export enum WithdrawStakeEvent_OrderBy { UnbondingLockId = "unbondingLockId", } -/** - * WithdrawalEvent entities are created for every emitted Withdrawal event. - * - */ +/** WithdrawalEvent entities are created for every emitted Withdrawal event. */ export type WithdrawalEvent = Event & { __typename: "WithdrawalEvent"; /** Deposit amount withdrawn */ @@ -9745,8 +8978,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", @@ -9778,7 +9018,6 @@ export type _Meta_ = { * will be null if the _meta field has a block constraint that asks for * a block number. It will be filled if the _meta field has no block constraint * and therefore asks for the latest block - * */ block: _Block_; /** The deployment ID */ @@ -9843,6 +9082,16 @@ export type AccountQuery = { 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; @@ -10284,6 +9533,31 @@ export type EventsQuery = { transcoders: Array<{ __typename: "Transcoder"; id: string }>; }; +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; @@ -10845,6 +10119,15 @@ export const AccountDocument = gql` id } } + gateway: broadcaster(id: $account) { + id + deposit + reserve + totalVolumeETH + ninetyDayVolumeETH + firstActiveDay + lastActiveDay + } protocol(id: "0") { id totalSupply @@ -11295,6 +10578,82 @@ export type EventsQueryResult = Apollo.QueryResult< EventsQuery, EventsQueryVariables >; +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< + GatewaysQuery, + GatewaysQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery( + GatewaysDocument, + options + ); +} +export type GatewaysQueryHookResult = ReturnType; +export type GatewaysLazyQueryHookResult = ReturnType< + typeof useGatewaysLazyQuery +>; +export type GatewaysQueryResult = Apollo.QueryResult< + GatewaysQuery, + GatewaysQueryVariables +>; export const OrchestratorsDocument = gql` query orchestrators( $currentRound: BigInt diff --git a/codegen.yml b/codegen.yml index e9f74ef0..fcc684bb 100644 --- a/codegen.yml +++ b/codegen.yml @@ -1,9 +1,18 @@ 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: plugins: + - add: + content: | + /* eslint-disable */ + /** + * @generated + * This file was automatically generated and should not be edited. + * To add new queries, mutations, or subscriptions, create or update files in the queries folder. + */ - typescript - typescript-operations - typescript-react-apollo @@ -19,4 +28,4 @@ config: hooks: afterAllFileWrite: - - yarn prettier --write ./apollo/subgraph.ts + - pnpm prettier --write ./apollo/subgraph.ts diff --git a/components/GatewayList/index.tsx b/components/GatewayList/index.tsx new file mode 100644 index 00000000..1002febe --- /dev/null +++ b/components/GatewayList/index.tsx @@ -0,0 +1,349 @@ +import Table from "@components/Table"; +import IdentityAvatar from "@components/IdentityAvatar"; +import PopoverLink from "@components/PopoverLink"; +import { ExplorerTooltip } from "@components/ExplorerTooltip"; +import { + Badge, + Box, + Flex, + Link as A, + IconButton, + Popover, + PopoverContent, + PopoverTrigger, + Text, +} from "@livepeer/design-system"; +import dayjs from "@lib/dayjs"; +import Link from "next/link"; +import numbro from "numbro"; +import { useMemo } from "react"; +import { GatewaysQuery } from "apollo"; +import { Column } from "react-table"; +import { useEnsData } from "hooks"; +import { textTruncate } from "@lib/utils"; +import { DotsHorizontalIcon } from "@radix-ui/react-icons"; + +type GatewayRow = + NonNullable[number] & { + depositNumber: number; + reserveNumber: number; + ninetyDayVolumeNumber: number; + totalVolumeNumber: number; + lastActiveDayNumber: number; + }; + +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 which is actively routing jobs to orchestrators and + paying fees. + + } + > + Gateway + + ), + id: "gateway", + accessor: "id", + disableSortBy: true, + 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/GatewayProfileView/index.tsx b/components/GatewayProfileView/index.tsx new file mode 100644 index 00000000..b2555370 --- /dev/null +++ b/components/GatewayProfileView/index.tsx @@ -0,0 +1,114 @@ +import Stat from "@components/Stat"; +import { Box, Flex, Grid } from "@livepeer/design-system"; +import dayjs from "@lib/dayjs"; +import numbro from "numbro"; +import { useMemo } from "react"; +import { GatewaysQuery } from "apollo"; + +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 BroadcastingView = ({ + gateway, +}: { + gateway?: GatewaysQuery["gateways"] extends Array ? G | null : null; +}) => { + const isActive = useMemo( + () => Number(gateway?.ninetyDayVolumeETH ?? 0) > 0, + [gateway?.ninetyDayVolumeETH] + ); + const activeSince = useMemo( + () => + gateway?.firstActiveDay + ? dayjs.unix(gateway.firstActiveDay).format("MMM D, YYYY") + : "Pending graph update", + [gateway?.firstActiveDay] + ); + const statusText = useMemo(() => { + if (isActive && gateway?.lastActiveDay) { + return `Active ${dayjs.unix(gateway.lastActiveDay).fromNow(true)}`; + } + return "Inactive"; + }, [gateway?.lastActiveDay, isActive]); + const statItems = useMemo( + () => [ + { + label: "Total fees distributed", + value: formatEth(gateway?.totalVolumeETH), + tooltip: "Lifetime fees this gateway has distributed on-chain.", + }, + { + label: "Status", + value: statusText, + tooltip: + "Gateways are marked active if they have distributed fees in the past 90 days.", + }, + { + label: "90d fees distributed", + value: formatEth(gateway?.ninetyDayVolumeETH), + tooltip: "Total fees distributed to orchestrators over the last 90 days.", + }, + { + label: "Activation date", + value: activeSince, + tooltip: "First day this gateway funded deposit/reserve on-chain.", + }, + { + label: "Deposit balance", + value: formatEth(gateway?.deposit), + tooltip: "Current deposit balance funded for gateway job payouts.", + }, + { + label: "Reserve balance", + value: formatEth(gateway?.reserve), + tooltip: "Reserve funds available for winning ticket payouts.", + }, + ], + [ + activeSince, + gateway?.deposit, + gateway?.ninetyDayVolumeETH, + gateway?.reserve, + gateway?.totalVolumeETH, + statusText, + ] + ); + + return ( + + + {statItems.map((stat) => ( + + ))} + + + ); +}; + +export default BroadcastingView; diff --git a/components/PerformanceList/index.tsx b/components/PerformanceList/index.tsx index 59e3d963..80a93c25 100644 --- a/components/PerformanceList/index.tsx +++ b/components/PerformanceList/index.tsx @@ -6,7 +6,6 @@ import { Region } from "@lib/api/types/get-regions"; import { textTruncate } from "@lib/utils"; import { Badge, Box, Flex, Link as A, Skeleton } from "@livepeer/design-system"; import { QuestionMarkCircledIcon } from "@modulz/radix-icons"; -import { OrchestratorsQueryResult } from "apollo"; import { useAllScoreData, useEnsData } from "hooks"; import Link from "next/link"; import numbro from "numbro"; diff --git a/layouts/account.tsx b/layouts/account.tsx index 37c2c8e1..24ccff93 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -6,6 +6,7 @@ import OrchestratingView from "@components/OrchestratingView"; import Profile from "@components/Profile"; import { getLayout, LAYOUT_MAX_WIDTH } from "@layouts/main"; import { bondingManager } from "@lib/api/abis/main/BondingManager"; +import BroadcastingView from "@components/GatewayProfileView"; import { checkAddressEquality } from "@lib/utils"; import { Box, @@ -37,9 +38,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 = ({ account, @@ -107,6 +113,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] @@ -125,9 +132,10 @@ const AccountLayout = ({ isOrchestrator, accountId ?? "", view ?? "delegating", - isMyDelegate + isMyDelegate, + isGateway ), - [isOrchestrator, accountId, view, isMyDelegate] + [isOrchestrator, accountId, view, isMyDelegate, isGateway] ); useEffect(() => { @@ -302,6 +310,9 @@ const AccountLayout = ({ /> )} {view === "history" && } + {view === "broadcasting" && ( + + )} {(isOrchestrator || isMyDelegate || isDelegatingAndIsMyAccountView) && (width > 1020 ? ( @@ -362,27 +373,34 @@ function getTabs( isOrchestrator: boolean, account: string, view: TabTypeEnum, - isMyDelegate: boolean + isMyDelegate: boolean, + hasGateway: 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", }); } + tabs.push({ + name: "Delegating", + href: `/accounts/${account}/delegating`, + isActive: view === "delegating", + }); + if (hasGateway) { + tabs.push({ + name: "Broadcasting", + href: `/accounts/${account}/broadcasting`, + isActive: view === "broadcasting", + }); + } + tabs.push({ + name: "History", + href: `/accounts/${account}/history`, + isActive: view === "history", + }); return tabs; } diff --git a/layouts/main.tsx b/layouts/main.tsx index d5c0c38e..dad02e7a 100644 --- a/layouts/main.tsx +++ b/layouts/main.tsx @@ -36,6 +36,7 @@ import { ChevronDownIcon, EyeOpenIcon, } from "@modulz/radix-icons"; +import { LuRadioTower } from "react-icons/lu"; import { usePollsQuery, useProtocolQuery, @@ -188,6 +189,13 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { icon: DNS, className: "orchestrators", }, + { + name: "Gateways", + href: "/gateways", + as: "/gateways", + icon: LuRadioTower, + className: "gateways", + }, { name: ( @@ -466,6 +474,32 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { Orchestrators + + + + + + + {!gateways?.gateways ? ( + + + + ) : ( + + + + )} + { 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 errorProps; } const props: PageProps = { 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/gateways.graphql b/queries/gateways.graphql new file mode 100644 index 00000000..3a49bfb5 --- /dev/null +++ b/queries/gateways.graphql @@ -0,0 +1,26 @@ +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 + } +} From 8d4699ac3c005775c236126a669c6f5e02a7835c Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sat, 22 Nov 2025 14:47:27 +0100 Subject: [PATCH 02/20] feat: add paid tickets to gateway history Ensure that paid tickets also show up on the GatewayHistory. --- apollo/subgraph.ts | 18 ++++++++++++++- components/HistoryView/index.tsx | 39 ++++++++++++++++++++++++++------ queries/transactions.graphql | 14 +++++++++++- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/apollo/subgraph.ts b/apollo/subgraph.ts index f8fdb04b..69a57192 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -9970,6 +9970,8 @@ export type TransactionsQuery = { | { __typename: "WinningTicketRedeemedEvent"; faceValue: string; + sender: { __typename: "Broadcaster"; id: string }; + recipient: { __typename: "Transcoder"; id: string }; round: { __typename: "Round"; id: string }; transaction: { __typename: "Transaction"; @@ -10014,6 +10016,8 @@ export type TransactionsQuery = { faceValue: string; round: { __typename: "Round"; id: string }; transaction: { __typename: "Transaction"; id: string; timestamp: number }; + sender: { __typename: "Broadcaster"; id: string }; + recipient: { __typename: "Transcoder"; id: string }; }>; }; @@ -11135,6 +11139,12 @@ export const TransactionsDocument = gql` } ... on WinningTicketRedeemedEvent { faceValue + sender { + id + } + recipient { + id + } } ... on DepositFundedEvent { sender { @@ -11153,7 +11163,7 @@ export const TransactionsDocument = gql` winningTicketRedeemedEvents( orderBy: timestamp orderDirection: desc - where: { recipient: $account } + where: { or: [{ recipient: $account }, { sender: $account }] } ) { __typename id @@ -11165,6 +11175,12 @@ export const TransactionsDocument = gql` timestamp } faceValue + sender { + id + } + recipient { + id + } } } `; diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index 903dda8a..07ccfd4e 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -56,6 +56,24 @@ const Index = () => { [events] ); + // Tag winning tickets (in/out/self) within the current window + const ticketEvents = useMemo(() => { + const tickets = data?.winningTicketRedeemedEvents ?? []; + 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, events.length, 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( @@ -64,14 +82,12 @@ const Index = () => { ...events.filter( (e) => (e as any)?.__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) { @@ -679,7 +695,15 @@ function renderSwitch(event: any, 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} @@ -731,7 +755,7 @@ function renderSwitch(event: any, i: number) { {" "} - + + {amountPrefix} {numbro(event.faceValue).format({ mantissa: 3, average: true, @@ -742,6 +766,7 @@ function renderSwitch(event: any, i: number) { ); + } case "DepositFundedEvent": return ( Date: Sat, 22 Nov 2025 14:52:35 +0100 Subject: [PATCH 03/20] fix: hide delegating tab when account is not delegator --- layouts/account.tsx | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/layouts/account.tsx b/layouts/account.tsx index 24ccff93..93f81625 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -133,9 +133,11 @@ const AccountLayout = ({ accountId ?? "", view ?? "delegating", isMyDelegate, - isGateway + isGateway, + isMyAccount, + Boolean(account?.delegator) ), - [isOrchestrator, accountId, view, isMyDelegate, isGateway] + [isOrchestrator, accountId, view, isMyDelegate, isGateway, isMyAccount, account?.delegator] ); useEffect(() => { @@ -374,7 +376,9 @@ function getTabs( account: string, view: TabTypeEnum, isMyDelegate: boolean, - hasGateway: boolean + hasGateway: boolean, + isMyAccount: boolean, + hasDelegator: boolean ): Array { const tabs: Array = []; if (isOrchestrator || isMyDelegate) { @@ -384,11 +388,13 @@ function getTabs( isActive: view === "orchestrating", }); } - tabs.push({ - name: "Delegating", - href: `/accounts/${account}/delegating`, - isActive: view === "delegating", - }); + if (isMyAccount || hasDelegator) { + tabs.push({ + name: "Delegating", + href: `/accounts/${account}/delegating`, + isActive: view === "delegating", + }); + } if (hasGateway) { tabs.push({ name: "Broadcasting", From 50655d330888aa2dd39762ca907a17907ca38e94 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sat, 22 Nov 2025 15:12:33 +0100 Subject: [PATCH 04/20] fix: correct gateway nav highlight when logged out Ensure the orchestrators button has the right background when the `/orchestrators` page is selected and the user is logged out. --- layouts/main.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/layouts/main.tsx b/layouts/main.tsx index dad02e7a..7cf43ba9 100644 --- a/layouts/main.tsx +++ b/layouts/main.tsx @@ -480,7 +480,8 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { css={{ marginLeft: "$2", backgroundColor: - !asPath.includes(accountAddress ?? "") && + (!accountAddress || + !asPath.includes(accountAddress)) && (asPath.includes("/gateways") || asPath.includes("/broadcasting")) ? "hsla(0,100%,100%,.05)" From 3adb176922ca1ab045031498bcc4b8561ae69339 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sun, 28 Dec 2025 10:25:51 +0100 Subject: [PATCH 05/20] fix: fix merge conflict --- codegen.yml | 1 - components/HistoryView/index.tsx | 6 +- components/PerformanceList/index.tsx | 1 + pages/accounts/[account]/broadcasting.tsx | 75 +---------------------- pages/gateways.tsx | 4 +- pages/index.tsx | 1 + 6 files changed, 6 insertions(+), 82 deletions(-) diff --git a/codegen.yml b/codegen.yml index fcc684bb..817ff9e0 100644 --- a/codegen.yml +++ b/codegen.yml @@ -7,7 +7,6 @@ generates: plugins: - add: content: | - /* eslint-disable */ /** * @generated * This file was automatically generated and should not be edited. diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index d12e7067..146e3d67 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -74,11 +74,7 @@ const Index = () => { ? ("out" as const) : ("in" as const), })); - }, [ - data?.winningTicketRedeemedEvents, - account, - lastEventTimestamp, - ]); + }, [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 diff --git a/components/PerformanceList/index.tsx b/components/PerformanceList/index.tsx index 34d662ea..fede2324 100644 --- a/components/PerformanceList/index.tsx +++ b/components/PerformanceList/index.tsx @@ -6,6 +6,7 @@ import { Region } from "@lib/api/types/get-regions"; import { textTruncate } from "@lib/utils"; import { Badge, Box, Flex, Link as A, Skeleton } from "@livepeer/design-system"; import { QuestionMarkCircledIcon } from "@modulz/radix-icons"; +import { OrchestratorsQueryResult } from "apollo"; import { useAllScoreData, useEnsData } from "hooks"; import Link from "next/link"; import numbro from "numbro"; diff --git a/pages/accounts/[account]/broadcasting.tsx b/pages/accounts/[account]/broadcasting.tsx index de982e49..9375f0fe 100644 --- a/pages/accounts/[account]/broadcasting.tsx +++ b/pages/accounts/[account]/broadcasting.tsx @@ -1,81 +1,8 @@ import AccountLayout from "@layouts/account"; import { getLayout } from "@layouts/main"; -import { getAccount, getGateways, getSortedOrchestrators } from "@lib/api/ssr"; -import { EnsIdentity } from "@lib/api/types/get-ens"; -import { - AccountQueryResult, - getApollo, - OrchestratorsSortedQueryResult, -} from "apollo"; -type PageProps = { - account: AccountQueryResult["data"]; - sortedOrchestrators: OrchestratorsSortedQueryResult["data"]; - fallback: { [key: string]: EnsIdentity }; -}; - -const BroadcastingPage = ({ account, sortedOrchestrators }: PageProps) => { - return ( - - ); -}; +const BroadcastingPage = () => ; BroadcastingPage.getLayout = getLayout; -export const getStaticPaths = async () => { - const { gateways } = await getGateways(); - - const paths = - gateways?.data?.gateways?.map((g) => ({ - params: { account: g.id }, - })) ?? []; - - return { - paths, - fallback: "blocking", - }; -}; - -export const getStaticProps = async (context) => { - try { - const client = getApollo(); - const accountId = context.params?.account?.toString().toLowerCase(); - - if (!accountId) { - return { notFound: true }; - } - - const { account, fallback } = await getAccount(client, accountId); - const { sortedOrchestrators, fallback: sortedOrchestratorsFallback } = - await getSortedOrchestrators(client); - - if (!account.data?.gateway || !sortedOrchestrators.data) { - return { notFound: true, revalidate: 300 }; - } - - const props: PageProps = { - account: account.data, - sortedOrchestrators: sortedOrchestrators.data, - fallback: { - ...sortedOrchestratorsFallback, - ...fallback, - }, - }; - - return { - props, - revalidate: 600, - }; - } catch (e) { - console.error(e); - } - - return { - notFound: true, - }; -}; - export default BroadcastingPage; diff --git a/pages/gateways.tsx b/pages/gateways.tsx index d17aa955..baca9ae6 100644 --- a/pages/gateways.tsx +++ b/pages/gateways.tsx @@ -1,5 +1,5 @@ import GatewayList from "@components/GatewayList"; -import { getLayout, LAYOUT_MAX_WIDTH } from "@layouts/main"; +import { getLayout } from "@layouts/main"; import { getGateways } from "@lib/api/ssr"; import { Box, Container, Flex, Heading } from "@livepeer/design-system"; import { GatewaysQueryResult, getApollo } from "apollo"; @@ -15,7 +15,7 @@ const GatewaysPage = ({ gateways }: PageProps) => { Livepeer Explorer - Gateways - + { const errorProps: PageProps = { hadError: true, orchestrators: null, + gateways: null, events: null, protocol: null, fallback: {}, From b74acd76af9f8e10e7c1d999b5186c1163ad4e7c Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sun, 28 Dec 2025 11:33:13 +0100 Subject: [PATCH 06/20] chore: remove duplicate env variable from template --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index e3ea91e8..521dd385 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,6 @@ NEXT_PUBLIC_L2_RPC_URL= NEXT_PUBLIC_GITHUB_LIP_NAMESPACE=adamsoffer NEXT_PUBLIC_SUBGRAPH_API_KEY= NEXT_PUBLIC_SUBGRAPH_ID=FE63YgkzcpVocxdCEyEYbvjYqEf2kb1A6daMYRxmejYC -NEXT_PUBLIC_SUBGRAPH_ENDPOINT= 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 From cc3cffef455747c692f76ab391c9e985210c8e20 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sun, 28 Dec 2025 11:51:34 +0100 Subject: [PATCH 07/20] refactor: apply code improvements --- components/{GatewayProfileView => BroadcastingView}/index.tsx | 1 + components/GatewayList/index.tsx | 1 + layouts/account.tsx | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) rename components/{GatewayProfileView => BroadcastingView}/index.tsx (98%) diff --git a/components/GatewayProfileView/index.tsx b/components/BroadcastingView/index.tsx similarity index 98% rename from components/GatewayProfileView/index.tsx rename to components/BroadcastingView/index.tsx index ca5650cf..f94da17e 100644 --- a/components/GatewayProfileView/index.tsx +++ b/components/BroadcastingView/index.tsx @@ -5,6 +5,7 @@ import { GatewaysQuery } from "apollo"; 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( diff --git a/components/GatewayList/index.tsx b/components/GatewayList/index.tsx index 8468e300..fc24aad9 100644 --- a/components/GatewayList/index.tsx +++ b/components/GatewayList/index.tsx @@ -30,6 +30,7 @@ type GatewayRow = NonNullable[number] & { lastActiveDayNumber: number; }; +// TODO: replace with common formatting util. const formatEth = (value: number) => { const amount = Number(value ?? 0) || 0; return `${numbro(amount).format( diff --git a/layouts/account.tsx b/layouts/account.tsx index 9c1400ce..10a934fa 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -1,7 +1,7 @@ import BottomDrawer from "@components/BottomDrawer"; +import BroadcastingView from "@components/BroadcastingView"; import DelegatingView from "@components/DelegatingView"; import DelegatingWidget from "@components/DelegatingWidget"; -import BroadcastingView from "@components/GatewayProfileView"; import HistoryView from "@components/HistoryView"; import OrchestratingView from "@components/OrchestratingView"; import Profile from "@components/Profile"; From d0edde771bc6f5e4929ce0ddc116d180da1157d5 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sun, 28 Dec 2025 12:47:54 +0100 Subject: [PATCH 08/20] refactor: ensure correct tab highlight behavoir Ensure that the gateways tab is highlighted when on a gateway profile and the orchestrator tab when on a orchestrator profile. --- layouts/main.tsx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/layouts/main.tsx b/layouts/main.tsx index 73114115..226283a7 100644 --- a/layouts/main.tsx +++ b/layouts/main.tsx @@ -126,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( () => @@ -455,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)", @@ -482,13 +486,9 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { size="3" css={{ marginLeft: "$2", - backgroundColor: - (!accountAddress || - !asPath.includes(accountAddress)) && - (asPath.includes("/gateways") || - asPath.includes("/broadcasting")) - ? "hsla(0,100%,100%,.05)" - : "transparent", + backgroundColor: isGatewaysNavActive + ? "hsla(0,100%,100%,.05)" + : "transparent", color: "white", "&:hover": { backgroundColor: "hsla(0,100%,100%,.1)", From f622b7864adbf96745ee6e458f47e90f215fe7b7 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sun, 28 Dec 2025 13:18:12 +0100 Subject: [PATCH 09/20] refactor: improve gateway inclusion criteria and UI Ensure we include gateways that were activated in the last year or did send tickets in the last 90D. Also improve the UI to make this behavoir more visible. --- components/BroadcastingView/index.tsx | 8 ++------ components/GatewayList/index.tsx | 4 +--- lib/api/ssr.ts | 2 +- pages/gateways.tsx | 13 +++++++++++++ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/components/BroadcastingView/index.tsx b/components/BroadcastingView/index.tsx index f94da17e..3b0db396 100644 --- a/components/BroadcastingView/index.tsx +++ b/components/BroadcastingView/index.tsx @@ -29,10 +29,7 @@ const BroadcastingView = ({ ); const statusText = useMemo(() => { const active = Number(gateway?.ninetyDayVolumeETH ?? 0) > 0; - if (active && gateway?.lastActiveDay) { - return `Active ${dayjs.unix(gateway.lastActiveDay).fromNow(true)}`; - } - return "Inactive"; + return active ? "Active" : "Inactive"; }, [gateway]); const statItems = useMemo( () => [ @@ -44,8 +41,7 @@ const BroadcastingView = ({ { label: "Status", value: statusText, - tooltip: - "Gateways are marked active if they have distributed fees in the past 90 days.", + tooltip: "Active if this gateway distributed fees in the last 90 days.", }, { label: "90d fees distributed", diff --git a/components/GatewayList/index.tsx b/components/GatewayList/index.tsx index fc24aad9..4f4ec6f1 100644 --- a/components/GatewayList/index.tsx +++ b/components/GatewayList/index.tsx @@ -68,8 +68,7 @@ const GatewayList = ({ multiline content={ - The account which is actively routing jobs to orchestrators and - paying fees. + The account routing jobs to orchestrators and paying fees. } > @@ -78,7 +77,6 @@ const GatewayList = ({ ), id: "gateway", accessor: "id", - disableSortBy: true, Cell: ({ row, value }) => { const address = value as string; const identity = useEnsData(address); diff --git a/lib/api/ssr.ts b/lib/api/ssr.ts index 40be8d4d..dde5da9c 100644 --- a/lib/api/ssr.ts +++ b/lib/api/ssr.ts @@ -31,7 +31,7 @@ export async function getProtocol(client = getApollo()) { export async function getGateways(client = getApollo()) { const currentDay = Math.floor(Date.now() / 1000 / 86400); - const minActiveDay = Math.max(currentDay - 90, 0); // include activated last 90d + const minActiveDay = Math.max(currentDay - 365, 0); // include activated last 12 months const gateways = await client.query({ query: GatewaysDocument, diff --git a/pages/gateways.tsx b/pages/gateways.tsx index baca9ae6..89ee0607 100644 --- a/pages/gateways.tsx +++ b/pages/gateways.tsx @@ -31,6 +31,19 @@ const GatewaysPage = ({ gateways }: PageProps) => { Gateways + + Showing gateways with fees in the last 90 days or activated within + the last 12 months. + From c5da583ae87d07075642176e4bdf306fd48d2980 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sun, 28 Dec 2025 13:24:41 +0100 Subject: [PATCH 10/20] refactor: improve profile tab order Ensure gateway tab is shown before the delegator tab if an entity is both a orchestrator and gateway. --- layouts/account.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/layouts/account.tsx b/layouts/account.tsx index 10a934fa..b0aea4c4 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -468,13 +468,6 @@ function getTabs( isActive: view === "orchestrating", }); } - if (isMyAccount || hasDelegator) { - tabs.push({ - name: "Delegating", - href: `/accounts/${account}/delegating`, - isActive: view === "delegating", - }); - } if (hasGateway) { tabs.push({ name: "Broadcasting", @@ -482,6 +475,13 @@ function getTabs( 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`, From f1faa977a9a79f77b25b0fb9ee0f7543fa6daf1a Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Mon, 29 Dec 2025 12:49:22 +0100 Subject: [PATCH 11/20] feat: add warning for self-redeeming gateways Adds a warning to inform users when gateways self-redeem tickets, helping them better assess whether the gateway provides real value. --- apollo/subgraph.ts | 51 ++++++++++++++++++++++++++- components/BroadcastingView/index.tsx | 35 ++++++++++++++++-- hooks/index.tsx | 1 + hooks/useGatewaySelfRedeemStatus.ts | 20 +++++++++++ queries/gatewaySelfRedeem.graphql | 12 +++++++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 hooks/useGatewaySelfRedeemStatus.ts create mode 100644 queries/gatewaySelfRedeem.graphql diff --git a/apollo/subgraph.ts b/apollo/subgraph.ts index f4a9aabc..ec710b10 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -9160,6 +9160,13 @@ 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']; @@ -9675,6 +9682,48 @@ 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") { @@ -10285,4 +10334,4 @@ export function useVoteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions; export type VoteLazyQueryHookResult = ReturnType; -export type VoteQueryResult = Apollo.QueryResult; +export type VoteQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/components/BroadcastingView/index.tsx b/components/BroadcastingView/index.tsx index 3b0db396..ff4ba506 100644 --- a/components/BroadcastingView/index.tsx +++ b/components/BroadcastingView/index.tsx @@ -1,7 +1,10 @@ +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"; @@ -15,11 +18,24 @@ const formatEth = (value?: string | number | null) => { )} 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 @@ -34,38 +50,51 @@ const BroadcastingView = ({ const statItems = useMemo( () => [ { + id: "total-fees-distributed", label: "Total fees distributed", - value: formatEth(gateway?.totalVolumeETH), + 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] + [gateway, activeSince, statusText, isSelfRedeeming] ); return ( @@ -87,7 +116,7 @@ const BroadcastingView = ({ > {statItems.map((stat) => ( { + 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/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 + } + } +} From 478ba3f32c8155150330671609b68bf189a2e01d Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Mon, 29 Dec 2025 14:15:46 +0100 Subject: [PATCH 12/20] refactor: improve account tabs mobile layout Ensure that a scrollbar is created on mobile devices to allow the new gateway tab to fit small screen sizes. --- layouts/account.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/layouts/account.tsx b/layouts/account.tsx index b0aea4c4..8caff729 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -349,6 +349,9 @@ const AccountLayout = () => { position: "relative", borderBottom: "1px solid", borderColor: "$neutral6", + overflowX: "auto", + flexWrap: "nowrap", + WebkitOverflowScrolling: "touch", }} > {tabs.map((tab: TabType, i: number) => ( @@ -365,6 +368,8 @@ const AccountLayout = () => { paddingBottom: "$2", fontSize: "$3", fontWeight: 500, + flex: "0 0 auto", + whiteSpace: "nowrap", borderBottom: "2px solid", borderColor: tab.isActive ? "$primary11" : "transparent", "&:hover": { From a15d1be66c6c8875d7ca7413cfe636b02bf65094 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Mon, 29 Dec 2025 15:01:35 +0100 Subject: [PATCH 13/20] feat: add HorizontalScrollContainer component Adds a reusable component that improves horizontal scrolling behavior on mobile devices. --- .../HorizontalScrollContainer/index.tsx | 90 +++++++++++++++++++ layouts/account.tsx | 18 +--- 2 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 components/HorizontalScrollContainer/index.tsx diff --git a/components/HorizontalScrollContainer/index.tsx b/components/HorizontalScrollContainer/index.tsx new file mode 100644 index 00000000..517d2fe1 --- /dev/null +++ b/components/HorizontalScrollContainer/index.tsx @@ -0,0 +1,90 @@ +import { Box } from "@livepeer/design-system"; +import { + forwardRef, + ReactNode, + useLayoutEffect, + useRef, + useState, +} from "react"; + +type HorizontalScrollContainerProps = { + children: ReactNode; +}; + +const HorizontalScrollContainer = forwardRef< + HTMLDivElement, + HorizontalScrollContainerProps +>(({ children }, ref) => { + const innerRef = useRef(null); + const [hasOverflow, setHasOverflow] = useState(false); + + useLayoutEffect(() => { + const el = innerRef.current; + if (!el) return; + const updateState = () => { + const overflow = el.scrollWidth > el.clientWidth + 1; + const maxVisibleRight = el.scrollLeft + el.clientWidth; + setHasOverflow(overflow && maxVisibleRight < el.scrollWidth - 1); + }; + 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(); + }; + }, []); + + 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/layouts/account.tsx b/layouts/account.tsx index 8caff729..1c84894f 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -3,6 +3,7 @@ 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"; @@ -12,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, @@ -341,19 +341,7 @@ const AccountLayout = () => { ))} - + {tabs.map((tab: TabType, i: number) => ( { {tab.name} ))} - + {view === "orchestrating" && ( Date: Tue, 30 Dec 2025 11:44:49 +0100 Subject: [PATCH 14/20] refactor: add scrollBuffer constant Make scrollbuffer a constant to improve code readability. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- components/HorizontalScrollContainer/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/HorizontalScrollContainer/index.tsx b/components/HorizontalScrollContainer/index.tsx index 517d2fe1..3c38dce6 100644 --- a/components/HorizontalScrollContainer/index.tsx +++ b/components/HorizontalScrollContainer/index.tsx @@ -22,9 +22,12 @@ const HorizontalScrollContainer = forwardRef< const el = innerRef.current; if (!el) return; const updateState = () => { - const overflow = el.scrollWidth > el.clientWidth + 1; + const scrollBuffer = 1; + const overflow = el.scrollWidth > el.clientWidth + scrollBuffer; const maxVisibleRight = el.scrollLeft + el.clientWidth; - setHasOverflow(overflow && maxVisibleRight < el.scrollWidth - 1); + setHasOverflow( + overflow && maxVisibleRight < el.scrollWidth - scrollBuffer + ); }; updateState(); const observer = new ResizeObserver(updateState); From 6ae32ab31541e04192397186015c268b4d880850 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Tue, 30 Dec 2025 11:52:12 +0100 Subject: [PATCH 15/20] refactor: add accessibility labels to horizontalScrollContainer Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com --- components/HorizontalScrollContainer/index.tsx | 6 +++++- layouts/account.tsx | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/components/HorizontalScrollContainer/index.tsx b/components/HorizontalScrollContainer/index.tsx index 3c38dce6..81bae7e2 100644 --- a/components/HorizontalScrollContainer/index.tsx +++ b/components/HorizontalScrollContainer/index.tsx @@ -9,12 +9,14 @@ import { type HorizontalScrollContainerProps = { children: ReactNode; + ariaLabel?: string; + role?: "navigation"; }; const HorizontalScrollContainer = forwardRef< HTMLDivElement, HorizontalScrollContainerProps ->(({ children }, ref) => { +>(({ children, ariaLabel, role }, ref) => { const innerRef = useRef(null); const [hasOverflow, setHasOverflow] = useState(false); @@ -66,6 +68,8 @@ const HorizontalScrollContainer = forwardRef< WebkitOverflowScrolling: "touch", flexWrap: "nowrap", }} + role={role} + aria-label={ariaLabel} ref={setRefs} > {children} diff --git a/layouts/account.tsx b/layouts/account.tsx index 1c84894f..8a6cbf87 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -341,7 +341,10 @@ const AccountLayout = () => { ))} - + {tabs.map((tab: TabType, i: number) => ( Date: Tue, 30 Dec 2025 12:28:16 +0100 Subject: [PATCH 16/20] refactor: improve horizontalScrollContainer scroll behaviour Ensure that the container scrolls to the active page when clicked. --- components/HorizontalScrollContainer/index.tsx | 9 +++++++++ layouts/account.tsx | 2 ++ 2 files changed, 11 insertions(+) diff --git a/components/HorizontalScrollContainer/index.tsx b/components/HorizontalScrollContainer/index.tsx index 81bae7e2..523445b2 100644 --- a/components/HorizontalScrollContainer/index.tsx +++ b/components/HorizontalScrollContainer/index.tsx @@ -42,6 +42,15 @@ const HorizontalScrollContainer = forwardRef< }; }, []); + 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") { diff --git a/layouts/account.tsx b/layouts/account.tsx index 8a6cbf87..3d889f54 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -353,6 +353,8 @@ const AccountLayout = () => { 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", From 4db10d5dbf6ea95bbeea09a7022521222365bb03 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Tue, 30 Dec 2025 12:31:26 +0100 Subject: [PATCH 17/20] refactor: stabilize horizontalScrollContainer fade behavior Recalculate overflow effects when child elements change to ensure consistent fading. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- components/HorizontalScrollContainer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/HorizontalScrollContainer/index.tsx b/components/HorizontalScrollContainer/index.tsx index 523445b2..7ce7a21b 100644 --- a/components/HorizontalScrollContainer/index.tsx +++ b/components/HorizontalScrollContainer/index.tsx @@ -40,7 +40,7 @@ const HorizontalScrollContainer = forwardRef< el.removeEventListener("scroll", handleScroll); observer.disconnect(); }; - }, []); + }, [children]); useLayoutEffect(() => { const el = innerRef.current; From bbc039eb71239576c4605e2c0bdccba40f133495 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 7 Jan 2026 09:57:43 +0100 Subject: [PATCH 18/20] refactor: small formatting fix introduce during merge --- components/HorizontalScrollContainer/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/HorizontalScrollContainer/index.tsx b/components/HorizontalScrollContainer/index.tsx index 4dd03156..44ea0d80 100644 --- a/components/HorizontalScrollContainer/index.tsx +++ b/components/HorizontalScrollContainer/index.tsx @@ -23,10 +23,12 @@ const HorizontalScrollContainer = forwardRef< 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 ); From 76c98862994d81373ba68035eda8aa5e48bdadd7 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Thu, 8 Jan 2026 19:04:59 +0100 Subject: [PATCH 19/20] fix: ensure gateway filtering is correct Fixes a bug in the gateway filtering which caused it to not adhere to our filtering condition. Also applied some small code improvements. --- codegen.yml | 3 +-- layouts/account.tsx | 4 ++-- lib/api/ssr.ts | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/codegen.yml b/codegen.yml index 817ff9e0..143c8c99 100644 --- a/codegen.yml +++ b/codegen.yml @@ -1,6 +1,5 @@ overwrite: true -# 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. +schema: https://gateway.thegraph.com/api/${NEXT_PUBLIC_SUBGRAPH_API_KEY}/subgraphs/id/${NEXT_PUBLIC_SUBGRAPH_ID} documents: ./queries/**/*.graphql generates: ./apollo/subgraph.ts: diff --git a/layouts/account.tsx b/layouts/account.tsx index 3d889f54..8c431142 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -454,7 +454,7 @@ function getTabs( account: string, view: TabTypeEnum, isMyDelegate: boolean, - hasGateway: boolean, + isGateway: boolean, isMyAccount: boolean, hasDelegator: boolean ): Array { @@ -466,7 +466,7 @@ function getTabs( isActive: view === "orchestrating", }); } - if (hasGateway) { + if (isGateway) { tabs.push({ name: "Broadcasting", href: `/accounts/${account}/broadcasting`, diff --git a/lib/api/ssr.ts b/lib/api/ssr.ts index dde5da9c..2089b086 100644 --- a/lib/api/ssr.ts +++ b/lib/api/ssr.ts @@ -30,8 +30,8 @@ export async function getProtocol(client = getApollo()) { } export async function getGateways(client = getApollo()) { - const currentDay = Math.floor(Date.now() / 1000 / 86400); - const minActiveDay = Math.max(currentDay - 365, 0); // include activated last 12 months + const currentTimestamp = Math.floor(Date.now() / 1000); + const minActiveDay = Math.max(currentTimestamp - 365 * 86400, 0); // include activated last 12 months const gateways = await client.query({ query: GatewaysDocument, From babb95a6eb28c610edc05261725035575d844642 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Fri, 9 Jan 2026 15:52:08 +0100 Subject: [PATCH 20/20] chore: add gateway pagination comment --- lib/api/ssr.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/api/ssr.ts b/lib/api/ssr.ts index 2089b086..774b7bc4 100644 --- a/lib/api/ssr.ts +++ b/lib/api/ssr.ts @@ -33,6 +33,7 @@ export async function getGateways(client = getApollo()) { const currentTimestamp = Math.floor(Date.now() / 1000); const minActiveDay = Math.max(currentTimestamp - 365 * 86400, 0); // include activated last 12 months + // TODO: paginate gateways; currently under the 1000 entity cap. const gateways = await client.query({ query: GatewaysDocument, variables: {