Feat/1924: Reward details API queries refactor (#1966)

* Feat/1924: Reward details API queries refactor. Also now shows rewards of any asset type

* Lint fix
This commit is contained in:
Sam Keen 2022-11-07 17:46:35 +00:00 committed by GitHub
parent 113eb90469
commit 927b2a86d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 224 additions and 441 deletions

View File

@ -1,34 +1,43 @@
fragment RewardFields on Reward {
rewardType
asset {
id
symbol
}
party {
id
}
epoch {
id
}
amount
amountFormatted @client
percentageOfTotal
receivedAt
}
fragment DelegationFields on Delegation {
amount
amountFormatted @client
epoch
}
query Rewards($partyId: ID!) {
party(id: $partyId) {
id
rewardDetails {
asset {
id
symbol
rewardsConnection {
edges {
node {
...RewardFields
}
}
rewards {
rewardType
asset {
id
}
party {
id
}
epoch {
id
}
amount
amountFormatted @client
percentageOfTotal
receivedAt
}
totalAmount
totalAmountFormatted @client
}
delegations {
amount
amountFormatted @client
epoch
delegationsConnection {
edges {
node {
...DelegationFields
}
}
}
}
epoch {

View File

@ -1,174 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: Rewards
// ====================================================
export interface Rewards_party_rewardDetails_asset {
__typename: "Asset";
/**
* The ID of the asset
*/
id: string;
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
}
export interface Rewards_party_rewardDetails_rewards_asset {
__typename: "Asset";
/**
* The ID of the asset
*/
id: string;
}
export interface Rewards_party_rewardDetails_rewards_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
}
export interface Rewards_party_rewardDetails_rewards_epoch {
__typename: "Epoch";
/**
* Numeric sequence number used to identify the epoch
*/
id: string;
}
export interface Rewards_party_rewardDetails_rewards {
__typename: "Reward";
/**
* The type of reward
*/
rewardType: AccountType;
/**
* The asset this reward is paid in
*/
asset: Rewards_party_rewardDetails_rewards_asset;
/**
* Party receiving the reward
*/
party: Rewards_party_rewardDetails_rewards_party;
/**
* Epoch for which this reward was distributed
*/
epoch: Rewards_party_rewardDetails_rewards_epoch;
/**
* Amount received for this reward
*/
amount: string;
/**
* The amount field formatted by the client
*/
amountFormatted: string;
/**
* Percentage out of the total distributed reward
*/
percentageOfTotal: string;
/**
* Time at which the rewards was received
*/
receivedAt: string;
}
export interface Rewards_party_rewardDetails {
__typename: "RewardPerAssetDetail";
/**
* Asset in which the reward was paid
*/
asset: Rewards_party_rewardDetails_asset;
/**
* A list of rewards received for this asset
*/
rewards: (Rewards_party_rewardDetails_rewards | null)[] | null;
/**
* The total amount of rewards received for this asset.
*/
totalAmount: string;
/**
* The total amount field formatted by the client
*/
totalAmountFormatted: string;
}
export interface Rewards_party_delegations {
__typename: "Delegation";
/**
* Amount delegated
*/
amount: string;
/**
* The amount field formatted by the client
*/
amountFormatted: string;
/**
* Epoch of delegation
*/
epoch: number;
}
export interface Rewards_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
/**
* Return reward information
*/
rewardDetails: (Rewards_party_rewardDetails | null)[] | null;
delegations: Rewards_party_delegations[] | null;
}
export interface Rewards_epoch_timestamps {
__typename: "EpochTimestamps";
/**
* RFC3339 timestamp - Vega time of epoch start, null if not started
*/
start: string | null;
/**
* RFC3339 timestamp - Vega time of epoch end, null if not ended
*/
end: string | null;
/**
* RFC3339 timestamp - Vega time of epoch expiry
*/
expiry: string | null;
}
export interface Rewards_epoch {
__typename: "Epoch";
/**
* Numeric sequence number used to identify the epoch
*/
id: string;
/**
* Timestamps for start and end of epochs
*/
timestamps: Rewards_epoch_timestamps;
}
export interface Rewards {
/**
* An entity that is trading on the Vega network
*/
party: Rewards_party | null;
/**
* Get data for a specific epoch, if ID omitted it gets the current epoch. If the string is 'next', fetch the next epoch
*/
epoch: Rewards_epoch;
}
export interface RewardsVariables {
partyId: string;
}

View File

@ -3,46 +3,60 @@ import { Schema as Types } from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type RewardFieldsFragment = { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, amountFormatted: string, percentageOfTotal: string, receivedAt: string, asset: { __typename?: 'Asset', id: string, symbol: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } };
export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: string, amountFormatted: string, epoch: number };
export type RewardsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardDetails?: Array<{ __typename?: 'RewardPerAssetDetail', totalAmount: string, totalAmountFormatted: string, asset: { __typename?: 'Asset', id: string, symbol: string }, rewards?: Array<{ __typename?: 'Reward', rewardType: Types.AccountType, amount: string, amountFormatted: string, percentageOfTotal: string, receivedAt: string, asset: { __typename?: 'Asset', id: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } | null> | null } | null> | null, delegations?: Array<{ __typename?: 'Delegation', amount: string, amountFormatted: string, epoch: number }> | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: string | null, end?: string | null, expiry?: string | null } } };
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, amountFormatted: string, percentageOfTotal: string, receivedAt: string, asset: { __typename?: 'Asset', id: string, symbol: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, amountFormatted: string, epoch: number } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: string | null, end?: string | null, expiry?: string | null } } };
export const RewardFieldsFragmentDoc = gql`
fragment RewardFields on Reward {
rewardType
asset {
id
symbol
}
party {
id
}
epoch {
id
}
amount
amountFormatted @client
percentageOfTotal
receivedAt
}
`;
export const DelegationFieldsFragmentDoc = gql`
fragment DelegationFields on Delegation {
amount
amountFormatted @client
epoch
}
`;
export const RewardsDocument = gql`
query Rewards($partyId: ID!) {
party(id: $partyId) {
id
rewardDetails {
asset {
id
symbol
rewardsConnection {
edges {
node {
...RewardFields
}
}
rewards {
rewardType
asset {
id
}
party {
id
}
epoch {
id
}
amount
amountFormatted @client
percentageOfTotal
receivedAt
}
totalAmount
totalAmountFormatted @client
}
delegations {
amount
amountFormatted @client
epoch
delegationsConnection {
edges {
node {
...DelegationFields
}
}
}
}
epoch {
@ -54,7 +68,8 @@ export const RewardsDocument = gql`
}
}
}
`;
${RewardFieldsFragmentDoc}
${DelegationFieldsFragmentDoc}`;
/**
* __useRewardsQuery__

View File

@ -1,168 +1 @@
import { useQuery } from '@apollo/client';
import { Button, Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import { formatDistance } from 'date-fns';
// @ts-ignore No types available for duration-js
import Duration from 'duration-js';
import gql from 'graphql-tag';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { EpochCountdown } from '../../../components/epoch-countdown';
import { Heading } from '../../../components/heading';
import { SplashLoader } from '../../../components/splash-loader';
import {
AppStateActionType,
useAppState,
} from '../../../contexts/app-state/app-state-context';
import type { Rewards } from './__generated__/Rewards';
import { RewardInfo } from './reward-info';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
export const REWARDS_QUERY = gql`
query Rewards($partyId: ID!) {
party(id: $partyId) {
id
rewardDetails {
asset {
id
symbol
}
rewards {
rewardType
asset {
id
}
party {
id
}
epoch {
id
}
amount
amountFormatted @client
percentageOfTotal
receivedAt
}
totalAmount
totalAmountFormatted @client
}
delegations {
amount
amountFormatted @client
epoch
}
}
epoch {
id
timestamps {
start
end
expiry
}
}
}
`;
export const RewardsIndex = () => {
const { t } = useTranslation();
const { pubKey, pubKeys } = useVegaWallet();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { appDispatch } = useAppState();
const { data, loading, error } = useQuery<Rewards>(REWARDS_QUERY, {
variables: { partyId: pubKey },
skip: !pubKey,
});
const { params } = useNetworkParams([
NetworkParams.reward_asset,
NetworkParams.reward_staking_delegation_payoutDelay,
]);
const payoutDuration = React.useMemo(() => {
if (!params) {
return 0;
}
return new Duration(
params.reward_staking_delegation_payoutDelay
).milliseconds();
}, [params]);
if (error) {
return (
<section>
<p>{t('Something went wrong')}</p>
{error && <pre>{error.message}</pre>}
</section>
);
}
if (loading || !params) {
return (
<Splash>
<SplashLoader />
</Splash>
);
}
return (
<section className="rewards">
<Heading title={t('pageTitleRewards')} />
<p>{t('rewardsPara1')}</p>
<p>{t('rewardsPara2')}</p>
{payoutDuration ? (
<div className="my-8">
<Callout
title={t('rewardsCallout', {
duration: formatDistance(new Date(0), payoutDuration),
})}
headingLevel={3}
intent={Intent.Warning}
>
<p className="mb-0">{t('rewardsPara3')}</p>
</Callout>
</div>
) : null}
{!loading &&
data &&
!error &&
data.epoch.timestamps.start &&
data.epoch.timestamps.expiry && (
<section className="mb-8">
<EpochCountdown
// eslint-disable-next-line
id={data!.epoch.id}
startDate={new Date(data.epoch.timestamps.start)}
// eslint-disable-next-line
endDate={new Date(data.epoch.timestamps.expiry!)}
/>
</section>
)}
<section>
{pubKey && pubKeys?.length ? (
<RewardInfo
currVegaKey={pubKey}
data={data}
rewardAssetId={params.reward_asset}
/>
) : (
<div>
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
{t('connectVegaWallet')}
</Button>
</div>
)}
</section>
</section>
);
};
export * from './rewards-page';

View File

@ -1,4 +1,4 @@
import * as Sentry from '@sentry/react';
import compact from 'lodash/compact';
import { format } from 'date-fns';
import React from 'react';
import { useTranslation } from 'react-i18next';
@ -7,69 +7,53 @@ import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import { BigNumber } from '../../../lib/bignumber';
import { DATE_FORMAT_DETAILED } from '../../../lib/date-formats';
import type {
Rewards,
Rewards_party_delegations,
Rewards_party_rewardDetails_rewards,
} from './__generated__/Rewards';
RewardsQuery,
RewardFieldsFragment,
DelegationFieldsFragment,
} from './__generated___/Rewards';
interface RewardInfoProps {
data: Rewards | undefined;
data: RewardsQuery | undefined;
currVegaKey: string;
rewardAssetId: string;
}
export const RewardInfo = ({
data,
currVegaKey,
rewardAssetId,
}: RewardInfoProps) => {
export const RewardInfo = ({ data, currVegaKey }: RewardInfoProps) => {
const { t } = useTranslation();
// Create array of rewards per epoch
const vegaTokenRewards = React.useMemo(() => {
if (!data?.party || !data.party.rewardDetails?.length) return [];
const rewards = React.useMemo(() => {
if (!data?.party || !data.party.rewardsConnection?.edges?.length) return [];
const vegaTokenRewards = data.party.rewardDetails.find(
(r) => r?.asset.id === rewardAssetId
return (
compact(data.party.rewardsConnection.edges.map((edge) => edge?.node)) ||
[]
);
}, [data]);
// We only issue rewards as Vega tokens for now so there should only be one
// item in the rewardDetails array
if (!vegaTokenRewards) {
const rewardAssets = data.party.rewardDetails
.map((r) => r?.asset.symbol)
.join(', ');
Sentry.captureMessage(
`Could not find VEGA token rewards ${rewardAssets}`
);
const delegations = React.useMemo(() => {
if (!data?.party || !data.party.delegationsConnection?.edges?.length) {
return [];
}
if (!vegaTokenRewards?.rewards?.length) return [];
const sorted = Array.from(vegaTokenRewards.rewards).sort((a, b) => {
if (!a || !b) return 0;
if (a.epoch > b.epoch) return -1;
if (a.epoch < b.epoch) return 1;
return 0;
});
return sorted;
}, [data, rewardAssetId]);
return (
compact(
data.party.delegationsConnection.edges.map((edge) => edge?.node)
) || []
);
}, [data]);
return (
<div>
<p>
{t('Connected Vega key')}: {currVegaKey}
</p>
{vegaTokenRewards.length ? (
vegaTokenRewards.map((reward, i) => {
{rewards.length ? (
rewards.map((reward, i) => {
if (!reward) return null;
return (
<RewardTable
key={i}
reward={reward}
delegations={data?.party?.delegations || []}
delegations={delegations || []}
/>
);
})
@ -81,8 +65,8 @@ export const RewardInfo = ({
};
interface RewardTableProps {
reward: Rewards_party_rewardDetails_rewards;
delegations: Rewards_party_delegations[];
reward: RewardFieldsFragment;
delegations: DelegationFieldsFragment[] | [];
}
export const RewardTable = ({ reward, delegations }: RewardTableProps) => {

View File

@ -0,0 +1,116 @@
import { Button, Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import { formatDistance } from 'date-fns';
// @ts-ignore No types available for duration-js
import Duration from 'duration-js';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { EpochCountdown } from '../../../components/epoch-countdown';
import { Heading } from '../../../components/heading';
import { SplashLoader } from '../../../components/splash-loader';
import {
AppStateActionType,
useAppState,
} from '../../../contexts/app-state/app-state-context';
import { RewardInfo } from './reward-info';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
import { useRewardsQuery } from './__generated___/Rewards';
export const RewardsPage = () => {
const { t } = useTranslation();
const { pubKey, pubKeys } = useVegaWallet();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { appDispatch } = useAppState();
const { data, loading, error } = useRewardsQuery({
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const { params } = useNetworkParams([
NetworkParams.reward_staking_delegation_payoutDelay,
]);
const payoutDuration = React.useMemo(() => {
if (!params) {
return 0;
}
return new Duration(
params.reward_staking_delegation_payoutDelay
).milliseconds();
}, [params]);
if (error) {
return (
<section>
<p>{t('Something went wrong')}</p>
{error && <pre>{error.message}</pre>}
</section>
);
}
if (loading || !params) {
return (
<Splash>
<SplashLoader />
</Splash>
);
}
return (
<section className="rewards">
<Heading title={t('pageTitleRewards')} />
<p>{t('rewardsPara1')}</p>
<p>{t('rewardsPara2')}</p>
{payoutDuration ? (
<div className="my-8">
<Callout
title={t('rewardsCallout', {
duration: formatDistance(new Date(0), payoutDuration),
})}
headingLevel={3}
intent={Intent.Warning}
>
<p className="mb-0">{t('rewardsPara3')}</p>
</Callout>
</div>
) : null}
{!loading &&
data &&
!error &&
data.epoch.timestamps.start &&
data.epoch.timestamps.expiry && (
<section className="mb-8">
<EpochCountdown
// eslint-disable-next-line
id={data!.epoch.id}
startDate={new Date(data.epoch.timestamps.start)}
// eslint-disable-next-line
endDate={new Date(data.epoch.timestamps.expiry!)}
/>
</section>
)}
<section>
{pubKey && pubKeys?.length ? (
<RewardInfo currVegaKey={pubKey} data={data} />
) : (
<div>
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
{t('connectVegaWallet')}
</Button>
</div>
)}
</section>
</section>
);
};

View File

@ -1,11 +1,11 @@
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { RouteChildProps } from '..';
import { RewardsIndex } from './home';
import { RewardsPage } from './home';
const Rewards = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
return <RewardsIndex />;
return <RewardsPage />;
};
export default Rewards;