diff --git a/apollo/subgraph.ts b/apollo/subgraph.ts index e7511b3e..adfa80fc 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -5764,8 +5764,8 @@ export type Transaction = { id: Scalars['ID']; /** Timestamp for transaction */ timestamp: Scalars['Int']; - /** The receiving party of the transaction */ - to: Scalars['String']; + /** The receiving party of the transaction - null for contract-creation transactions */ + to?: Maybe; }; @@ -9707,7 +9707,7 @@ export type TransactionsQueryVariables = Exact<{ }>; -export type TransactionsQuery = { __typename: 'Query', transactions: Array<{ __typename: 'Transaction', events?: Array<{ __typename: 'BondEvent', additionalAmount: string, delegator: { __typename: 'Delegator', id: string }, newDelegate: { __typename: 'Transcoder', id: string }, oldDelegate?: { __typename: 'Transcoder', id: string } | null, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'BurnEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'DepositFundedEvent', amount: string, sender: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'EarningsClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'MigrateDelegatorFinalizedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'MintEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'NewRoundEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ParameterUpdateEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'PauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'PollCreatedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'RebondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ReserveClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ReserveFundedEvent', amount: string, reserveHolder: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'RewardEvent', rewardTokens: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ServiceURIUpdateEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'SetCurrentRewardTokensEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'StakeClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderActivatedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderDeactivatedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderEvictedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderResignedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderSlashedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderUpdateEvent', rewardCut: string, feeShare: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TransferBondEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TreasuryVoteEvent', id: string, reason?: string | null, support: TreasuryVoteSupport, timestamp: number, weight: string, proposal: { __typename: 'TreasuryProposal', id: string, targets: Array, description: string }, treasuryVoter: { __typename: 'LivepeerAccount', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'UnbondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'UnpauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'VoteEvent', voter: string, choiceID: string, id: string, timestamp: number, poll: { __typename: 'Poll', id: string, proposal: string, endBlock: string, quorum: string, quota: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number }, round: { __typename: 'Round', id: string } } | { __typename: 'WinningTicketRedeemedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'WithdrawFeesEvent', amount: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'WithdrawStakeEvent', amount: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'WithdrawalEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } }> | null }>, winningTicketRedeemedEvents: Array<{ __typename: 'WinningTicketRedeemedEvent', id: string, faceValue: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number }, sender: { __typename: 'Broadcaster', id: string }, recipient: { __typename: 'Transcoder', id: string } }> }; +export type TransactionsQuery = { __typename: 'Query', transactions: Array<{ __typename: 'Transaction', events?: Array<{ __typename: 'BondEvent', additionalAmount: string, delegator: { __typename: 'Delegator', id: string }, newDelegate: { __typename: 'Transcoder', id: string }, oldDelegate?: { __typename: 'Transcoder', id: string } | null, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'BurnEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'DepositFundedEvent', amount: string, sender: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'EarningsClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'MigrateDelegatorFinalizedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'MintEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'NewRoundEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ParameterUpdateEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'PauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'PollCreatedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'RebondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ReserveClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ReserveFundedEvent', amount: string, reserveHolder: { __typename: 'Broadcaster', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'RewardEvent', rewardTokens: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'ServiceURIUpdateEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'SetCurrentRewardTokensEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'StakeClaimedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderActivatedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderDeactivatedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderEvictedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderResignedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderSlashedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TranscoderUpdateEvent', rewardCut: string, feeShare: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TransferBondEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'TreasuryVoteEvent', id: string, reason?: string | null, support: TreasuryVoteSupport, timestamp: number, weight: string, proposal: { __typename: 'TreasuryProposal', id: string, targets: Array, description: string }, treasuryVoter: { __typename: 'LivepeerAccount', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'UnbondEvent', amount: string, delegate: { __typename: 'Transcoder', id: string }, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'UnpauseEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'VoteEvent', voter: string, choiceID: string, id: string, timestamp: number, poll: { __typename: 'Poll', id: string, proposal: string, endBlock: string, quorum: string, quota: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number }, round: { __typename: 'Round', id: string } } | { __typename: 'WinningTicketRedeemedEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'WithdrawFeesEvent', amount: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'WithdrawStakeEvent', amount: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } } | { __typename: 'WithdrawalEvent', round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } }> | null }>, winningTicketRedeemedEvents: Array<{ __typename: 'WinningTicketRedeemedEvent', id: string, faceValue: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number }, sender: { __typename: 'Broadcaster', id: string }, recipient: { __typename: 'Transcoder', id: string } }>, rewardEvents: Array<{ __typename: 'RewardEvent', id: string, rewardTokens: string, round: { __typename: 'Round', id: string }, transaction: { __typename: 'Transaction', id: string, timestamp: number } }> }; export type TranscoderActivatedEventsQueryVariables = Exact<{ where?: InputMaybe; @@ -10781,6 +10781,24 @@ export const TransactionsDocument = gql` id } } + rewardEvents( + orderBy: timestamp + orderDirection: desc + where: {delegate: $account} + first: $first + skip: $skip + ) { + __typename + id + round { + id + } + transaction { + id + timestamp + } + rewardTokens + } } `; diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index 25c76097..4a1c4535 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -83,6 +83,8 @@ const Index = () => { const isVoteEvent = isType("VoteEvent"); const isTreasuryVoteEvent = isType("TreasuryVoteEvent"); + // Clamps tickets/rewards to the oldest loaded transaction so rows only + // append; nothing is stranded, registration precedes both. const lastEventTimestamp = useMemo( () => Number(events?.[(events?.length || 0) - 1]?.transaction?.timestamp ?? 0), @@ -170,18 +172,30 @@ const Index = () => { })); }, [data?.winningTicketRedeemedEvents, account, lastEventTimestamp]); - // WinningTicketRedeemedEvents, VoteEvents, and TreasuryVoteEvents are replaced - // with enriched versions (direction-tagged tickets, IPFS-enriched votes) + // Orchestrator-keyed so delegated reward calls, whose transaction is sent by + // the reward caller, are included too. + const rewardEvents = useMemo( + () => + data?.rewardEvents?.filter( + (e) => (e?.transaction?.timestamp ?? 0) > lastEventTimestamp + ) ?? [], + [data?.rewardEvents, lastEventTimestamp] + ); + const mergedEvents = useMemo( () => [ + // Dropped here and re-added below from the enriched (tickets, votes) + // and superset (rewards) lists, so they are not listed twice. ...events.filter( (e) => e?.__typename !== "WinningTicketRedeemedEvent" && e?.__typename !== "TreasuryVoteEvent" && - e?.__typename !== "VoteEvent" + e?.__typename !== "VoteEvent" && + e?.__typename !== "RewardEvent" ), ...ticketEvents, + ...rewardEvents, ...extendedTreasuryVoteEventsData, ...extendedVoteEventsData, ].sort( @@ -191,6 +205,7 @@ const Index = () => { [ events, ticketEvents, + rewardEvents, extendedTreasuryVoteEventsData, extendedVoteEventsData, ] @@ -198,7 +213,8 @@ const Index = () => { const totalLoaded = Math.max( data?.transactions?.length ?? 0, - data?.winningTicketRedeemedEvents?.length ?? 0 + data?.winningTicketRedeemedEvents?.length ?? 0, + data?.rewardEvents?.length ?? 0 ); const fetchingRef = useRef(false); @@ -210,11 +226,19 @@ const Index = () => { try { await fetchMoreTransactions({ variables: { + // Shared skip: a shorter list is only over-skipped once exhausted. skip: totalLoaded, }, updateQuery: (previousResult, { fetchMoreResult }) => { if (!fetchMoreResult) return previousResult; - if (fetchMoreResult.transactions.length < PAGE_SIZE) + // Stop only once every list is exhausted. Rewards and ticket + // redemptions can be submitted by another address, so neither list is + // bounded by `transactions`. + if ( + fetchMoreResult.transactions.length < PAGE_SIZE && + fetchMoreResult.winningTicketRedeemedEvents.length < PAGE_SIZE && + fetchMoreResult.rewardEvents.length < PAGE_SIZE + ) setReachedEnd(true); return { @@ -223,13 +247,14 @@ const Index = () => { ...previousResult.transactions, ...fetchMoreResult.transactions, ], - // Basing the query skip for winning tickets on transactions.length is fine because there will always be more transactions than winning tickets - // So, we will always have winning ticket events that are older than the last transaction timestamp - // Allowing mergedEvents to filter correctly winningTicketRedeemedEvents: [ ...previousResult.winningTicketRedeemedEvents, ...fetchMoreResult.winningTicketRedeemedEvents, ], + rewardEvents: [ + ...previousResult.rewardEvents, + ...fetchMoreResult.rewardEvents, + ], }; }, }); @@ -276,7 +301,8 @@ const Index = () => { if ( !data?.transactions?.length && - !data?.winningTicketRedeemedEvents?.length + !data?.winningTicketRedeemedEvents?.length && + !data?.rewardEvents?.length ) { return No history; } diff --git a/queries/transactions.graphql b/queries/transactions.graphql index 36949a97..27644b88 100644 --- a/queries/transactions.graphql +++ b/queries/transactions.graphql @@ -132,4 +132,26 @@ query transactions($account: String!, $first: Int!, $skip: Int!) { id } } + # Keyed on the orchestrator (RewardEvent.delegate), not the transaction sender. + # Since LIP-118 a delegated reward caller may submit reward() on the + # orchestrator's behalf, so those calls are absent from the transactions query + # above, which filters on `from`. + rewardEvents( + orderBy: timestamp + orderDirection: desc + where: { delegate: $account } + first: $first + skip: $skip + ) { + __typename + id + round { + id + } + transaction { + id + timestamp + } + rewardTokens + } }