fix(trading): game results (#6083)

This commit is contained in:
Art 2024-03-25 17:25:50 +01:00 committed by GitHub
parent 0ada3a2f8d
commit 91d00a0520
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 316 additions and 179 deletions

View File

@ -283,101 +283,104 @@ const Games = ({
};
return (
<Table
columns={[
{
name: 'epoch',
displayName: t('Epoch'),
},
{
name: 'endtime',
displayName: t('End time'),
},
{ name: 'type', displayName: t('Type') },
{
name: 'asset',
displayName: t('Reward asset'),
},
{ name: 'daily', displayName: t('Daily reward amount') },
{ name: 'rank', displayName: t('Rank') },
{ name: 'amount', displayName: t('Amount earned this epoch') },
{ name: 'total', displayName: t('Cumulative amount earned') },
{
name: 'participatingTeams',
displayName: t('No. of participating teams'),
},
{
name: 'participatingMembers',
displayName: t('No. of participating members'),
},
].map((c) => ({ ...c, headerClassName: 'text-left' }))}
data={games.map((game) => {
let transfer = transfers?.find((t) => {
if (!isScopedToTeams(t)) return false;
<div className="text-sm">
<Table
columns={[
{
name: 'epoch',
displayName: t('Epoch'),
},
{
name: 'endtime',
displayName: t('End time'),
},
{ name: 'type', displayName: t('Type') },
{
name: 'asset',
displayName: t('Reward asset'),
},
{ name: 'daily', displayName: t('Daily reward amount') },
{ name: 'rank', displayName: t('Rank') },
{ name: 'amount', displayName: t('Amount earned this epoch') },
{ name: 'total', displayName: t('Cumulative amount earned') },
{
name: 'participatingTeams',
displayName: t('No. of participating teams'),
},
{
name: 'participatingMembers',
displayName: t('No. of participating members'),
},
].map((c) => ({ ...c, headerClassName: 'text-left' }))}
data={games.map((game) => {
let transfer = transfers?.find((t) => {
if (!isScopedToTeams(t)) return false;
const idMatch = t.transfer.gameId === game.id;
const metricMatch =
t.transfer.kind.dispatchStrategy?.dispatchMetric ===
game.team.rewardMetric;
const idMatch = t.transfer.gameId === game.id;
const metricMatch =
t.transfer.kind.dispatchStrategy?.dispatchMetric ===
game.team.rewardMetric;
const start = t.transfer.kind.startEpoch <= game.epoch;
const end = t.transfer.kind.endEpoch
? t.transfer.kind.endEpoch >= game.epoch
: true;
const start = t.transfer.kind.startEpoch <= game.epoch;
const end = t.transfer.kind.endEpoch
? t.transfer.kind.endEpoch >= game.epoch
: true;
const rejected = t.transfer.status === TransferStatus.STATUS_REJECTED;
const rejected =
t.transfer.status === TransferStatus.STATUS_REJECTED;
return idMatch && metricMatch && start && end && !rejected;
});
if (!transfer || !isScopedToTeams(transfer)) transfer = undefined;
const asset = transfer?.transfer.asset;
return idMatch && metricMatch && start && end && !rejected;
});
if (!transfer || !isScopedToTeams(transfer)) transfer = undefined;
const asset = transfer?.transfer.asset;
const dailyAmount =
asset && transfer
const dailyAmount =
asset && transfer
? addDecimalsFormatNumberQuantum(
transfer.transfer.amount,
asset.decimals,
asset.quantum
)
: '-';
const earnedAmount = asset
? addDecimalsFormatNumberQuantum(
transfer.transfer.amount,
game.team.rewardEarned,
asset.decimals,
asset.quantum
)
: '-';
const earnedAmount = asset
? addDecimalsFormatNumberQuantum(
game.team.rewardEarned,
asset.decimals,
asset.quantum
)
: '-';
const totalAmount = asset
? addDecimalsFormatNumberQuantum(
game.team.totalRewardsEarned,
asset.decimals,
asset.quantum
)
: '-';
const totalAmount = asset
? addDecimalsFormatNumberQuantum(
game.team.totalRewardsEarned,
asset.decimals,
asset.quantum
)
: '-';
const assetSymbol = asset ? <RewardAssetCell asset={asset} /> : '-';
const assetSymbol = asset ? <RewardAssetCell asset={asset} /> : '-';
return {
id: game.id,
amount: dependable(earnedAmount),
asset: dependable(assetSymbol),
daily: dependable(dailyAmount),
endtime: <EndTimeCell epoch={game.epoch} />,
epoch: game.epoch,
participatingMembers: game.numberOfParticipants,
participatingTeams: game.entities.length,
rank: game.team.rank,
total: totalAmount,
// type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
type: dependable(
<GameTypeCell transfer={transfer} allMarkets={allMarkets} />
),
};
})}
noCollapse={false}
/>
return {
id: game.id,
amount: dependable(earnedAmount),
asset: dependable(assetSymbol),
daily: dependable(dailyAmount),
endtime: <EndTimeCell epoch={game.epoch} />,
epoch: game.epoch,
participatingMembers: game.numberOfParticipants,
participatingTeams: game.entities.length,
rank: game.team.rank,
total: totalAmount,
// type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
type: dependable(
<GameTypeCell transfer={transfer} allMarkets={allMarkets} />
),
};
})}
noCollapse={false}
/>
</div>
);
};
@ -419,24 +422,26 @@ const Members = ({ members }: { members?: Member[] }) => {
);
return (
<Table
columns={[
{ name: 'referee', displayName: t('Member') },
{ name: 'rewards', displayName: t('Rewards earned') },
{ name: 'volume', displayName: t('Total volume') },
{ name: 'gamesPlayed', displayName: t('Games played') },
{
name: 'joinedAt',
displayName: t('Joined at'),
},
{
name: 'joinedAtEpoch',
displayName: t('Joined epoch'),
},
]}
data={data}
noCollapse={true}
/>
<div className="text-sm">
<Table
columns={[
{ name: 'referee', displayName: t('Member') },
{ name: 'rewards', displayName: t('Rewards earned') },
{ name: 'volume', displayName: t('Total volume') },
{ name: 'gamesPlayed', displayName: t('Games played') },
{
name: 'joinedAt',
displayName: t('Joined at'),
},
{
name: 'joinedAtEpoch',
displayName: t('Joined epoch'),
},
]}
data={data}
noCollapse={true}
/>
</div>
);
};
@ -503,7 +508,7 @@ const EndTimeCell = ({ epoch }: { epoch?: number }) => {
variables: {
epochId: epoch ? epoch.toString() : undefined,
},
fetchPolicy: 'cache-and-network',
fetchPolicy: 'cache-first',
});
if (loading) return <Loader size="small" />;

View File

@ -3,7 +3,10 @@ import { type EnrichedRewardTransfer } from '../../lib/hooks/use-rewards';
import { useVegaWallet } from '@vegaprotocol/wallet-react';
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
import { useMyTeam } from '../../lib/hooks/use-my-team';
import { ActiveRewardCard } from '../rewards-container/reward-card';
import {
ActiveRewardCard,
areAllMarketsSettled,
} from '../rewards-container/reward-card';
import {
VegaIcon,
VegaIconNames,
@ -127,6 +130,9 @@ export const GamesContainer = ({
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{data
.filter((n) => applyFilter(n, filter))
// filter out the cards (rewards) for which all of the markets
// are settled
.filter((n) => !areAllMarketsSettled(n))
.map((game, i) => {
// TODO: Remove `kind` prop from ActiveRewardCard
const { transfer } = game;

View File

@ -18,7 +18,7 @@ import { useRewards } from '../../lib/hooks/use-rewards';
import { useMyTeam } from '../../lib/hooks/use-my-team';
import { useVegaWallet } from '@vegaprotocol/wallet-react';
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
import { ActiveRewardCard } from './reward-card';
import { ActiveRewardCard, areAllMarketsSettled } from './reward-card';
export type Filter = {
searchTerm: string;
@ -122,6 +122,9 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
<div className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(335px,_1fr))] pr-2">
{data
.filter((n) => applyFilter(n, filter))
// filter out the cards (rewards) for which all of the markets
// are settled
.filter((n) => !areAllMarketsSettled(n))
.map((node, i) => (
<ActiveRewardCard
key={i}

View File

@ -925,17 +925,8 @@ const EntityIcon = ({
);
};
export const ActiveRewardCard = ({
transferNode,
currentEpoch,
requirements,
}: {
transferNode: EnrichedRewardTransfer;
currentEpoch: number;
requirements?: Requirements;
}) => {
// don't display the cards that are scoped to not trading markets
const marketSettled = transferNode.markets?.filter(
export const areAllMarketsSettled = (transferNode: EnrichedRewardTransfer) => {
const settledMarkets = transferNode.markets?.filter(
(m) =>
m?.state &&
[
@ -946,20 +937,40 @@ export const ActiveRewardCard = ({
].includes(m.state)
);
return (
settledMarkets?.length === transferNode.markets?.length &&
Boolean(transferNode.markets && transferNode.markets.length > 0)
);
};
export const areAllMarketsSuspended = (
transferNode: EnrichedRewardTransfer
) => {
return (
transferNode.markets?.filter(
(m) =>
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
).length === transferNode.markets?.length &&
Boolean(transferNode.markets && transferNode.markets.length > 0)
);
};
export const ActiveRewardCard = ({
transferNode,
currentEpoch,
requirements,
}: {
transferNode: EnrichedRewardTransfer;
currentEpoch: number;
requirements?: Requirements;
}) => {
const startsIn = transferNode.transfer.kind.startEpoch - currentEpoch;
const endsIn =
transferNode.transfer.kind.endEpoch != null
? transferNode.transfer.kind.endEpoch - currentEpoch
: undefined;
// hide the card if all of the markets are being marked as e.g. settled
if (
marketSettled?.length === transferNode.markets?.length &&
Boolean(transferNode.markets && transferNode.markets.length > 0)
) {
return null;
}
if (
!transferNode.transfer.kind.dispatchStrategy &&
transferNode.transfer.toAccountType ===
@ -987,17 +998,21 @@ export const ActiveRewardCard = ({
transferNode.transfer.kind.dispatchStrategy.dispatchMetric
];
// grey out of any of the markets is suspended or
// if the asset is not currently traded on any of the active markets
const marketSuspended =
transferNode.markets?.filter(
(m) =>
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
).length === transferNode.markets?.length &&
Boolean(transferNode.markets && transferNode.markets.length > 0);
if (marketSuspended || !transferNode.isAssetTraded || startsIn > 0) {
/**
* Display the card as grey if any of the condition is `true`:
*
* - all markets scoped to the reward are settled
* - all markets scoped to the reward are suspended
* - the reward's asset is not actively traded on any of the active markets
* - it start in the future
*
*/
if (
areAllMarketsSettled(transferNode) ||
areAllMarketsSuspended(transferNode) ||
!transferNode.isAssetTraded ||
startsIn > 0
) {
colour = CardColour.GREY;
}

View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "certifi"
@ -1166,7 +1166,7 @@ profile = ["pytest-profiling", "snakeviz"]
type = "git"
url = "https://github.com/vegaprotocol/vega-market-sim.git/"
reference = "HEAD"
resolved_reference = "e48b43322286b94680c4b0b94d5fe3bb2589c50c"
resolved_reference = "c7685cf9eacd829dceab34f9ebe79c125b127a97"
[[package]]
name = "websocket-client"

View File

@ -25,9 +25,10 @@ fragment GameFields on Game {
}
}
query Games($epochFrom: Int, $teamId: ID) {
query Games($epochFrom: Int, $epochTo: Int, $teamId: ID) {
games(
epochFrom: $epochFrom
epochTo: $epochTo
teamId: $teamId
entityScope: ENTITY_SCOPE_TEAMS
) {

View File

@ -9,6 +9,7 @@ export type GameFieldsFragment = { __typename?: 'Game', id: string, epoch: numbe
export type GamesQueryVariables = Types.Exact<{
epochFrom?: Types.InputMaybe<Types.Scalars['Int']>;
epochTo?: Types.InputMaybe<Types.Scalars['Int']>;
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
@ -45,8 +46,13 @@ export const GameFieldsFragmentDoc = gql`
}
${TeamEntityFragmentDoc}`;
export const GamesDocument = gql`
query Games($epochFrom: Int, $teamId: ID) {
games(epochFrom: $epochFrom, teamId: $teamId, entityScope: ENTITY_SCOPE_TEAMS) {
query Games($epochFrom: Int, $epochTo: Int, $teamId: ID) {
games(
epochFrom: $epochFrom
epochTo: $epochTo
teamId: $teamId
entityScope: ENTITY_SCOPE_TEAMS
) {
edges {
node {
...GameFields
@ -69,6 +75,7 @@ export const GamesDocument = gql`
* const { data, loading, error } = useGamesQuery({
* variables: {
* epochFrom: // value for 'epochFrom'
* epochTo: // value for 'epochTo'
* teamId: // value for 'teamId'
* },
* });

View File

@ -1,13 +1,15 @@
import {
useGamesQuery,
type GameFieldsFragment,
type TeamEntityFragment,
GamesDocument,
type GamesQuery,
} from './__generated__/Games';
import orderBy from 'lodash/orderBy';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { isSafeInteger, removePaginationWrapper } from '@vegaprotocol/utils';
import { useEpochInfoQuery } from './__generated__/Epoch';
import { type ApolloError } from '@apollo/client';
import { useApolloClient, type ApolloError } from '@apollo/client';
import { TEAMS_STATS_EPOCHS } from './constants';
import { useEffect, useMemo, useState } from 'react';
const findTeam = (entities: GameFieldsFragment['entities'], teamId: string) => {
const team = entities.find(
@ -30,10 +32,68 @@ export const areTeamGames = (games?: Game[]): games is TeamGame[] =>
type GamesData = {
data?: Game[];
loading: boolean;
error?: ApolloError;
error?: Error | ApolloError;
};
export const useGames = (teamId?: string, epochFrom?: number): GamesData => {
const MAX_EPOCHS = 30;
/**
* Converts the given variables (`teamId`, `epochFrom`, `epochTo`) of
* `GamesQuery` into chunks so that the maximum difference between given
* `epochFrom` and `epochTo` is not greater than the limit of `MAX_EPOCHS`.
*
* Example: When `epochFrom == 1` and `epochTo == 59` this function should
* produce an array of variables consisting of two entries where:
* - 1st chunk: `epochFrom == 1` and `epochTo == 31`
* - 2nd chunk: `epochFrom == 32` and `epochTo == 59`
*/
const prepareVariables = (
teamId?: string,
epochFrom?: number,
epochTo?: number
) => {
let from = epochFrom;
const to = epochTo;
if (isSafeInteger(from) && from < 1) from = 1; // make sure it's not negative
let variables = [
{
teamId,
epochFrom: from,
epochTo: to,
},
];
if (isSafeInteger(from) && isSafeInteger(to)) {
// if the difference between "from" and "to" is greater than MAX_EPOCHS
// then we need to divide the variables into N chunks.
if (to - from > MAX_EPOCHS) {
const N = Math.ceil((to - from) / MAX_EPOCHS);
variables = Array(N)
.fill(null)
.map((_, i) => {
const segmentFrom = Number(from) + MAX_EPOCHS * i;
let segmentTo = Number(from) + MAX_EPOCHS * (i + 1) - 1;
if (segmentTo > to) segmentTo = to;
return {
teamId,
epochFrom: segmentFrom,
epochTo: segmentTo,
};
});
}
}
return variables;
};
export const useGames = (
teamId?: string,
epochFrom?: number,
epochTo?: number
): GamesData => {
const client = useApolloClient();
const {
data: epochData,
loading: epochLoading,
@ -42,35 +102,81 @@ export const useGames = (teamId?: string, epochFrom?: number): GamesData => {
skip: Boolean(epochFrom),
});
let from = epochFrom;
if (!from && epochData) {
from = Number(epochData.epoch.id) - TEAMS_STATS_EPOCHS;
if (from < 1) from = 1; // make sure it's not negative
}
const [games, setGames] = useState<Game[] | undefined>(undefined);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | ApolloError | undefined>(
undefined
);
const { data, loading, error } = useGamesQuery({
variables: {
epochFrom: from,
teamId: teamId,
},
skip: !from,
fetchPolicy: 'cache-and-network',
context: { isEnlargedTimeout: true },
});
const variables = useMemo(() => {
let from = epochFrom;
let to = epochTo;
const allGames = removePaginationWrapper(data?.games.edges);
const allOrScoped = allGames
.map((g) => ({
...g,
team: teamId ? findTeam(g.entities, teamId) : undefined,
}))
.filter((g) => {
// passthrough if not scoped to particular team
if (!teamId) return true;
return isTeamGame(g);
});
if (epochData?.epoch.id && !epochFrom) {
const currentEpoch = Number(epochData.epoch.id);
from = currentEpoch - TEAMS_STATS_EPOCHS;
to = currentEpoch;
}
const games = orderBy(allOrScoped, 'epoch', 'desc');
if (!from) return [];
return prepareVariables(teamId, from, to);
}, [epochData?.epoch.id, epochFrom, epochTo, teamId]);
/**
* Because of the games API limitation to alway return max up to 30 epochs
* worth of data (regardless of the actual span of given variables
* `epochFrom` and `epochTo`) we need to do a trick of asking for longer
* periods in a way of chunks that are then combined into one `games`.
*
* The code below uses the direct reference to the `ApolloClient` and runs
* N queries (see `prepareVariables` function) in order to obtain the whole
* set of data.
*/
useEffect(() => {
if (loading || games || variables.length === 0) return;
if (!loading) setLoading(true);
const processChunks = async () => {
const chunks = variables.map((v) =>
client
.query<GamesQuery>({
query: GamesDocument,
variables: v,
context: { isEnlargedTimeout: true },
})
.then(({ data, loading, error }) => ({ data, loading, error }))
.catch(() => {
/* NOOP */
})
);
try {
const results = await Promise.allSettled(chunks);
const games = results.reduce((all, r) => {
if (r.status === 'fulfilled' && r.value) {
const { data, error } = r.value;
if (error) setError(error);
const allGames = removePaginationWrapper(data?.games.edges);
const allOrScoped = allGames
.map((g) => ({
...g,
team: teamId ? findTeam(g.entities, teamId) : undefined,
}))
.filter((g) => {
// passthrough if not scoped to particular team
if (!teamId) return true;
return isTeamGame(g);
});
return [...all, ...allOrScoped];
}
return all;
}, [] as Game[]);
if (games.length > 0) setGames(orderBy(games, 'epoch', 'desc'));
} finally {
setLoading(false);
}
};
processChunks();
}, [client, games, loading, teamId, variables]);
return {
data: games,

View File

@ -7187,14 +7187,6 @@ export type UpdateReferralProgram = {
windowLength: Scalars['Int'];
};
export type UpdateSpotInstrumentConfiguration = {
__typename?: 'UpdateSpotInstrumentConfiguration';
/** Instrument code, human-readable shortcode used to describe the instrument. */
code: Scalars['String'];
/** Instrument name */
name: Scalars['String'];
};
/** Update an existing spot market on Vega */
export type UpdateSpotMarket = {
__typename?: 'UpdateSpotMarket';
@ -7206,8 +7198,6 @@ export type UpdateSpotMarket = {
export type UpdateSpotMarketConfiguration = {
__typename?: 'UpdateSpotMarketConfiguration';
/** Updated spot market instrument configuration. */
instrument: UpdateSpotInstrumentConfiguration;
/** Specifies how the liquidity fee for the market will be calculated */
liquidityFeeSettings?: Maybe<LiquidityFeeSettings>;
/** Specifies the liquidity provision SLA parameters */

View File

@ -302,3 +302,7 @@ export const toQUSD = (
const qUSD = value.dividedBy(q);
return qUSD;
};
export const isSafeInteger = (x: unknown): x is number => {
return x != null && typeof x === 'number' && Number.isSafeInteger(x);
};