Compare commits

...
Author SHA1 Message Date
Matthew Russell 654dd1e7b0 fix(trading): stored state causing wrong chart data (#5928) 2024-03-05 18:17:18 +00:00
Matthew Russellandbwallacee 7ea7362a7d fix(trading): market into tab 24hr vol and price fix (#5910)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-03-04 19:08:42 +00:00
Art bbfe42ddb1 fix(governance): rewards moved to console notification (#5904) 2024-03-04 18:09:52 +00:00
Matthew Russell b163af3e8a fix(trading): 24hr price and volume in header (#5908) 2024-03-04 18:00:54 +00:00
Matthew Russell 82df401611 fix(governance): incorrect penalty information in validators table and page (#5896) 2024-03-01 22:22:17 +00:00
Matthew Russell 4733bc169c fix(governance): rewards pagination and columns (#5879) 2024-02-29 14:57:07 +00:00
m.ray 2b91aebc2a fix(trading): fees by market fix order (#5877) 2024-02-29 10:25:56 +00:00
m.rayandbwallacee 375f4da541 fix(trading): fix reward card asssets (#5888)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-02-29 09:21:35 +00:00
Matthew Russell bc413ed314 chore(trading): revert reuse vega work (#5883) 2024-02-28 16:57:42 +00:00
BenandMatthew Russell 2fab3daebd chore(trading): update vega binaries (#5873)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2024-02-27 23:24:28 +00:00
Art 2e07ada966 fix(governance): network paramater change in batch proposals (#5869) 2024-02-27 11:47:10 +01:00
ArtandDariusz Majcherczyk b81c4bc948 fix(governance): add multisig score to the validator page (#5845)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2024-02-27 11:46:48 +01:00
4f8d6bd876 fix(trading): banner for resume trading (#5849)
Co-authored-by: Art <artur@vegaprotocol.io>
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2024-02-26 16:17:08 +00:00
Ben 52e5a37da3 chore(trading): fix skipped tests (#5868) 2024-02-26 15:52:22 +00:00
Ben 22599673ec chore(trading): remove fixture usage (#5865) 2024-02-26 14:24:56 +00:00
Ben 3c6a806ad3 chore(trading): reuse vega (#5850) 2024-02-26 12:57:16 +00:00
Dexter Edwards 7101d49d1d chore(governance, trading): update links for new beta extension (#5858) 2024-02-26 12:57:09 +00:00
Art 72e0cb76aa feat(governance): batch proposal indicators, vote breakdown ui changes (#5857) 2024-02-26 09:48:29 +00:00
m.ray ca64516a52 fix(trading): lp data provider reload - updating book (#5846) 2024-02-23 17:08:22 +00:00
daro-majanddaniel1302 55d692ea6f test(governance): revert tests (#5808)
Co-authored-by: daniel1302 <daniel.1302@gmail.com>
2024-02-23 10:27:35 +01:00
Ben f235c03abe chore(trading): update e2e test readme (#5844) 2024-02-22 21:50:55 +01:00
Matthew Russell 546deb0e1c fix(trading): use same epochs for all games/stats (#5834) 2024-02-22 13:27:13 +00:00
Art 042919eca9 fix(governance): update market proposal diff (#5842) 2024-02-22 13:37:48 +01:00
Ben c4a56e0de3 chore(trading): fix-order-status-test (#5840) 2024-02-21 15:36:55 +00:00
Ben 3c3bfb7dac chore(trading): update e2e dependencies (#5831) 2024-02-21 13:51:30 +00:00
Ben 196ba78806 chore(trading): refactor tests (#5835) 2024-02-21 13:22:48 +00:00
Bartłomiej GłowniaandMatthew Russell 53ac2dadee feat(trading): support modified funding payment calc (#5780)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2024-02-20 19:08:29 +00:00
Matthew Russell e532f88daa fix(governance): market name component not showing correct instrument code (#5830) 2024-02-20 18:00:51 +00:00
Ben 7b06c05853 chore(trading): update to vega 74.3 (#5829) 2024-02-20 17:56:54 +00:00
Edd a92fe92778 fix(governance): fix willpassbylpvotecalculation (#5824) 2024-02-20 16:10:44 +00:00
Art b8725a7fa8 fix(trading): competitions font tweaks (#5828) 2024-02-20 16:17:02 +01:00
Art f556247e1a chore(trading): competitions texts (#5826) 2024-02-20 14:39:41 +00:00
Art 48d6be0adf fix(governance): batch proposal diff and proposal headers (#5825) 2024-02-20 15:04:11 +01:00
Art be6f395ce4 feat(trading): colourful rewards (#5812) 2024-02-20 13:20:35 +01:00
Edd 9a37572f51 fix(explorer): sizedelta in ammends displayed incorrectly' (#5816) 2024-02-20 09:24:25 +00:00
Edd 19fb406d49 feat(explorer): add treasury view (#5798) 2024-02-19 10:24:51 +00:00
Matthew Russell a2a04c57d2 fix(trading): add depth chart on mobile view (#5811) 2024-02-16 21:34:01 +00:00
180 changed files with 6032 additions and 3792 deletions
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
- name: Install dependencies
run: |
rm package.json
npm install --no-save @commitlint/cli @commitlint/config-conventional @commitlint/config-nx-scopes nx
npm install --no-save @commitlint/cli@16.3.0 @commitlint/config-conventional@18.6.1 @commitlint/config-nx-scopes@18.6.1 nx@17.1.2
- name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
run: echo "${{ github.event.pull_request.title }}" | npx @commitlint/cli@16.3.0 --config ./commitlint.config-ci.js
@@ -1,7 +1,7 @@
import { getNewAssetTxBody } from '../support/governance.functions';
context('Proposal page', { tags: '@smoke' }, function () {
describe('Verify elements on page', function () {
describe.skip('Verify elements on page', function () {
const proposalHeading = 'proposals-heading';
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
@@ -7,6 +7,7 @@ export type AssetBalanceProps = {
price: string;
showAssetLink?: boolean;
showAssetSymbol?: boolean;
rounded?: boolean;
};
/**
@@ -18,12 +19,17 @@ const AssetBalance = ({
price,
showAssetLink = true,
showAssetSymbol = false,
rounded = false,
}: AssetBalanceProps) => {
const { data: asset, loading } = useAssetDataProvider(assetId);
const label =
!loading && asset && asset.decimals
? addDecimalsFixedFormatNumber(price, asset.decimals)
? addDecimalsFixedFormatNumber(
price,
asset.decimals,
rounded ? 0 : undefined
)
: price;
return (
@@ -41,6 +41,7 @@ export const Header = () => {
Routes.ASSETS,
Routes.MARKETS,
Routes.GOVERNANCE,
Routes.TREASURY,
Routes.NETWORK_PARAMETERS,
Routes.GENESIS,
].map((n) => pages.find((r) => r.path === n))
@@ -32,9 +32,17 @@ export function getNameForParty(id: string, data?: ExplorerNodeNamesQuery) {
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
truncate?: boolean;
networkLabel?: string;
truncateLength?: number;
};
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
const PartyLink = ({
id,
truncate = false,
truncateLength = 4,
networkLabel = t('Network'),
...props
}: PartyLinkProps) => {
const { data } = useExplorerNodeNamesQuery();
const name = useMemo(() => getNameForParty(id, data), [data, id]);
const useName = name !== id;
@@ -44,7 +52,7 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
return (
<span className="font-mono" data-testid="network">
{t('Network')}
{networkLabel}
</span>
);
}
@@ -70,7 +78,11 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
{useName ? (
name
) : (
<Hash text={truncate ? truncateMiddle(id, 4, 4) : id} />
<Hash
text={
truncate ? truncateMiddle(id, truncateLength, truncateLength) : id
}
/>
)}
</Link>
</span>
@@ -175,6 +175,7 @@ describe('Amend order details', () => {
const res = renderExistingAmend('123', 1, amend);
expect(await res.findByText('New size')).toBeInTheDocument();
expect(await res.findByText('Size ±')).toBeInTheDocument();
});
it('Renders Reference if provided', async () => {
@@ -82,7 +82,7 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
{amend.sizeDelta && amend.sizeDelta !== '0' ? (
<div className="mb-12 md:mb-0">
<h2 className="text-dark mb-4 text-2xl font-bold">
{t('New size')}
{t('Size ±')}
</h2>
<h5
className={`mb-0 text-lg font-medium capitalize text-gray-500 ${getSideDeltaColour(
@@ -93,6 +93,16 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
</h5>
</div>
) : null}
{o && (
<div className="">
<h2 className="text-dark mb-4 text-2xl font-bold">
{t('New size')}
</h2>
<h5 className="mb-0 text-lg font-medium text-gray-500">
{o ? o.size : null}
</h5>
</div>
)}
{amend.price && amend.price !== '0' ? (
<div className="">
@@ -34,10 +34,7 @@ export function TransferStatusView({ status, loading }: TransferStatusProps) {
) : (
<>
<p className="leading-10 my-2">
<Icon
name={getIconForStatus(status)}
className={getColourForStatus(status)}
/>
<TransferStatusIcon status={status} />
</p>
<p className="leading-10 my-2">{TransferStatusMapping[status]}</p>
</>
@@ -47,6 +44,21 @@ export function TransferStatusView({ status, loading }: TransferStatusProps) {
);
}
interface TransferStatusIconProps {
status: TransferStatus;
}
export function TransferStatusIcon({ status }: TransferStatusIconProps) {
return (
<span title={TransferStatusMapping[status]}>
<Icon
name={getIconForStatus(status)}
className={getColourForStatus(status)}
/>
</span>
);
}
/**
* Simple mapping from status to icon name
* @param status TransferStatus
@@ -60,6 +72,8 @@ export function getIconForStatus(status: TransferStatus): IconName {
return IconNames.TICK;
case TransferStatus.STATUS_REJECTED:
return IconNames.CROSS;
case TransferStatus.STATUS_CANCELLED:
return IconNames.CROSS;
default:
return IconNames.TIME;
}
@@ -78,6 +92,8 @@ export function getColourForStatus(status: TransferStatus): string {
return 'text-green-500';
case TransferStatus.STATUS_REJECTED:
return 'text-red-500';
case TransferStatus.STATUS_CANCELLED:
return 'text-red-600';
default:
return 'text-yellow-500';
}
@@ -12,4 +12,5 @@ export const Routes = {
ORACLES: 'oracles',
NETWORK_PARAMETERS: 'network-parameters',
DISCLAIMER: 'disclaimer',
TREASURY: 'treasury',
};
@@ -30,6 +30,7 @@ import { PartyAccountsByAsset } from './parties/id/accounts';
import { Disclaimer } from './pages/disclaimer';
import { useFeatureFlags } from '@vegaprotocol/environment';
import RestrictedPage from './restricted';
import { NetworkTreasury } from './treasury';
export type Navigable = {
path: string;
@@ -229,6 +230,17 @@ export const useRouterConfig = () => {
]
: [];
const treasuryRoutes: Route[] = [
{
path: Routes.TREASURY,
handle: {
name: t('Treasury'),
text: t('Treasury'),
breadcrumb: () => <Link to={Routes.TREASURY}>{t('Treasury')}</Link>,
},
element: <NetworkTreasury />,
},
];
const validators: Route[] = featureFlags.EXPLORER_VALIDATORS
? [
{
@@ -358,6 +370,7 @@ export const useRouterConfig = () => {
...marketsRoutes,
...networkParametersRoutes,
...validators,
...treasuryRoutes,
],
},
{
@@ -0,0 +1,12 @@
query ExplorerTreasury {
assetsConnection(pagination: { last: 1000 }) {
edges {
node {
id
networkTreasuryAccount {
balance
}
}
}
}
}
@@ -0,0 +1,44 @@
query ExplorerTreasuryTransfers {
transfersConnection(
partyId: "network"
direction: ToOrFrom
pagination: { last: 200 }
) {
pageInfo {
hasNextPage
}
edges {
node {
transfer {
timestamp
from
amount
to
status
reason
toAccountType
fromAccountType
asset {
id
}
id
status
kind {
... on OneOffTransfer {
deliverOn
}
... on RecurringTransfer {
startEpoch
}
... on OneOffGovernanceTransfer {
deliverOn
}
... on RecurringGovernanceTransfer {
endEpoch
}
}
}
}
}
}
}
@@ -0,0 +1,52 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerTreasuryQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerTreasuryQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, networkTreasuryAccount?: { __typename?: 'AccountBalance', balance: string } | null } } | null> | null } | null };
export const ExplorerTreasuryDocument = gql`
query ExplorerTreasury {
assetsConnection(pagination: {last: 1000}) {
edges {
node {
id
networkTreasuryAccount {
balance
}
}
}
}
}
`;
/**
* __useExplorerTreasuryQuery__
*
* To run a query within a React component, call `useExplorerTreasuryQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerTreasuryQuery` 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 } = useExplorerTreasuryQuery({
* variables: {
* },
* });
*/
export function useExplorerTreasuryQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>(ExplorerTreasuryDocument, options);
}
export function useExplorerTreasuryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>(ExplorerTreasuryDocument, options);
}
export type ExplorerTreasuryQueryHookResult = ReturnType<typeof useExplorerTreasuryQuery>;
export type ExplorerTreasuryLazyQueryHookResult = ReturnType<typeof useExplorerTreasuryLazyQuery>;
export type ExplorerTreasuryQueryResult = Apollo.QueryResult<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>;
@@ -0,0 +1,84 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerTreasuryTransfersQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerTreasuryTransfersQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', timestamp: any, from: string, amount: string, to: string, status: Types.TransferStatus, reason?: string | null, toAccountType: Types.AccountType, fromAccountType: Types.AccountType, id: string, asset?: { __typename?: 'Asset', id: string } | null, kind: { __typename?: 'OneOffGovernanceTransfer', deliverOn?: any | null } | { __typename?: 'OneOffTransfer', deliverOn?: any | null } | { __typename?: 'RecurringGovernanceTransfer', endEpoch?: number | null } | { __typename?: 'RecurringTransfer', startEpoch: number } } } } | null> | null } | null };
export const ExplorerTreasuryTransfersDocument = gql`
query ExplorerTreasuryTransfers {
transfersConnection(
partyId: "network"
direction: ToOrFrom
pagination: {last: 200}
) {
pageInfo {
hasNextPage
}
edges {
node {
transfer {
timestamp
from
amount
to
status
reason
toAccountType
fromAccountType
asset {
id
}
id
status
kind {
... on OneOffTransfer {
deliverOn
}
... on RecurringTransfer {
startEpoch
}
... on OneOffGovernanceTransfer {
deliverOn
}
... on RecurringGovernanceTransfer {
endEpoch
}
}
}
}
}
}
}
`;
/**
* __useExplorerTreasuryTransfersQuery__
*
* To run a query within a React component, call `useExplorerTreasuryTransfersQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerTreasuryTransfersQuery` 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 } = useExplorerTreasuryTransfersQuery({
* variables: {
* },
* });
*/
export function useExplorerTreasuryTransfersQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>(ExplorerTreasuryTransfersDocument, options);
}
export function useExplorerTreasuryTransfersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>(ExplorerTreasuryTransfersDocument, options);
}
export type ExplorerTreasuryTransfersQueryHookResult = ReturnType<typeof useExplorerTreasuryTransfersQuery>;
export type ExplorerTreasuryTransfersLazyQueryHookResult = ReturnType<typeof useExplorerTreasuryTransfersLazyQuery>;
export type ExplorerTreasuryTransfersQueryResult = Apollo.QueryResult<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>;
@@ -0,0 +1,37 @@
// NOTE: These are a temporary measure, pulled from an old branch on console.
import { IconNames } from '@blueprintjs/icons';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { USDc } from './usdc';
import { Vega } from './vega';
import { USDt } from './usdt';
export interface AssetIconProps {
symbol: string;
}
/**
* A poorly implemented, limited support for asset icons.
*
* These are committed as 'deprecated' to discourage use outside the Treasury page. Rather
* than use this, a better approach would be to use source contract addresses to match assets.
* This will be done separately.
*
* @deprecated
*/
export function AssetIcon({ symbol }: AssetIconProps) {
const s = symbol.toLowerCase();
switch (s) {
case 'a4a16e250a09a86061ec83c2f9466fc9dc33d332f86876ee74b6f128a5cd6710': // mainnet
case 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d': // mainnet
return <USDc size={32} />;
case 'd1984e3d365faa05bcafbe41f50f90e3663ee7c0da22bb1e24b164e9532691b2': // mainnet
case 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55': // testnet
return <Vega size={32} />;
case 'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba': // mainnet
case 'ede4076aef07fd79502d14326c54ab3911558371baaf697a19d077f4f89de399': // testnet
return <USDt size={32} />;
default:
return <Icon name={IconNames.BANK_ACCOUNT} size={8} />;
}
}
@@ -0,0 +1,24 @@
/**
* See note in index.tsx. This component is intended as a placeholder for a
* better, more generic solution.
*
* @deprecated
*/
export const USDc = ({ size = 16 }: { size?: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 2000 2000">
<path
d="M1000 2000c554.17 0 1000-445.83 1000-1000S1554.17 0 1000 0 0 445.83 0 1000s445.83 1000 1000 1000z"
fill="#2775ca"
/>
<path
d="M1275 1158.33c0-145.83-87.5-195.83-262.5-216.66-125-16.67-150-50-150-108.34s41.67-95.83 125-95.83c75 0 116.67 25 137.5 87.5 4.17 12.5 16.67 20.83 29.17 20.83h66.66c16.67 0 29.17-12.5 29.17-29.16v-4.17c-16.67-91.67-91.67-162.5-187.5-170.83v-100c0-16.67-12.5-29.17-33.33-33.34h-62.5c-16.67 0-29.17 12.5-33.34 33.34v95.83c-125 16.67-204.16 100-204.16 204.17 0 137.5 83.33 191.66 258.33 212.5 116.67 20.83 154.17 45.83 154.17 112.5s-58.34 112.5-137.5 112.5c-108.34 0-145.84-45.84-158.34-108.34-4.16-16.66-16.66-25-29.16-25h-70.84c-16.66 0-29.16 12.5-29.16 29.17v4.17c16.66 104.16 83.33 179.16 220.83 200v100c0 16.66 12.5 29.16 33.33 33.33h62.5c16.67 0 29.17-12.5 33.34-33.33v-100c125-20.84 208.33-108.34 208.33-220.84z"
fill="#fff"
/>
<path
d="M787.5 1595.83c-325-116.66-491.67-479.16-370.83-800 62.5-175 200-308.33 370.83-370.83 16.67-8.33 25-20.83 25-41.67V325c0-16.67-8.33-29.17-25-33.33-4.17 0-12.5 0-16.67 4.16-395.83 125-612.5 545.84-487.5 941.67 75 233.33 254.17 412.5 487.5 487.5 16.67 8.33 33.34 0 37.5-16.67 4.17-4.16 4.17-8.33 4.17-16.66v-58.34c0-12.5-12.5-29.16-25-37.5zM1229.17 295.83c-16.67-8.33-33.34 0-37.5 16.67-4.17 4.17-4.17 8.33-4.17 16.67v58.33c0 16.67 12.5 33.33 25 41.67 325 116.66 491.67 479.16 370.83 800-62.5 175-200 308.33-370.83 370.83-16.67 8.33-25 20.83-25 41.67V1700c0 16.67 8.33 29.17 25 33.33 4.17 0 12.5 0 16.67-4.16 395.83-125 612.5-545.84 487.5-941.67-75-237.5-258.34-416.67-487.5-491.67z"
fill="#fff"
/>
</svg>
);
};
@@ -0,0 +1,20 @@
/**
* See note in index.tsx. This component is intended as a placeholder for a
* better, more generic solution.
*
* @deprecated
*/
export const USDt = ({ size = 16 }: { size?: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 339.43 295.27">
<path
fill="#50af95"
d="M62.15,1.45l-61.89,130a2.52,2.52,0,0,0,.54,2.94L167.95,294.56a2.55,2.55,0,0,0,3.53,0L338.63,134.4a2.52,2.52,0,0,0,.54-2.94l-61.89-130A2.5,2.5,0,0,0,275,0H64.45a2.5,2.5,0,0,0-2.3,1.45h0Z"
/>
<path
fill="#fff"
d="M191.19,144.8v0c-1.2.09-7.4,0.46-21.23,0.46-11,0-18.81-.33-21.55-0.46v0c-42.51-1.87-74.24-9.27-74.24-18.13s31.73-16.25,74.24-18.15v28.91c2.78,0.2,10.74.67,21.74,0.67,13.2,0,19.81-.55,21-0.66v-28.9c42.42,1.89,74.08,9.29,74.08,18.13s-31.65,16.24-74.08,18.12h0Zm0-39.25V79.68h59.2V40.23H89.21V79.68H148.4v25.86c-48.11,2.21-84.29,11.74-84.29,23.16s36.18,20.94,84.29,23.16v82.9h42.78V151.83c48-2.21,84.12-11.73,84.12-23.14s-36.09-20.93-84.12-23.15h0Zm0,0h0Z"
/>
</svg>
);
};
@@ -0,0 +1,28 @@
/**
* See note in index.tsx. This component is intended as a placeholder for a
* better, more generic solution.
*
* @deprecated
*/
export const Vega = ({ size = 16 }: { size?: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 42 42">
<rect width="42" height="42" rx="21" fill="black" />
<path d="M13 27.2726H16.4545V10H13V27.2726Z" fill="white" />
<path d="M25.667 23.8181H29.1215V10H25.667V23.8181Z" fill="white" />
<path d="M19.333 33.6059H22.7875V30.1514H19.333V33.6059Z" fill="white" />
<path
d="M22.7871 30.7271H26.2416V27.2726H22.7871V30.7271Z"
fill="white"
/>
<path
d="M29.1211 27.2726H31.9999V23.8181H29.1211V27.2726Z"
fill="white"
/>
<path
d="M16.4551 30.7271H19.3339V27.2726H16.4551V30.7271Z"
fill="white"
/>
</svg>
);
};
@@ -0,0 +1,171 @@
import type { DeepPartial } from '@apollo/client/utilities';
import { parseResultsToAccounts } from './network-accounts-table';
import {
ExplorerTreasuryDocument,
type ExplorerTreasuryQuery,
} from '../__generated__/Treasury';
import { render, screen } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { NetworkAccountsTable } from './network-accounts-table';
describe('parseResultsToAccounts', () => {
it('should return an array of non-zero treasury accounts', () => {
const data: DeepPartial<ExplorerTreasuryQuery> = {
assetsConnection: {
edges: [
{
node: {
id: 'asset1',
networkTreasuryAccount: {
balance: '100',
},
},
},
{
node: {
id: 'has0assets',
networkTreasuryAccount: {
balance: '0',
},
},
},
{
node: {
id: 'asset3',
networkTreasuryAccount: {
balance: '50',
},
},
},
{
node: {
id: 'hasnonetworktreasuryaccount',
},
},
],
},
};
const result = parseResultsToAccounts(data as ExplorerTreasuryQuery);
expect(result).toHaveLength(2);
expect(result).toEqual([
{
assetId: 'asset1',
balance: '100',
type: 'ACCOUNT_TYPE_NETWORK_TREASURY',
},
{
assetId: 'asset3',
balance: '50',
type: 'ACCOUNT_TYPE_NETWORK_TREASURY',
},
]);
});
it('should return an empty array if no non-zero accounts are found', () => {
const data: DeepPartial<ExplorerTreasuryQuery> = {
assetsConnection: {
edges: [
{
node: {
id: 'asset1',
networkTreasuryAccount: {
balance: '0',
},
},
},
{
node: {
id: 'asset2',
networkTreasuryAccount: {
balance: '0',
},
},
},
],
},
};
const result = parseResultsToAccounts(data as ExplorerTreasuryQuery);
expect(result).toHaveLength(0);
expect(result).toEqual([]);
});
it('should handle missing data', () => {
const result = parseResultsToAccounts(
undefined as unknown as ExplorerTreasuryQuery
);
expect(result).toHaveLength(0);
expect(result).toEqual([]);
});
});
describe('NetworkAccountsTable', () => {
const mockData: ExplorerTreasuryQuery = {
assetsConnection: {
edges: [
{
node: {
id: 'asset1',
networkTreasuryAccount: {
balance: '100',
},
},
},
{
node: {
id: 'asset2',
networkTreasuryAccount: {
balance: '50',
},
},
},
],
},
};
const mocks = [
{
request: {
query: ExplorerTreasuryDocument,
},
result: {
data: mockData,
},
},
];
it('should render network accounts (as many as match - often just 1)', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<NetworkAccountsTable />
</MemoryRouter>
</MockedProvider>
);
// Wait for the data to load
await screen.findByText('Loading...');
// Assert that the network accounts are rendered
expect(screen.getByText('asset1')).toBeInTheDocument();
expect(screen.getByText('asset2')).toBeInTheDocument();
});
it('should handle loading state', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<NetworkAccountsTable />
</MemoryRouter>
</MockedProvider>
);
// Assert that the loading state is rendered
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
});
@@ -0,0 +1,87 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import {
type ExplorerTreasuryQuery,
useExplorerTreasuryQuery,
} from '../__generated__/Treasury';
import AssetBalance from '../../../components/asset-balance/asset-balance';
import { AssetLink } from '../../../components/links';
import { useMemo } from 'react';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { AssetIcon } from './asset-icon';
import { type NonZeroAccount } from '../network-treasury';
import { AccountType } from '@vegaprotocol/types';
import { removePaginationWrapper } from '@vegaprotocol/utils';
export const NetworkAccountsTable = () => {
const { data, loading, error } = useExplorerTreasuryQuery({
// This needs to ignore error as old assets may no longer properly resolve
errorPolicy: 'ignore',
});
const { screenSize } = useScreenDimensions();
const shouldRound = useMemo(
() => ['xs', 'sm', 'md', 'lg'].includes(screenSize),
[screenSize]
);
return (
<AsyncRenderer
data={data}
loading={loading}
error={error}
render={(data) => {
const c = parseResultsToAccounts(data);
return (
<section className="md:flex md:flex-row flex-wrap">
{c.map((a) => (
<div className="basis-1/2 md:basis-1/4">
<div className="bg-white rounded overflow-hidden shadow-lg dark:bg-black dark:border-slate-500 dark:border">
<div className="text-center p-6 bg-gray-100 dark:bg-slate-900 border-b dark:border-slate-500">
<p className="flex justify-center">
<AssetIcon symbol={a.assetId} />
</p>
<p className="mt-3" data-testid="name">
<AssetLink assetId={a.assetId} />
</p>
</div>
<div className="text-center py-5" data-testid="balance">
<AssetBalance
assetId={a.assetId}
price={a.balance}
showAssetSymbol={true}
rounded={shouldRound}
/>
</div>
</div>
</div>
))}
</section>
);
}}
/>
);
};
export function parseResultsToAccounts(
data: ExplorerTreasuryQuery
): NonZeroAccount[] {
const nonZeroAccounts: NonZeroAccount[] = [];
if (data?.assetsConnection?.edges) {
const edges = removePaginationWrapper(data?.assetsConnection?.edges);
if (edges) {
edges.forEach((edge) => {
if (
edge.networkTreasuryAccount &&
edge.networkTreasuryAccount?.balance !== '0'
) {
nonZeroAccounts.push({
assetId: edge.id,
balance: edge.networkTreasuryAccount?.balance,
type: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
});
}
});
}
}
return nonZeroAccounts;
}
@@ -0,0 +1,262 @@
import { AccountType } from '@vegaprotocol/types';
import {
typeLabel,
getToAccountTypeLabel,
filterAccountTransfers,
} from './network-transfers-table';
import { render, screen } from '@testing-library/react';
import { NetworkTransfersTable } from './network-transfers-table';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import {
ExplorerTreasuryTransfersDocument,
type ExplorerTreasuryTransfersQuery,
} from '../__generated__/TreasuryTransfers';
import type { DeepPartial } from '@apollo/client/utilities';
describe('typeLabel', () => {
it('should return "Transfer" for "OneOffTransfer" kind', () => {
expect(typeLabel('OneOffTransfer')).toBe('Transfer');
});
it('should return "Transfer" for "RecurringTransfer" kind', () => {
expect(typeLabel('RecurringTransfer')).toBe('Transfer');
});
it('should return "Governance" for "OneOffGovernanceTransfer" kind', () => {
expect(typeLabel('OneOffGovernanceTransfer')).toBe('Governance');
});
it('should return "Governance" for "RecurringGovernanceTransfer" kind', () => {
expect(typeLabel('RecurringGovernanceTransfer')).toBe('Governance');
});
it('should return "Unknown" for unknown kind', () => {
expect(typeLabel()).toBe('Unknown');
expect(typeLabel('')).toBe('Unknown');
expect(typeLabel('InvalidKind')).toBe('Unknown');
});
});
describe('getToAccountTypeLabel', () => {
it('should return "Treasury" when type is ACCOUNT_TYPE_NETWORK_TREASURY', () => {
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_NETWORK_TREASURY)
).toBe('Treasury');
});
it('should return "Fees" when type is any of the fee account types', () => {
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE)
).toBe('Fees');
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_FEES_MAKER)).toBe(
'Fees'
);
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY)).toBe(
'Fees'
);
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES)
).toBe('Fees');
expect(
getToAccountTypeLabel(
AccountType.ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD
)
).toBe('Fees');
});
it('should return "Insurance" when type is ACCOUNT_TYPE_GLOBAL_INSURANCE', () => {
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_GLOBAL_INSURANCE)
).toBe('Insurance');
});
it('should return "Rewards" when type is any of the reward account types', () => {
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_GLOBAL_REWARD)).toBe(
'Rewards'
);
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING)
).toBe('Rewards');
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_VESTED_REWARDS)).toBe(
'Rewards'
);
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_VESTING_REWARDS)
).toBe('Rewards');
});
it('should return "Other" for any other type', () => {
expect(getToAccountTypeLabel(undefined)).toBe('Other');
expect(getToAccountTypeLabel('unknown' as AccountType)).toBe('Other');
});
});
describe('filterAccountTransfers', () => {
it('filters out transactions that are not to or from a treasury account', () => {
const data: DeepPartial<ExplorerTreasuryTransfersQuery> = {
transfersConnection: {
edges: [
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
fromAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
},
},
},
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
fromAccountType:
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
},
},
},
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
fromAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
},
},
},
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
fromAccountType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
},
},
},
],
},
};
const result = filterAccountTransfers(
data as ExplorerTreasuryTransfersQuery
);
expect(result).toHaveLength(3);
});
it('should return an empty array if no transfers match the filter', () => {
const data: DeepPartial<ExplorerTreasuryTransfersQuery> = {
transfersConnection: {
edges: [
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
fromAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
},
},
},
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
fromAccountType:
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
},
},
},
],
},
};
const result = filterAccountTransfers(
data as ExplorerTreasuryTransfersQuery
);
expect(result).toHaveLength(0);
});
});
describe('NetworkTransfersTable', () => {
it('renders table headers correctly', async () => {
const mocks = [
{
request: {
query: ExplorerTreasuryTransfersDocument,
},
result: {
data: {
transfersConnection: {
edges: [
{
node: {
transfer: {
id: '123',
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
fromAccountType:
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
amount: '100',
asset: {
id: '1',
},
timestamp: '2022-01-01T00:00:00Z',
from: 'network',
to: '7100a8a82ef45adb9efa070cc821c6c5c48172d6dc5f842431549490fe5897a0',
reason: '',
status: 'COMPLETED',
kind: {
__typename: 'OneOffGovernanceTransfer',
deliverOn: '123',
},
},
},
},
],
},
},
},
},
];
render(
<MockedProvider mocks={mocks} addTypename={true}>
<MemoryRouter>
<NetworkTransfersTable />
</MemoryRouter>
</MockedProvider>
);
expect(await screen.findByText('Amount')).toBeInTheDocument();
expect(screen.getByText('Asset')).toBeInTheDocument();
expect(screen.getByText('Age')).toBeInTheDocument();
expect(screen.getByText('From')).toBeInTheDocument();
expect(screen.getByText('To')).toBeInTheDocument();
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByTestId('from-account').textContent).toEqual('Treasury');
expect(screen.getByTestId('to-account').textContent).toEqual('7100…97a0');
expect(screen.getByTestId('transfer-kind').textContent).toEqual(
'Governance'
);
});
});
@@ -0,0 +1,253 @@
import { AsyncRenderer, Icon } from '@vegaprotocol/ui-toolkit';
import AssetBalance from '../../../components/asset-balance/asset-balance';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { AssetLink, PartyLink } from '../../../components/links';
import {
type ExplorerTreasuryTransfersQuery,
useExplorerTreasuryTransfersQuery,
} from '../__generated__/TreasuryTransfers';
import { TimeAgo } from '../../../components/time-ago';
import { TransferStatusIcon } from '../../../components/txs/details/transfer/blocks/transfer-status';
import { t } from '@vegaprotocol/i18n';
import { IconNames } from '@blueprintjs/icons';
import { useMemo } from 'react';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
export const colours = {
INCOMING: '!fill-vega-green-600 text-vega-green-600 mr-2',
OUTGOING: '!fill-vega-pink-600 text-vega-pink-600 mr-2',
};
export const theadClasses =
'py-2 border text-center bg-vega-light-150 dark:bg-vega-dark-150';
export function getToAccountTypeLabel(type?: AccountType): string {
switch (type) {
case AccountType.ACCOUNT_TYPE_NETWORK_TREASURY:
return t('Treasury');
case AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE:
case AccountType.ACCOUNT_TYPE_FEES_MAKER:
case AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY:
case AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES:
case AccountType.ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD:
return t('Fees');
case AccountType.ACCOUNT_TYPE_GLOBAL_INSURANCE:
return t('Insurance');
case AccountType.ACCOUNT_TYPE_GLOBAL_REWARD:
case AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION:
case AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES:
case AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES:
case AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES:
case AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS:
case AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN:
case AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY:
case AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING:
case AccountType.ACCOUNT_TYPE_VESTED_REWARDS:
case AccountType.ACCOUNT_TYPE_VESTING_REWARDS:
return t('Rewards');
default:
return t('Other');
}
}
export function typeLabel(kind?: string): string {
switch (kind) {
case 'OneOffTransfer':
case 'RecurringTransfer':
return t('Transfer');
case 'OneOffGovernanceTransfer':
case 'RecurringGovernanceTransfer':
return t('Governance');
default:
return t('Unknown');
}
}
export function filterAccountTransfers(data: ExplorerTreasuryTransfersQuery) {
return data.transfersConnection?.edges
?.filter((edge) => {
if (
edge?.node.transfer.toAccountType ===
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY
) {
return true;
} else if (
edge?.node.transfer.fromAccountType ===
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY
) {
return true;
}
return false;
})
.map((edge) => {
return edge?.node.transfer;
});
}
export const NetworkTransfersTable = () => {
const { data, loading, error } = useExplorerTreasuryTransfersQuery({
// This needs to ignore error as old assets may no longer properly resolve
errorPolicy: 'ignore',
});
const { screenSize } = useScreenDimensions();
const shouldRound = useMemo(
() => ['xs', 'sm', 'md', 'lg'].includes(screenSize),
[screenSize]
);
const shouldTruncate = useMemo(
() => ['xs', 'sm', 'md', 'lg', 'xl'].includes(screenSize),
[screenSize]
);
const shouldHideColumns = useMemo(
() => ['xs', 'sm'].includes(screenSize),
[screenSize]
);
return (
<section>
<AsyncRenderer
data={data}
loading={loading}
error={error}
render={(data) => {
const c = filterAccountTransfers(data);
if (!c) {
return null;
}
return (
<table className="table-fixed border-spacing-3">
<thead>
<tr>
<th className={theadClasses}>{t('Amount')}</th>
<th className={theadClasses}>{t('Asset')}</th>
<th className={theadClasses}>{t('Age')}</th>
<th className={theadClasses}>{t('From')}</th>
<th className={theadClasses}>{t('To')}</th>
<th
className={`${theadClasses} ${
shouldHideColumns ? 'hidden' : ''
}`}
>
{t('Status')}
</th>
<th
className={`${theadClasses} ${
shouldHideColumns ? 'hidden' : ''
}`}
>
{t('Type')}
</th>
</tr>
</thead>
<tbody>
{c.map((a) => {
const isIncoming =
a?.toAccountType ===
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY;
return (
<tr>
{a && a.amount && a.asset && (
<td
className={`px-2 py-1 border whitespace-nowrap text-right ${
isIncoming ? colours.INCOMING : colours.OUTGOING
}`}
title={a.amount}
>
{a &&
a.toAccountType ===
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY ? (
<Icon
name={IconNames.PLUS}
className={colours.INCOMING}
/>
) : (
<Icon
name={IconNames.MINUS}
className={colours.OUTGOING}
/>
)}
<AssetBalance
assetId={a.asset.id}
price={a.amount}
showAssetLink={false}
rounded={shouldRound}
/>
</td>
)}
<td className="px-2 py-1 border whitespace-nowrap">
{a && a.amount && a.asset && (
<AssetLink
assetId={a.asset.id}
showAssetSymbol={true}
/>
)}
</td>
<td className="px-2 py-1 border">
{a && a.timestamp && <TimeAgo date={a.timestamp} />}
</td>
<td
className="px-2 py-1 border"
data-testid="from-account"
>
{a && a.from && (
<PartyLink
id={a.from}
truncate={true}
truncateLength={shouldTruncate ? 4 : 15}
networkLabel={t('Treasury')}
/>
)}
</td>
<td className="px-2 py-1 border" data-testid="to-account">
{a && a.to && (
<PartyLink
id={a.to}
networkLabel={t('Treasury')}
truncate={true}
truncateLength={shouldTruncate ? 4 : 15}
/>
)}
{a && !a.to && (
<span
className="underline decoration-dotted"
title={AccountTypeMapping[a.toAccountType]}
>
{getToAccountTypeLabel(a.toAccountType)}
</span>
)}
</td>
<td
className={`px-2 py-1 border text-center ${
shouldHideColumns ? 'hidden' : ''
}`}
>
{a && a.status && (
<TransferStatusIcon status={a.status} />
)}
</td>
<td
className={`px-2 py-1 border ${
shouldHideColumns ? 'hidden' : ''
}`}
>
<span
className="underline decoration-dotted"
title={a?.kind.__typename}
data-testid="transfer-kind"
>
{a && typeLabel(a.kind.__typename)}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
);
}}
/>
</section>
);
};
@@ -0,0 +1 @@
export * from './network-treasury';
@@ -0,0 +1,28 @@
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { AccountType } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { RouteTitle } from '../../components/route-title';
import { NetworkAccountsTable } from './components/network-accounts-table';
import { NetworkTransfersTable } from './components/network-transfers-table';
export type NonZeroAccount = {
assetId: string;
balance: string;
type: AccountType;
};
export const NetworkTreasury = () => {
useDocumentTitle(['Network Treasury']);
return (
<section>
<RouteTitle data-testid="block-header">{t(`Treasury`)}</RouteTitle>
<div>
<NetworkAccountsTable />
</div>
<div className="mt-5">
<h2 className="text-3xl mb-2">{t('Transfers')}</h2>
<NetworkTransfersTable />
</div>
</section>
);
};
@@ -7,7 +7,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
describe('Links and buttons', function () {
it.skip('should have link for proposal page', function () {
it('should have link for proposal page', function () {
cy.getByTestId('home-proposals').within(() => {
cy.get('[href="/proposals"]')
.should('exist')
@@ -51,7 +51,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it.skip('should have external link for governance', function () {
it('should have external link for governance', function () {
cy.getByTestId('home-proposals').within(() => {
cy.getByTestId('external-link')
.should('have.attr', 'href')
@@ -59,7 +59,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it.skip('should have link for validator page', function () {
it('should have link for validator page', function () {
cy.getByTestId('home-validators').within(() => {
cy.get('[href="/validators"]')
.first()
@@ -68,7 +68,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it.skip('should have external link for validators', function () {
it('should have external link for validators', function () {
cy.getByTestId('home-validators').within(() => {
cy.getByTestId('external-link')
.should('have.attr', 'href')
@@ -79,29 +79,29 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it.skip('should have information on active nodes', function () {
it('should have information on active nodes', function () {
cy.getByTestId('node-information')
.first()
.should('contain.text', '2')
.should('contain.text', '1')
.and('contain.text', 'active nodes');
});
it.skip('should have information on consensus nodes', function () {
it('should have information on consensus nodes', function () {
cy.getByTestId('node-information')
.last()
.should('contain.text', '2')
.should('contain.text', '1')
.and('contain.text', 'consensus nodes');
});
it.skip('should contain link to specific validators', function () {
it('should contain link to specific validators', function () {
cy.getByTestId('validators')
.should('have.length', '2')
.should('have.length', '1')
.each(($validator) => {
cy.wrap($validator).find('a').should('have.attr', 'href');
});
});
it.skip('should have link for rewards page', function () {
it('should have link for rewards page', function () {
cy.getByTestId('home-rewards').within(() => {
cy.get('[href="/rewards"]')
.first()
@@ -110,7 +110,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it.skip('should have link for withdrawal page', function () {
it('should have link for withdrawal page', function () {
cy.getByTestId('home-vega-token').within(() => {
cy.get('[href="/token/withdraw"]')
.first()
@@ -132,7 +132,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
// 0006-NETW-003 0006-NETW-008 0006-NETW-009 0006-NETW-010 0006-NETW-012 0006-NETW-013 0006-NETW-017 0006-NETW-018 0006-NETW-019 0006-NETW-020
it.skip('should have option to switch to different network node', function () {
it('should have option to switch to different network node', function () {
cy.getByTestId('git-network-data').within(() => {
cy.getByTestId('link').click();
});
@@ -159,7 +159,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.getByTestId('node-url-custom').click({ force: true });
cy.get('input').should('exist');
cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('icon-cross').click();
cy.getByTestId('dialog-close').click();
});
it('should display eth data', function () {
@@ -33,12 +33,12 @@ context(
verifyTabHighlighted(navigation.proposals);
});
it.skip('should have GOVERNANCE header visible', function () {
it('should have GOVERNANCE header visible', function () {
verifyPageHeader('Proposals');
});
// 3002-PROP-023 3004-PMAC-002 3005-PASN-002 3006-PASC-002 3007-PNEC-002 3008-PFRO-003
it.skip('new proposal page should have button for link to more information on proposals', function () {
it('new proposal page should have button for link to more information on proposals', function () {
cy.getByTestId('new-proposal-link').click();
cy.url().should('include', '/proposals/propose/raw');
cy.contains('To see Explorer data on proposals visit').within(() => {
@@ -73,7 +73,7 @@ context(
navigateTo(navigation.proposals);
});
it.skip('should be able to see a working link for - find out more about Vega governance', function () {
it('should be able to see a working link for - find out more about Vega governance', function () {
// 3001-VOTE-001 // 3002-PROP-001
cy.getByTestId(proposalDocumentationLink)
.should('be.visible')
@@ -40,10 +40,10 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
verifyConnectedToPubKey();
});
it('Able to connect public key via wallet and view assets in wallet', function () {
it.skip('Able to connect public key via wallet and view assets in wallet', function () {
verifyConnectedToPubKey();
cy.getByTestId('currency-title', { timeout: 10000 })
.should('have.length.at.least', 4)
.should('have.length.at.least', 2)
.and('contain.text', 'USDC (fake)');
});
@@ -46,11 +46,11 @@ context('Validators Page - verify elements on page', function () {
// @ts-ignore clash between jest and cypress
describe('with wallets disconnected', { tags: '@smoke' }, function () {
it.skip('Should have validators tab highlighted', function () {
it('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators);
});
it.skip('Should have validators ON VEGA header visible', function () {
it('Should have validators ON VEGA header visible', function () {
verifyPageHeader('Validators');
});
@@ -192,7 +192,7 @@ context('Validators Page - verify elements on page', function () {
});
// 1002-STKE-006
it.skip('Should be able to see validator name', function () {
it('Should be able to see validator name', function () {
cy.getByTestId(validatorTitle).should('not.be.empty');
});
+4 -5
View File
@@ -14,7 +14,6 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
@@ -24,7 +23,7 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
#Test configuration variables
CYPRESS_FAIRGROUND=false
@@ -33,7 +32,7 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_GOVERNANCE_TRANSFERS=false
NX_GOVERNANCE_TRANSFERS=true
+1 -2
View File
@@ -15,12 +15,11 @@ NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit suppl
NX_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
+1 -2
View File
@@ -8,14 +8,13 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=#
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
-1
View File
@@ -10,7 +10,6 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4a9141bd@o286262.ingest.sentry.io/5882996
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.vega.community/api/v2/
-1
View File
@@ -9,7 +9,6 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
+1 -2
View File
@@ -5,7 +5,6 @@ NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","STAGNET1":"https:
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
@@ -13,7 +12,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
+1 -2
View File
@@ -9,7 +9,6 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
@@ -18,7 +17,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+1 -1
View File
@@ -14,7 +14,7 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
+3
View File
@@ -62,6 +62,9 @@ const cache: InMemoryCacheConfig = {
Account: {
keyFields: false,
},
Instrument: {
keyFields: ['code'],
},
Delegation: {
keyFields: false,
// Only get full updates
@@ -15,19 +15,16 @@ export const CollapsibleToggle = ({
dataTestId,
children,
}: CollapsibleToggleProps) => {
const classes = classnames(
'mb-4 transition-transform ease-in-out duration-300',
{
'rotate-180': toggleState,
}
);
const classes = classnames('transition-transform ease-in-out duration-300', {
'rotate-180': toggleState,
});
return (
<button
onClick={() => setToggleState(!toggleState)}
data-testid={dataTestId}
>
<div className="flex items-center gap-3">
<div className="flex items-baseline gap-3">
{children}
<div className={classes} data-testid="toggle-icon-wrapper">
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
@@ -1,7 +1,8 @@
import classNames from 'classnames';
import { type ReactNode } from 'react';
interface HeadingProps {
title?: string;
title?: ReactNode;
centerContent?: boolean;
marginTop?: boolean;
marginBottom?: boolean;
@@ -0,0 +1,44 @@
import {
useLinks,
DApp,
CONSOLE_REWARDS_PAGE,
} from '@vegaprotocol/environment';
import {
ExternalLink,
Intent,
NotificationBanner,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Trans } from 'react-i18next';
import { useMatch } from 'react-router-dom';
import Routes from '../../routes/routes';
import { type ReactNode } from 'react';
const ConsoleRewardsLink = ({ children }: { children: ReactNode }) => {
const consoleLink = useLinks(DApp.Console);
return (
<ExternalLink
href={consoleLink(CONSOLE_REWARDS_PAGE)}
className="underline inline-flex gap-1 items-center"
title="Rewards in Console"
>
<span>{children}</span>
<VegaIcon size={12} name={VegaIconNames.OPEN_EXTERNAL} />
</ExternalLink>
);
};
export const RewardsMovedNotification = () => {
const onRewardsPage = useMatch(Routes.REWARDS);
if (!onRewardsPage) return null;
return (
<NotificationBanner intent={Intent.Warning}>
<Trans
i18nKey="rewardsMovedNotification"
components={[<ConsoleRewardsLink>Console</ConsoleRewardsLink>]}
/>
</NotificationBanner>
);
};
@@ -10,6 +10,7 @@ import {
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
import { RewardsMovedNotification } from '../notifications/rewards-moved-notification';
interface AppLayoutProps {
children: ReactNode;
@@ -45,8 +46,10 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
const NotificationsContainer = () => {
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return (
<div data-testid="banners">
<RewardsMovedNotification />
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
@@ -4,7 +4,6 @@ import keyBy from 'lodash/keyBy';
import uniq from 'lodash/uniq';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ENV } from '../../config';
import noIcon from '../../images/token-no-icon.png';
import vegaBlack from '../../images/vega_black.png';
@@ -37,7 +36,6 @@ export const usePollForDelegations = () => {
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const client = useApolloClient();
const { delegationsPagination } = ENV;
const [delegations, setDelegations] = React.useState<
WalletDelegationFieldsFragment[]
>([]);
@@ -68,11 +66,9 @@ export const usePollForDelegations = () => {
query: DelegationsDocument,
variables: {
partyId: pubKey,
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
delegationsPagination: {
first: 50,
},
},
fetchPolicy: 'network-only',
})
@@ -236,14 +232,14 @@ export const usePollForDelegations = () => {
// will just continue to fail
clearInterval(interval);
});
}, 1000);
}, 20000);
}
return () => {
clearInterval(interval);
mounted = false;
};
}, [delegationsPagination, client, decimals, pubKey, t, vegaToken.address]);
}, [client, decimals, pubKey, t, vegaToken.address]);
return { delegations, currentStakeAvailable, delegatedNodes, accounts };
};
-1
View File
@@ -65,7 +65,6 @@ export const ENV = {
docsUrl: windowOrDefault('NX_VEGA_DOCS_URL'),
ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'),
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
delegationsPagination: windowOrDefault('NX_DELEGATIONS_PAGINATION'),
rest: windowOrDefault('NX_VEGA_REST_URL'),
addresses:
ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks],
@@ -20,6 +20,7 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({
multisigStatus: MultisigStatus.noNodes,
showMultisigStatusError: true,
zeroScoreNodes: [],
});
});
@@ -35,6 +36,7 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({
multisigStatus: MultisigStatus.correct,
showMultisigStatusError: false,
zeroScoreNodes: [],
});
});
@@ -50,6 +52,22 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({
multisigStatus: MultisigStatus.nodeNeedsRemoving,
showMultisigStatusError: true,
zeroScoreNodes: [
{
id: '1',
rewardScore: {
multisigScore: '0',
},
stakedTotal: '1000',
},
{
id: '2',
rewardScore: {
multisigScore: '0',
},
stakedTotal: '1000',
},
],
});
});
@@ -65,6 +83,15 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({
multisigStatus: MultisigStatus.nodeNeedsAdding,
showMultisigStatusError: true,
zeroScoreNodes: [
{
id: '1',
rewardScore: {
multisigScore: '0',
},
stakedTotal: '1000',
},
],
});
});
});
@@ -1,5 +1,8 @@
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch';
import type {
PreviousEpochQuery,
ValidatorNodeFragment,
} from '../routes/staking/__generated__/PreviousEpoch';
export enum MultisigStatus {
'correct' = 'correct',
@@ -17,12 +20,15 @@ export const getMultisigStatusInfo = (
previousEpochData?.epoch.validatorsConnection?.edges
);
const hasZero = allNodesInPreviousEpoch.some(
(node) => Number(node?.rewardScore?.multisigScore) === 0
);
const hasOne = allNodesInPreviousEpoch.some(
(node) => Number(node?.rewardScore?.multisigScore) === 1
);
const zeroScore = (node: ValidatorNodeFragment) =>
Number(node.rewardScore?.multisigScore) === 0;
const oneScore = (node: ValidatorNodeFragment) =>
Number(node.rewardScore?.multisigScore) === 1;
const hasZero = allNodesInPreviousEpoch.some(zeroScore);
const hasOne = allNodesInPreviousEpoch.some(oneScore);
const zeroScoreNodes = allNodesInPreviousEpoch.filter(zeroScore);
if (hasZero && hasOne) {
// If any individual node has 0 it means that node is missing from the multisig and needs to be added
@@ -38,5 +44,6 @@ export const getMultisigStatusInfo = (
return {
showMultisigStatusError: status !== MultisigStatus.correct,
multisigStatus: status,
zeroScoreNodes,
};
};
@@ -160,7 +160,7 @@ describe('Proposal header', () => {
screen.queryByTestId('proposal-description')
).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Update to market ID: MarketId'
'Update to market: MarketId'
);
});
@@ -36,6 +36,8 @@ import { type Proposal, type BatchProposal } from '../../types';
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import { MarketName } from '../proposal/market-name';
import { Indicator } from '../proposal/indicator';
const ProposalTypeTags = ({
proposal,
@@ -52,11 +54,8 @@ const ProposalTypeTags = ({
if (proposal.__typename === 'BatchProposal') {
return (
<div data-testid="proposal-type" className="flex gap-1">
{proposal.subProposals?.map((subProposal, i) => {
if (!subProposal?.terms) return null;
return <ProposalTypeTag key={i} terms={subProposal.terms} />;
})}
<div data-testid="proposal-type">
<ProposalInfoLabel variant="secondary">BatchProposal</ProposalInfoLabel>
</div>
);
}
@@ -138,50 +137,77 @@ const ProposalDetails = ({
);
}
case 'UpdateMarketState': {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(':marketId', terms.change.market.id)
);
return (
<span>
{featureFlags.UPDATE_MARKET_STATE &&
terms.change?.market?.id &&
terms.change.updateType ? (
<>
{t(terms.change.updateType)}:{' '}
{truncateMiddle(terms.change.market.id)}
<span>{t(terms.change.updateType)}: </span>
<span className="inline-flex gap-2">
<span className="break-all">
<MarketName marketId={terms.change.market.id} />
</span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={terms.change.market.id}
description={t('copyId')}
>
<button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center">
<Link
className="inline-block px-1"
to={marketPageLink}
target="_blank"
>
<VegaIcon
size={20}
name={VegaIconNames.OPEN_EXTERNAL}
/>
</Link>
</Tooltip>
</span>
</span>
</>
) : null}
</span>
);
}
case 'UpdateMarket': {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(':marketId', terms.change.marketId)
);
return (
<>
<span>{t('UpdateToMarket')}:</span>{' '}
<span>{t('UpdateToMarket')}: </span>
<span className="inline-flex items-start gap-2">
<span className="break-all">{terms.change.marketId} </span>
<span className="break-all">
<MarketName marketId={terms.change.marketId} />
</span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={terms.change.marketId}
description={t('copyToClipboard')}
description={t('copyId')}
>
<button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center">
<button
<Link
className="inline-block px-1"
onClick={() => {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(
':marketId',
// @ts-ignore ts doesn't like this field even though its already a string above???
terms.change.marketId
)
);
window.open(marketPageLink, '_blank');
}}
target="_blank"
to={marketPageLink}
>
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
</button>
</Link>
</Tooltip>
</span>
</span>
@@ -224,7 +250,7 @@ const ProposalDetails = ({
}}
components={{
// @ts-ignore children passed by i18next
lozenge: <Lozenge />,
lozenge: <Lozenge className="text-xs" />,
}}
/>
);
@@ -286,12 +312,18 @@ const ProposalDetails = ({
{proposal.subProposals.map((p, i) => {
if (!p?.terms) return null;
return (
<li key={i}>
<div>{renderDetails(p.terms)}</div>
<SubProposalStateText
state={proposal.state}
enactmentDatetime={p.terms.enactmentDatetime}
/>
<li
key={i}
className="grid grid-cols-[40px_minmax(0,1fr)] grid-rows-1 gap-3 items-center"
>
<Indicator indicator={i + 1} />
<span>
<div>{renderDetails(p.terms)}</div>
<SubProposalStateText
state={proposal.state}
enactmentDatetime={p.terms.enactmentDatetime}
/>
</span>
</li>
);
})}
@@ -3,16 +3,8 @@ import { useTranslation } from 'react-i18next';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import {
type BatchProposalFieldsFragment,
type ProposalFieldsFragment,
} from '../../__generated__/Proposals';
export const ProposalJson = ({
proposal,
}: {
proposal: ProposalFieldsFragment | BatchProposalFieldsFragment;
}) => {
export const ProposalJson = ({ proposal }: { proposal?: unknown }) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
@@ -26,7 +18,7 @@ export const ProposalJson = ({
<SubHeading title={t('proposalJson')} />
</CollapsibleToggle>
{showDetails && <SyntaxHighlighter data={proposal} />}
{showDetails && <SyntaxHighlighter size="smaller" data={proposal} />}
</section>
);
};
@@ -5,6 +5,11 @@ import {
} from './proposal-market-changes';
import type { JsonValue } from '../../../../components/json-diff';
jest.mock('../proposal/market-name.tsx', () => ({
...jest.requireActual('../proposal/market-name.tsx'),
MarketName: jest.fn(),
}));
describe('applyImmutableKeysFromEarlierVersion', () => {
it('returns an empty object if any argument is not an object or null', () => {
const earlierVersion: JsonValue = null;
@@ -57,21 +62,21 @@ describe('applyImmutableKeysFromEarlierVersion', () => {
describe('ProposalMarketChanges', () => {
it('renders correctly', () => {
const { getByTestId } = render(
<ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} />
);
expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
});
it('JsonDiff is not visible when showChanges is false', () => {
const { queryByTestId } = render(
<ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} />
);
expect(queryByTestId('json-diff')).not.toBeInTheDocument();
});
it('JsonDiff is visible when showChanges is true', async () => {
const { getByTestId } = render(
<ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} />
);
fireEvent.click(getByTestId('proposal-market-changes-toggle'));
expect(getByTestId('json-diff')).toBeInTheDocument();
@@ -1,14 +1,24 @@
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import get from 'lodash/get';
import { JsonDiff } from '../../../../components/json-diff';
import compact from 'lodash/compact';
import orderBy from 'lodash/orderBy';
import { JsonDiff, type JsonValue } from '../../../../components/json-diff';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import type { JsonValue } from '../../../../components/json-diff';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../../config';
import {
useFetchProposal,
useFetchProposals,
flatten,
isBatchProposalNode,
isSingleProposalNode,
type ProposalNode,
type SingleProposalData,
type SubProposalData,
} from '../proposal/proposal-utils';
import { MarketName } from '../proposal/market-name';
const immutableKeys = [
'decimalPlaces',
@@ -18,8 +28,8 @@ const immutableKeys = [
];
export const applyImmutableKeysFromEarlierVersion = (
earlierVersion: JsonValue,
updatedVersion: JsonValue
earlierVersion: unknown,
updatedVersion: unknown
) => {
if (
typeof earlierVersion !== 'object' ||
@@ -35,7 +45,8 @@ export const applyImmutableKeysFromEarlierVersion = (
// Overwrite the immutable keys in the updatedVersionCopy with the earlier values
immutableKeys.forEach((key) => {
set(updatedVersionCopy, key, get(earlierVersion, key));
const earlier = get(earlierVersion, key);
if (earlier) set(updatedVersionCopy, key, earlier);
});
return updatedVersionCopy;
@@ -43,49 +54,84 @@ export const applyImmutableKeysFromEarlierVersion = (
interface ProposalMarketChangesProps {
marketId: string;
updatedProposal: JsonValue;
/** This are the changes from proposal */
updateProposalNode: ProposalNode | null;
indicator?: number;
}
export const ProposalMarketChanges = ({
marketId,
updatedProposal,
updateProposalNode,
indicator,
}: ProposalMarketChangesProps) => {
const { t } = useTranslation();
const [showChanges, setShowChanges] = useState(false);
const {
state: { data },
} = useFetch(`${ENV.rest}governance?proposalId=${marketId}`, undefined, true);
const { data: originalProposalData } = useFetchProposal({
proposalId: marketId,
});
const {
state: { data: enactedProposalData },
} = useFetch(
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined,
true
const { data: enactedProposalsData } = useFetchProposals({
proposalState: 'STATE_ENACTED',
proposalType: 'TYPE_UPDATE_MARKET',
});
let updateProposal: SingleProposalData | SubProposalData | undefined;
if (isBatchProposalNode(updateProposalNode)) {
updateProposal = updateProposalNode.proposals.find(
(p, i) =>
p.terms.updateMarket?.marketId === marketId &&
(indicator != null ? i === indicator - 1 : true)
);
}
if (isSingleProposalNode(updateProposalNode)) {
updateProposal = updateProposalNode.proposal;
}
// this should get the proposal before the current one
const enactedUpdateMarketProposals = orderBy(
compact(
flatten(enactedProposalsData).filter((enacted) => {
const related = enacted.terms.updateMarket?.marketId === marketId;
const notCurrent =
enacted.id !== updateProposal?.id ||
('batchId' in enacted && enacted.batchId !== updateProposal.id);
const beforeCurrent =
Number(enacted.terms.enactmentTimestamp) <
Number(updateProposal?.terms.enactmentTimestamp);
return related && notCurrent && beforeCurrent;
})
),
[(proposal) => Number(proposal.terms.enactmentTimestamp)],
'desc'
);
// @ts-ignore no types here :-/
const enacted = enactedProposalData?.connection?.edges
.filter(
// @ts-ignore no type here
({ node }) => node?.proposal?.terms?.updateMarket?.marketId === marketId
)
// @ts-ignore no type here
.sort((a, b) => {
return (
new Date(a?.node?.terms?.enactmentTimestamp).getTime() -
new Date(b?.node?.terms?.enactmentTimestamp).getTime()
);
});
const latestEnactedProposal =
enactedUpdateMarketProposals.length > 0
? enactedUpdateMarketProposals[0]
: undefined;
const latestEnactedProposal = enacted?.length
? enacted[enacted.length - 1]
: undefined;
let originalProposal;
if (isBatchProposalNode(originalProposalData)) {
originalProposal = originalProposalData.proposals.find(
(proposal) => proposal.id === marketId && proposal.terms.newMarket != null
);
}
if (isSingleProposalNode(originalProposalData)) {
originalProposal = originalProposalData.proposal;
}
const originalProposal =
// @ts-ignore no types with useFetch TODO: check this is good
data?.data?.proposal?.terms?.newMarket?.changes;
// LEFT SIDE: update market proposal enacted just before this one
// or original new market proposal
const left =
latestEnactedProposal?.terms.updateMarket?.changes ||
originalProposal?.terms.newMarket?.changes;
// RIGHT SIDE: this update market proposal
const right = applyImmutableKeysFromEarlierVersion(
left,
updateProposal?.terms.updateMarket?.changes
);
return (
<section data-testid="proposal-market-changes">
@@ -94,25 +140,18 @@ export const ProposalMarketChanges = ({
setToggleState={setShowChanges}
dataTestId={'proposal-market-changes-toggle'}
>
<SubHeading title={t('updatesToMarket')} />
<SubHeading
title={
<>
{t('UpdateToMarket')}: <MarketName marketId={marketId} truncate />
</>
}
/>
</CollapsibleToggle>
{showChanges && (
<div className="mb-6">
<JsonDiff
left={latestEnactedProposal || originalProposal}
right={
latestEnactedProposal
? applyImmutableKeysFromEarlierVersion(
latestEnactedProposal,
updatedProposal
)
: applyImmutableKeysFromEarlierVersion(
originalProposal,
updatedProposal
)
}
/>
<div className="mb-6 bg-vega-cdark-900 p-2 rounded-lg">
<JsonDiff left={left as JsonValue} right={right as JsonValue} />
</div>
)}
</section>
@@ -2,6 +2,11 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { ProposalUpdateMarketState } from './proposal-update-market-state';
import { MarketUpdateType } from '@vegaprotocol/types';
jest.mock('../proposal/market-name.tsx', () => ({
...jest.requireActual('../proposal/market-name.tsx'),
MarketName: jest.fn(),
}));
describe('<ProposalUpdateMarketState />', () => {
const suspendProposal = {
__typename: 'UpdateMarketState' as const,
@@ -9,6 +9,8 @@ import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals';
import { MarketUpdateTypeMapping } from '@vegaprotocol/types';
import { MarketName } from '../proposal/market-name';
interface ProposalUpdateMarketStateProps {
change: UpdateMarketStatesFragment | null;
@@ -19,16 +21,18 @@ export const ProposalUpdateMarketState = ({
}: ProposalUpdateMarketStateProps) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
let market;
let isTerminate = false;
if (!change) {
if (!change || change.__typename !== 'UpdateMarketState') {
return null;
}
if (change.__typename === 'UpdateMarketState') {
market = change?.market;
isTerminate = change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE';
const market = change?.market;
const isTerminate =
change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE';
let toggleTitle = t(change.updateType);
if (toggleTitle.length === 0) {
toggleTitle = t('MarketDetails');
}
return (
@@ -38,7 +42,13 @@ export const ProposalUpdateMarketState = ({
setToggleState={setShowDetails}
dataTestId="proposal-market-data-toggle"
>
<SubHeading title={t('MarketDetails')} />
<SubHeading
title={
<>
{toggleTitle}: <MarketName marketId={market?.id} />
</>
}
/>
</CollapsibleToggle>
{showDetails && (
@@ -49,6 +59,12 @@ export const ProposalUpdateMarketState = ({
{t('marketId')}
{market?.id}
</KeyValueTableRow>
<KeyValueTableRow>
{t('State')}
<span className="bg-vega-green-650 px-1">
{MarketUpdateTypeMapping[change.updateType]}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('marketName')}
{market?.tradableInstrument?.instrument?.name}
@@ -0,0 +1,46 @@
import classNames from 'classnames';
// rainbow-ish order
const COLOURS = ['red', 'pink', 'orange', 'yellow', 'green', 'blue', 'purple'];
const getColour = (indicator: number, max = COLOURS.length) => {
const available =
max < COLOURS.length ? COLOURS.slice(COLOURS.length - max) : COLOURS;
const tiers = Object.keys(available).length;
let index = Math.abs(indicator - 1);
if (indicator >= tiers) {
index = index % tiers;
}
return available[index];
};
export const getStyle = (indicator: number, max = COLOURS.length) =>
classNames({
'bg-vega-yellow-400 before:bg-vega-yellow-400':
'yellow' === getColour(indicator, max),
'bg-vega-green-400 before:bg-vega-green-400':
'green' === getColour(indicator, max),
'bg-vega-blue-400 before:bg-vega-blue-400':
'blue' === getColour(indicator, max),
'bg-vega-purple-400 before:bg-vega-purple-400':
'purple' === getColour(indicator, max),
'bg-vega-pink-400 before:bg-vega-pink-400':
'pink' === getColour(indicator, max),
'bg-vega-orange-400 before:bg-vega-orange-400':
'orange' === getColour(indicator, max),
'bg-vega-red-400 before:bg-vega-red-400':
'red' === getColour(indicator, max),
'bg-vega-clight-600 before:bg-vega-clight-600':
'none' === getColour(indicator, max),
});
export const getIndicatorStyle = (indicator: number) =>
classNames(
'rounded-sm text-black inline-block px-1 py-1 font-alpha calt h-8 w-7 text-center',
'text-border-1',
getStyle(indicator),
// Comment below if you want to remove the "chevron"
'relative mr-[11px] z-1',
'before:absolute before:z-0 before:top-1 before:right-[-11px] before:rounded-sm',
"before:w-[22.62px] before:h-[22.62px] before:rotate-45 before:content-['']"
);
@@ -0,0 +1,9 @@
import { getIndicatorStyle } from './colours';
export const Indicator = ({ indicator }: { indicator: number }) => (
<div className={getIndicatorStyle(indicator)}>
<span className="absolute top-0 left-0 p-1 w-full text-center z-1">
{indicator}
</span>
</div>
);
@@ -0,0 +1,21 @@
import { useMarketInfoQuery } from '@vegaprotocol/markets';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
export const MarketName = ({
marketId,
truncate,
}: {
marketId?: string;
truncate?: boolean;
}) => {
const { data } = useMarketInfoQuery({
variables: {
marketId: marketId || '',
},
skip: !marketId,
});
const id = truncate ? truncateMiddle(marketId || '') : marketId;
return <span>{data?.market?.tradableInstrument.instrument.code || id}</span>;
};
@@ -1,3 +1,4 @@
import { Trans, useTranslation } from 'react-i18next';
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
import { type Proposal, type BatchProposal } from '../../types';
import { ListAsset } from '../list-asset';
@@ -12,21 +13,29 @@ import {
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { ProposalUpdateMarketState } from '../proposal-update-market-state';
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
import { type ProposalNode } from './proposal-utils';
import { Lozenge } from '@vegaprotocol/ui-toolkit';
import { Indicator } from './indicator';
import { SubHeading } from '../../../../components/heading';
export const ProposalChangeDetails = ({
proposal,
terms,
restData,
indicator,
}: {
proposal: Proposal | BatchProposal;
terms: ProposalTermsFieldsFragment;
// eslint-disable-next-line
restData: any;
restData: ProposalNode | null;
indicator?: number;
}) => {
const { t } = useTranslation();
let details = null;
switch (terms.change.__typename) {
case 'NewAsset': {
if (proposal.id && terms.change.source.__typename === 'ERC20') {
return (
details = (
<div>
<ListAsset
assetId={proposal.id}
@@ -37,62 +46,64 @@ export const ProposalChangeDetails = ({
</div>
);
}
return null;
break;
}
case 'UpdateAsset': {
if (proposal.id) {
return (
details = (
<ProposalAssetDetails
change={terms.change}
assetId={terms.change.assetId}
/>
);
}
return null;
break;
}
case 'NewMarket': {
if (proposal.id) {
return <ProposalMarketData proposalId={proposal.id} />;
details = <ProposalMarketData proposalId={proposal.id} />;
}
return null;
break;
}
case 'UpdateMarket': {
if (proposal.id) {
return (
details = (
<div className="flex flex-col gap-4">
<ProposalMarketData proposalId={proposal.id} />
<ProposalMarketChanges
indicator={indicator}
marketId={terms.change.marketId}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes
}
updateProposalNode={restData}
/>
</div>
);
}
return null;
break;
}
case 'NewTransfer': {
if (proposal.id) {
return <ProposalTransferDetails proposalId={proposal.id} />;
details = <ProposalTransferDetails proposalId={proposal.id} />;
}
return null;
break;
}
case 'CancelTransfer': {
if (proposal.id) {
return <ProposalCancelTransferDetails proposalId={proposal.id} />;
details = <ProposalCancelTransferDetails proposalId={proposal.id} />;
}
return null;
break;
}
case 'UpdateMarketState': {
return <ProposalUpdateMarketState change={terms.change} />;
details = <ProposalUpdateMarketState change={terms.change} />;
break;
}
case 'UpdateReferralProgram': {
return <ProposalReferralProgramDetails change={terms.change} />;
details = <ProposalReferralProgramDetails change={terms.change} />;
break;
}
case 'UpdateVolumeDiscountProgram': {
return <ProposalVolumeDiscountProgramDetails change={terms.change} />;
details = <ProposalVolumeDiscountProgramDetails change={terms.change} />;
break;
}
case 'UpdateNetworkParameter': {
if (
@@ -100,18 +111,48 @@ export const ProposalChangeDetails = ({
terms.change.networkParameter.key ===
'rewards.activityStreak.benefitTiers'
) {
return <ProposalUpdateBenefitTiers change={terms.change} />;
details = <ProposalUpdateBenefitTiers change={terms.change} />;
} else {
details = (
<div className="mb-4">
<SubHeading title={t(terms.change.__typename as string)} />
<span>
<Trans
i18nKey="Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>"
values={{
key: terms.change.networkParameter.key,
value: terms.change.networkParameter.value,
}}
components={{
// @ts-ignore children passed by i18next
lozenge: <Lozenge />,
}}
/>
</span>
</div>
);
}
return null;
break;
}
case 'NewFreeform':
case 'NewSpotMarket':
case 'UpdateSpotMarket': {
return null;
}
case 'UpdateSpotMarket':
default: {
return null;
break;
}
}
if (indicator != null && details != null) {
details = (
<div className="grid grid-cols-[40px_minmax(0,1fr)] grid-rows-1 gap-3 mb-3">
<div className="w-10">
<Indicator indicator={indicator} />
</div>
<div>{details}</div>
</div>
);
}
return details;
};
@@ -0,0 +1,261 @@
import compact from 'lodash/compact';
import { ENV } from '../../../../config';
import { useEffect, useState } from 'react';
type Maybe<T> = T | null | undefined;
type ProposalState =
| 'STATE_UNSPECIFIED'
| 'STATE_FAILED'
| 'STATE_OPEN'
| 'STATE_PASSED'
| 'STATE_REJECTED'
| 'STATE_DECLINED'
| 'STATE_ENACTED'
| 'STATE_WAITING_FOR_NODE_VOTE';
type ProposalType =
| 'TYPE_UNSPECIFIED'
| 'TYPE_ALL'
| 'TYPE_NEW_MARKET'
| 'TYPE_UPDATE_MARKET'
| 'TYPE_NETWORK_PARAMETERS'
| 'TYPE_NEW_ASSET'
| 'TYPE_NEW_FREE_FORM'
| 'TYPE_UPDATE_ASSET'
| 'TYPE_NEW_SPOT_MARKET'
| 'TYPE_UPDATE_SPOT_MARKET'
| 'TYPE_NEW_TRANSFER'
| 'TYPE_CANCEL_TRANSFER'
| 'TYPE_UPDATE_MARKET_STATE'
| 'TYPE_UPDATE_REFERRAL_PROGRAM'
| 'TYPE_UPDATE_VOLUME_DISCOUNT_PROGRAM';
type ProposalNodeType = 'TYPE_SINGLE_OR_UNSPECIFIED' | 'TYPE_BATCH';
type ProposalData = {
id: string;
rationale: {
description: string;
title: string;
};
state: ProposalState;
timestamp: string;
};
type Terms = {
cancelTransfer?: { changes: unknown };
enactmentTimestamp: string;
newAsset?: { changes: unknown };
newFreeform: object;
newMarket?: { changes: unknown };
newSpotMarket?: { changes: unknown };
newTransfer?: { changes: unknown };
updateAsset?: { assetId: string; changes: unknown };
updateMarket?: { marketId: string; changes: unknown };
updateMarketState?: {
changes: {
marketId: string;
price: string;
updateType:
| 'MARKET_STATE_UPDATE_TYPE_UNSPECIFIED'
| 'MARKET_STATE_UPDATE_TYPE_TERMINATE'
| 'MARKET_STATE_UPDATE_TYPE_SUSPEND'
| 'MARKET_STATE_UPDATE_TYPE_RESUME';
};
};
updateNetworkParameter?: { changes: unknown };
updateReferralProgram?: { changes: unknown };
updateSpotMarket?: { marketId: string; changes: unknown };
updateVolumeDiscountProgram?: { changes: unknown };
};
export type SingleProposalData = ProposalData & {
terms: Terms & {
closingTimestamp: string;
validationTimestamp: string;
};
};
type BatchProposalData = ProposalData & {
batchTerms: {
changes: Terms[];
};
};
export type SubProposalData = SingleProposalData & {
batchId: string;
};
export type ProposalNode = {
proposal: ProposalData;
proposalType: ProposalNodeType;
proposals: SubProposalData[];
};
type SingleProposalNode = ProposalNode & {
proposal: SingleProposalData;
proposalType: 'TYPE_SINGLE_OR_UNSPECIFIED';
proposals: [];
};
type BatchProposalNode = ProposalNode & {
proposal: BatchProposalData;
proposalType: 'TYPE_BATCH';
};
export const isProposalNode = (node: unknown): node is ProposalNode =>
Boolean(
typeof node === 'object' &&
node &&
'proposal' in node &&
typeof node.proposal === 'object' &&
node?.proposal &&
'id' in node.proposal &&
node?.proposal?.id
);
export const isSingleProposalNode = (
node: Maybe<ProposalNode>
): node is SingleProposalNode =>
Boolean(
node &&
node?.proposalType === 'TYPE_SINGLE_OR_UNSPECIFIED' &&
node?.proposal
);
export const isBatchProposalNode = (
node: Maybe<ProposalNode>
): node is BatchProposalNode =>
Boolean(
node &&
node?.proposalType === 'TYPE_BATCH' &&
node?.proposal &&
'batchTerms' in node.proposal &&
node?.proposals?.length > 0
);
// this includes also batch proposals with `updateMarket`s 👍
const PROPOSALS_ENDPOINT = `${ENV.rest}governances?proposalState=:proposalState&proposalType=:proposalType`;
// this can be queried also by sub proposal id as `proposalId` and it will
// return full batch proposal data with all of its sub proposals including
// the requested one inside `proposals` array.
const PROPOSAL_ENDPOINT = `${ENV.rest}governance?proposalId=:proposalId`;
export const getProposals = async ({
proposalState,
proposalType,
}: {
proposalState: ProposalState;
proposalType: ProposalType;
}) => {
try {
const response = await fetch(
PROPOSALS_ENDPOINT.replace(':proposalState', proposalState).replace(
':proposalType',
proposalType
)
);
if (response.ok) {
const data = await response.json();
if (
data &&
'connection' in data &&
data.connection &&
'edges' in data.connection &&
data.connection.edges?.length > 0
) {
const nodes = compact(
data.connection.edges.map((e: { node?: object }) => e?.node)
).filter(isProposalNode);
return nodes;
}
}
} catch {
// NOOP - ignore errors
}
return [];
};
export const getProposal = async ({ proposalId }: { proposalId: string }) => {
try {
const response = await fetch(
PROPOSAL_ENDPOINT.replace(':proposalId', proposalId)
);
if (response.ok) {
const data = await response.json();
if (data && 'data' in data && isProposalNode(data.data)) {
return data.data as ProposalNode;
}
}
} catch (err) {
// NOOP - ignore errors
}
return null;
};
export const useFetchProposal = ({ proposalId }: { proposalId?: string }) => {
const [data, setData] = useState<ProposalNode | null>(null);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
const cb = async () => {
if (!proposalId) return;
setLoading(true);
const data = await getProposal({ proposalId });
setLoading(false);
if (data) {
setData(data);
}
};
cb();
}, [proposalId]);
return { data, loading };
};
export const useFetchProposals = ({
proposalState,
proposalType,
}: {
proposalState: ProposalState;
proposalType: ProposalType;
}) => {
const [data, setData] = useState<ProposalNode[]>([]);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
const cb = async () => {
setLoading(true);
const data = await getProposals({ proposalState, proposalType });
setLoading(false);
if (data) {
setData(data);
}
};
cb();
}, [proposalState, proposalType]);
return { data, loading };
};
export const flatten = (
nodes: ProposalNode[]
): (SingleProposalData | SubProposalData)[] => {
const flattenNodes = [];
for (const node of nodes) {
if (isSingleProposalNode(node)) {
flattenNodes.push(node.proposal);
}
if (isBatchProposalNode(node)) {
for (const sub of node.proposals) {
flattenNodes.push(sub);
}
}
}
return flattenNodes;
};
@@ -61,7 +61,7 @@ const renderComponent = (proposal: IProposal) => {
<MemoryRouter>
<MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}>
<Proposal restData={{}} proposal={proposal} />
<Proposal restData={null} proposal={proposal} />
</VegaWalletProvider>
</MockedProvider>
</MemoryRouter>
@@ -8,15 +8,17 @@ import { ProposalJson } from '../proposal-json';
import { UserVote } from '../vote-details';
import Routes from '../../../routes';
import { ProposalState } from '@vegaprotocol/types';
import { type ProposalNode } from './proposal-utils';
import { useVoteSubmit } from '@vegaprotocol/proposals';
import { useUserVote } from '../vote-details/use-user-vote';
import { type Proposal as IProposal, type BatchProposal } from '../../types';
import { ProposalChangeDetails } from './proposal-change-details';
import { type JsonValue } from 'type-fest';
export interface ProposalProps {
proposal: IProposal | BatchProposal;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
restData: ProposalNode | null;
}
export const Proposal = ({ proposal, restData }: ProposalProps) => {
@@ -58,7 +60,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
<ProposalDescription description={proposal.rationale.description} />
</div>
<div className="mb-4">
<div className="mb-4 flex flex-col gap-0">
{proposal.__typename === 'Proposal' ? (
<ProposalChangeDetails
proposal={proposal}
@@ -70,6 +72,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
if (!p?.terms) return null;
return (
<ProposalChangeDetails
indicator={i + 1}
key={i}
proposal={proposal}
terms={p.terms}
@@ -94,7 +97,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
</div>
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
<ProposalJson proposal={restData?.proposal as unknown as JsonValue} />
</div>
</section>
);
@@ -206,7 +206,7 @@ export const ProposalsList = ({
/>
)}
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
<section className="-mx-4 p-4 mb-8 bg-vega-cdark-900 rounded-l-sm">
<SubHeading title={t('openProposals')} />
{sortedProposals.open.length > 0 ||
@@ -15,6 +15,8 @@ import {
type VoteFieldsFragment,
} from '../../__generated__/Proposals';
import { useBatchVoteInformation } from '../../hooks/use-vote-information';
import { MarketName } from '../proposal/market-name';
import { Indicator } from '../proposal/indicator';
export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber
@@ -39,8 +41,9 @@ const VoteProgress = ({
children,
}: VoteProgressProps) => {
const containerClasses = classNames(
'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden',
colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400'
'relative h-2 rounded-md overflow-hidden',
// 'border border-vega-dark-300',
colourfulBg ? 'bg-vega-red' : 'bg-vega-dark-200'
);
const progressClasses = classNames(
@@ -49,17 +52,19 @@ const VoteProgress = ({
);
const textClasses = classNames(
'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black'
'w-full flex items-center justify-start text-white text-sm pb-1'
);
return (
<div className={containerClasses}>
<div
className={progressClasses}
style={{ width: `${percentageFor}%` }}
data-testid={testId}
/>
<div>
<div className={textClasses}>{children}</div>
<div className={containerClasses}>
<div
className={progressClasses}
style={{ width: `${percentageFor}%` }}
data-testid={testId}
/>
</div>
</div>
);
};
@@ -78,14 +83,22 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
<div data-testid={testId}>
{reached ? (
<div className="flex items-center gap-2">
<VegaIcon name={VegaIconNames.TICK} size={20} />
<VegaIcon
name={VegaIconNames.TICK}
className="text-vega-green"
size={20}
/>
<span>
{threshold.toString()}% {text} {t('met')}
</span>
</div>
) : (
<div className="flex items-center gap-2">
<VegaIcon name={VegaIconNames.CROSS} size={20} />
<VegaIcon
name={VegaIconNames.CROSS}
className="text-vega-red"
size={20}
/>
<span>
{threshold.toString()}% {text} {t('not met')}
</span>
@@ -151,7 +164,7 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
<p className="flex gap-2 m-0 items-center">
<VegaIcon
name={VegaIconNames.CROSS}
className="text-vega-pink"
className="text-vega-red"
size={20}
/>
{t(
@@ -176,6 +189,7 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
if (!p?.terms) return null;
return (
<VoteBreakdownBatchSubProposal
indicator={i + 1}
key={i}
proposal={proposal}
votes={proposal.votes}
@@ -213,7 +227,7 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
<p className="flex gap-2 m-0 items-center">
<VegaIcon
name={VegaIconNames.CROSS}
className="text-vega-pink"
className="text-vega-red"
size={20}
/>
{t('Proposal failed: {{count}} of {{total}} proposals passed', {
@@ -235,6 +249,7 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
if (!p?.terms) return null;
return (
<VoteBreakdownBatchSubProposal
indicator={i + 1}
key={i}
proposal={proposal}
votes={proposal.votes}
@@ -255,10 +270,12 @@ const VoteBreakdownBatchSubProposal = ({
proposal,
votes,
terms,
indicator,
}: {
proposal: BatchProposal;
votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment;
indicator?: number;
}) => {
const { t } = useTranslation();
const voteInfo = useVoteInformation({
@@ -269,14 +286,37 @@ const VoteBreakdownBatchSubProposal = ({
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
let marketId = undefined;
if (terms?.change?.__typename === 'UpdateMarket') {
marketId = terms.change.marketId;
}
if (terms?.change?.__typename === 'UpdateMarketState') {
marketId = terms.change.market.id;
}
const marketName = marketId ? (
<>
: <MarketName marketId={marketId} />
</>
) : null;
const indicatorElement = indicator && <Indicator indicator={indicator} />;
return (
<div>
<h4>{t(terms.change.__typename)}</h4>
<VoteBreakDownUI
voteInfo={voteInfo}
isProposalOpen={isProposalOpen}
isUpdateMarket={isUpdateMarket}
/>
<div className="mb-6">
<div className="flex items-center gap-3 mb-3">
{indicatorElement}
<h4>
{t(terms.change.__typename)} {marketName}
</h4>
</div>
<div className="rounded-sm bg-vega-dark-100 p-3">
<VoteBreakDownUI
voteInfo={voteInfo}
isProposalOpen={isProposalOpen}
isUpdateMarket={isUpdateMarket}
/>
</div>
</div>
);
};
@@ -291,11 +331,13 @@ const VoteBreakdownNormal = ({ proposal }: { proposal: Proposal }) => {
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
return (
<VoteBreakDownUI
voteInfo={voteInfo}
isProposalOpen={isProposalOpen}
isUpdateMarket={isUpdateMarket}
/>
<div className="mb-6">
<VoteBreakDownUI
voteInfo={voteInfo}
isProposalOpen={isProposalOpen}
isUpdateMarket={isUpdateMarket}
/>
</div>
);
};
@@ -322,7 +364,6 @@ const VoteBreakDownUI = ({
noPercentage,
noLPPercentage,
yesPercentage,
yesLPPercentage,
yesTokens,
noTokens,
totalEquityLikeShareWeight,
@@ -335,6 +376,7 @@ const VoteBreakDownUI = ({
majorityLPMet,
willPassByTokenVote,
willPassByLPVote,
lpVoteWeight,
} = voteInfo;
const participationThresholdProgress = BigNumber.min(
@@ -359,13 +401,13 @@ const VoteBreakDownUI = ({
'flex justify-between flex-wrap gap-6'
);
const sectionClasses = classNames('min-w-[300px] flex-1 flex-grow');
const headingClasses = classNames('mb-2 text-vega-dark-400');
const headingClasses = classNames('mb-2 text-sm text-white font-bold');
const progressDetailsClasses = classNames(
'flex justify-between flex-wrap mt-2 text-sm'
);
return (
<div className="mb-6">
<div>
{isProposalOpen && (
<div
data-testid="vote-status"
@@ -382,7 +424,7 @@ const VoteBreakDownUI = ({
<VegaIcon
name={VegaIconNames.CROSS}
size={20}
className="text-vega-pink"
className="text-vega-red"
/>
)}
</span>
@@ -398,103 +440,13 @@ const VoteBreakDownUI = ({
<p className="m-0">
<Trans
i18nKey={'Currently expected to <0>fail</0>'}
components={[<span className="text-vega-pink" />]}
components={[<span className="text-vega-red" />]}
/>
</p>
)}
</div>
)}
{isUpdateMarket && (
<div className="mb-4">
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
<div className={sectionWrapperClasses}>
<section
className={sectionClasses}
data-testid="lp-majority-breakdown"
>
<VoteProgress
percentageFor={yesLPPercentage}
colourfulBg={true}
testId="lp-majority-progress"
>
<Status
reached={majorityLPMet}
threshold={requiredMajorityLPPercentage}
text={t('majorityThreshold')}
testId={
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{yesLPPercentage.toFixed(1)}%</button>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<span>
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(1)}%</button>
</Tooltip>
</span>
</div>
</div>
</section>
<section
className={sectionClasses}
data-testid="lp-participation-breakdown"
>
<VoteProgress
percentageFor={
lpParticipationThresholdProgress || new BigNumber(0)
}
testId="lp-participation-progress"
>
<Status
reached={participationLPMet}
threshold={requiredParticipationLP || new BigNumber(1)}
text={t('participationThreshold')}
testId={
participationLPMet
? 'lp-participation-met'
: 'lp-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
<Tooltip
description={formatNumber(
totalEquityLikeShareWeight,
defaultDP
)}
>
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
</Tooltip>
</div>
</div>
</section>
</div>
</div>
)}
{isUpdateMarket && <h3 className={headingClasses}>{t('tokenVote')}</h3>}
<div className={sectionWrapperClasses}>
<section
@@ -594,6 +546,97 @@ const VoteBreakDownUI = ({
</div>
</section>
</div>
{/** Liquidity provider vote */}
{isUpdateMarket && (
<div className="mt-3">
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
<div className={sectionWrapperClasses}>
<section
className={sectionClasses}
data-testid="lp-majority-breakdown"
>
<VoteProgress
percentageFor={lpVoteWeight}
colourfulBg={true}
testId="lp-majority-progress"
>
<Status
reached={majorityLPMet}
threshold={requiredMajorityLPPercentage}
text={t('majorityThreshold')}
testId={
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={
<span>{lpVoteWeight.toFixed(defaultDP)}%</span>
}
>
<button>{lpVoteWeight.toFixed(1)}%</button>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<span>
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(1)}%</button>
</Tooltip>
</span>
</div>
</div>
</section>
<section
className={sectionClasses}
data-testid="lp-participation-breakdown"
>
<VoteProgress
percentageFor={
lpParticipationThresholdProgress || new BigNumber(0)
}
testId="lp-participation-progress"
>
<Status
reached={participationLPMet}
threshold={requiredParticipationLP || new BigNumber(1)}
text={t('participationThreshold')}
testId={
participationLPMet
? 'lp-participation-met'
: 'lp-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
<Tooltip
description={formatNumber(
totalEquityLikeShareWeight,
defaultDP
)}
>
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
</Tooltip>
</div>
</div>
</section>
</div>
</div>
)}
</div>
);
};
@@ -273,4 +273,74 @@ describe('use-vote-information', () => {
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(true);
});
it('mainnet recreation: only yes LP votes equal passing', () => {
const yesVotes = 0;
const noVotes = 70;
const yesEquityLikeShareWeight = '0.21';
const noEquityLikeShareWeight = '0';
const fixedTokenValue = 1000000000000000000;
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(
yesVotes,
fixedTokenValue,
yesEquityLikeShareWeight
),
no: generateNoVotes(noVotes, fixedTokenValue, noEquityLikeShareWeight),
},
});
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(true);
});
it('mainnet recreation: mixed yes and no LP votes equal failing', () => {
const yesVotes = 0;
const noVotes = 70;
const yesEquityLikeShareWeight = '0.21';
const noEquityLikeShareWeight = '0.22';
const fixedTokenValue = 1000000000000000000;
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(
yesVotes,
fixedTokenValue,
yesEquityLikeShareWeight
),
no: generateNoVotes(noVotes, fixedTokenValue, noEquityLikeShareWeight),
},
});
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(false);
});
});
@@ -123,15 +123,19 @@ const getVoteData = (
totalSupply.multipliedBy(params.requiredParticipation)
);
const lpVoteWeight = yesEquityLikeShareWeight
.dividedBy(totalEquityLikeShareWeight)
.multipliedBy(100);
const participationLPMet = params.requiredParticipationLP
? totalEquityLikeShareWeight.isGreaterThan(params.requiredParticipationLP)
? lpVoteWeight.isGreaterThan(params.requiredParticipationLP)
: false;
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
requiredMajorityPercentage
);
const majorityLPMet = yesLPPercentage.isGreaterThanOrEqualTo(
const majorityLPMet = lpVoteWeight.isGreaterThanOrEqualTo(
requiredMajorityLPPercentage
);
@@ -149,14 +153,12 @@ const getVoteData = (
const willPassByLPVote =
participationLPMet &&
new BigNumber(yesLPPercentage).isGreaterThanOrEqualTo(
requiredMajorityLPPercentage
);
lpVoteWeight.isGreaterThanOrEqualTo(requiredMajorityLPPercentage);
let willPass = false;
if (changeType === 'UpdateMarket' || changeType === 'UpdateMarketState') {
willPass = willPassByTokenVote && willPassByLPVote;
willPass = willPassByTokenVote || willPassByLPVote;
} else {
willPass = willPassByTokenVote;
}
@@ -182,6 +184,7 @@ const getVoteData = (
totalLPTokensPercentage,
willPassByTokenVote,
willPassByLPVote,
lpVoteWeight: lpVoteWeight.isNaN() ? new BigNumber(0) : lpVoteWeight,
yesVotes: new BigNumber(votes.yes.totalNumber ?? 0),
noVotes: new BigNumber(votes.no.totalNumber ?? 0),
totalVotes: new BigNumber(votes.yes.totalNumber ?? 0).plus(
@@ -1,17 +1,16 @@
import { useParams } from 'react-router-dom';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { Proposal } from '../components/proposal';
import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from '../__generated__/Proposals';
import { useFetchProposal } from '../components/proposal/proposal-utils';
export const ProposalContainer = () => {
const params = useParams<{ proposalId: string }>();
const {
state: { data: restData, loading: restLoading, error: restError },
} = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`);
const { data: restData, loading: restLoading } = useFetchProposal({
proposalId: params.proposalId,
});
const { data, loading, error } = useProposalQuery({
fetchPolicy: 'network-only',
@@ -26,7 +25,7 @@ export const ProposalContainer = () => {
return (
<AsyncRenderer
loading={Boolean(loading || restLoading)}
error={error || restError}
error={error}
data={{
...data,
...(restData ? { restData } : {}),
@@ -1,10 +1,9 @@
import { useMemo, useEffect, useState, useCallback, useRef } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { EpochFieldsFragment } from '../home/__generated__/Rewards';
import { useRewardsQuery } from '../home/__generated__/Rewards';
import { ENV } from '../../../config';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
@@ -27,22 +26,16 @@ export const EpochIndividualRewards = ({
const [page, setPage] = useState(1);
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
const { param: marketCreationQuantumMultiple } = useNetworkParam(
'rewards_marketCreationQuantumMultiple'
);
const { data, loading, error, refetch } = useRewardsQuery({
const { data, loading, error } = useRewardsQuery({
notifyOnNetworkStatusChange: true,
variables: {
partyId: pubKey || '',
fromEpoch: epochId - EPOCHS_PAGE_SIZE,
toEpoch: epochId,
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
...calculateEpochOffset({ epochId, page, size: EPOCHS_PAGE_SIZE }),
delegationsPagination: { first: 50 },
},
skip: !pubKey,
});
@@ -70,36 +63,6 @@ export const EpochIndividualRewards = ({
});
}, [data?.party, epochId, epochRewardSummaries, page, rewards]);
const refetchData = useCallback(
async (toPage?: number) => {
const targetPage = toPage ?? page;
await refetch({
partyId: pubKey || '',
...calculateEpochOffset({ epochId, page, size: EPOCHS_PAGE_SIZE }),
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
});
setPage(targetPage);
},
[epochId, page, refetch, delegationsPagination, pubKey]
);
const prevEpochIdRef = useRef<number | null>(null);
useEffect(() => {
if (prevEpochIdRef.current === null) {
prevEpochIdRef.current = epochId;
} else if (epochId !== prevEpochIdRef.current) {
// When the epoch changes, we want to refetch the data to update the current page
refetchData();
}
prevEpochIdRef.current = epochId;
}, [epochId, refetchData]);
// Workarounds for the error handling of AsyncRenderer
const filteredErrors = filterAcceptableGraphqlErrors(error);
const filteredData = data || [];
@@ -131,10 +94,10 @@ export const EpochIndividualRewards = ({
isLoading={loading}
hasPrevPage={page > 1}
hasNextPage={page < totalPages}
onBack={() => refetchData(page - 1)}
onNext={() => refetchData(page + 1)}
onFirst={() => refetchData(1)}
onLast={() => refetchData(totalPages)}
onBack={() => setPage((x) => x - 1)}
onNext={() => setPage((x) => x + 1)}
onFirst={() => setPage(1)}
onLast={() => setPage(totalPages)}
>
{t('Page')} {page}
</Pagination>
@@ -23,36 +23,6 @@ describe('generateEpochIndividualRewardsList', () => {
epoch: { id: '2' },
};
const reward3: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'gbp', symbol: 'GBP', name: 'GBP', decimals: 7 },
party: { id: 'blah' },
epoch: { id: '2' },
};
const reward4: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
party: { id: 'blah' },
epoch: { id: '1' },
};
const reward5: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '150',
percentageOfTotal: '0.15',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
party: { id: 'blah' },
epoch: { id: '3' },
};
const rewardWrongType: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '50',
@@ -120,26 +90,10 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
@@ -147,7 +101,7 @@ describe('generateEpochIndividualRewardsList', () => {
});
it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4];
const rewards = [reward1, reward2];
const result1 = generateEpochIndividualRewardsList({
rewards,
epochId: 2,
@@ -157,7 +111,7 @@ describe('generateEpochIndividualRewardsList', () => {
expect(result1[0].epoch).toEqual(2);
expect(result1[1].epoch).toEqual(1);
const reorderedRewards = [reward4, reward3, reward2, reward1];
const reorderedRewards = [reward2, reward1];
const result2 = generateEpochIndividualRewardsList({
rewards: reorderedRewards,
epochId: 2,
@@ -170,7 +124,7 @@ describe('generateEpochIndividualRewardsList', () => {
it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4];
const rewards = [reward1, reward2];
const result = generateEpochIndividualRewardsList({
rewards,
epochId: 2,
@@ -181,37 +135,6 @@ describe('generateEpochIndividualRewardsList', () => {
{
epoch: 2,
rewards: [
{
asset: 'GBP',
totalAmount: '200',
decimals: 7,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '200',
percentageOfTotal: '0.2',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
{
asset: 'EUR',
totalAmount: '50',
@@ -221,26 +144,10 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '50',
percentageOfTotal: '0.05',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
@@ -250,33 +157,17 @@ describe('generateEpochIndividualRewardsList', () => {
rewards: [
{
asset: 'USD',
totalAmount: '200',
totalAmount: '100',
decimals: 6,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
@@ -285,7 +176,7 @@ describe('generateEpochIndividualRewardsList', () => {
});
it('returns data correctly for the requested epoch range', () => {
const rewards = [reward1, reward2, reward3, reward4, reward5];
const rewards = [reward1, reward2];
const resultPageOne = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
@@ -297,74 +188,11 @@ describe('generateEpochIndividualRewardsList', () => {
expect(resultPageOne).toEqual([
{
epoch: 3,
rewards: [
{
asset: 'USD',
decimals: 6,
totalAmount: '150',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '150',
percentageOfTotal: '0.15',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
rewards: [],
},
{
epoch: 2,
rewards: [
{
asset: 'GBP',
totalAmount: '200',
decimals: 7,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '200',
percentageOfTotal: '0.2',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
{
asset: 'EUR',
totalAmount: '50',
@@ -374,26 +202,10 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '50',
percentageOfTotal: '0.05',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
@@ -414,33 +226,17 @@ describe('generateEpochIndividualRewardsList', () => {
rewards: [
{
asset: 'USD',
totalAmount: '200',
totalAmount: '100',
decimals: 6,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
@@ -1,4 +1,4 @@
import { useState, useCallback, useEffect } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit';
import type { EpochFieldsFragment } from '../home/__generated__/Rewards';
@@ -23,37 +23,17 @@ export const EpochTotalRewards = ({ currentEpoch }: EpochTotalRewardsProps) => {
'rewards_marketCreationQuantumMultiple'
);
const [page, setPage] = useState(1);
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
const { data, loading, error } = useEpochAssetsRewardsQuery({
notifyOnNetworkStatusChange: true,
variables: {
epochRewardSummariesFilter: {
fromEpoch: epochId - EPOCHS_PAGE_SIZE,
},
epochRewardSummariesFilter: calculateEpochOffset({
epochId,
page: page,
size: EPOCHS_PAGE_SIZE,
}),
},
});
const refetchData = useCallback(
async (toPage?: number) => {
const targetPage = toPage ?? page;
await refetch({
epochRewardSummariesFilter: calculateEpochOffset({
epochId,
page: targetPage,
size: EPOCHS_PAGE_SIZE,
}),
});
setPage(targetPage);
},
[epochId, page, refetch]
);
useEffect(() => {
// when the epoch changes, we want to refetch the data to update the current page
if (data) {
refetchData();
}
}, [epochId, data, refetchData]);
const epochTotalRewardSummaries =
generateEpochTotalRewardsList({
data,
@@ -85,10 +65,10 @@ export const EpochTotalRewards = ({ currentEpoch }: EpochTotalRewardsProps) => {
isLoading={loading}
hasPrevPage={page > 1}
hasNextPage={page < totalPages}
onBack={() => refetchData(page - 1)}
onNext={() => refetchData(page + 1)}
onFirst={() => refetchData(1)}
onLast={() => refetchData(totalPages)}
onBack={() => setPage((x) => x - 1)}
onNext={() => setPage((x) => x + 1)}
onFirst={() => setPage(1)}
onLast={() => setPage(totalPages)}
>
{t('Page')} {page}
</Pagination>
@@ -149,38 +149,6 @@ describe('generateEpochAssetRewardsList', () => {
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '123',
},
@@ -291,40 +259,8 @@ describe('generateEpochAssetRewardsList', () => {
amount: '100',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '223',
totalAmount: '100',
},
],
]),
@@ -334,66 +270,7 @@ describe('generateEpochAssetRewardsList', () => {
'2',
{
epoch: 2,
assetRewards: new Map([
[
'1',
{
assetId: '1',
decimals: 18,
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '5',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '5',
},
],
]),
assetRewards: new Map(),
},
],
])
@@ -489,66 +366,7 @@ describe('generateEpochAssetRewardsList', () => {
'2',
{
epoch: 2,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
decimals: 18,
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '33',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '33',
},
],
]),
assetRewards: new Map(),
},
],
[
@@ -578,38 +396,6 @@ describe('generateEpochAssetRewardsList', () => {
amount: '15',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '15',
},
@@ -656,40 +442,8 @@ describe('generateEpochAssetRewardsList', () => {
amount: '100',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '223',
totalAmount: '100',
},
],
]),
@@ -18,22 +18,6 @@ export const RowAccountTypes = {
columnTitle: 'rewardsColInfraHeader',
description: 'rewardsColInfraTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
columnTitle: 'rewardsColPriceTakingHeader',
description: 'rewardsColPriceTakingTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
columnTitle: 'rewardsColPriceMakingHeader',
description: 'rewardsColPriceMakingTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
columnTitle: 'rewardsColLiquidityProvisionHeader',
description: 'rewardsColLiquidityProvisionTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
columnTitle: 'rewardsColMarketCreationHeader',
description: 'rewardsColMarketCreationTooltip',
},
};
interface ColumnHeaderProps {
@@ -55,7 +39,7 @@ export const rowGridItemStyles = (last = false) =>
});
const gridStyles = classNames(
'grid grid-cols-[repeat(8,minmax(100px,auto))] max-w-full overflow-auto',
'grid grid-cols-[repeat(4,minmax(100px,auto))] max-w-full overflow-auto',
`border-t border-vega-dark-200`,
'text-sm'
);
@@ -1,27 +1,31 @@
fragment ValidatorNode on Node {
id
stakedTotal
rewardScore {
rawValidatorScore
performanceScore
multisigScore
validatorScore
normalisedScore
validatorStatus
}
rankingScore {
status
previousStatus
rankingScore
stakeScore
performanceScore
votingPower
}
}
query PreviousEpoch($epochId: ID) {
epoch(id: $epochId) {
id
validatorsConnection {
edges {
node {
id
stakedTotal
rewardScore {
rawValidatorScore
performanceScore
multisigScore
validatorScore
normalisedScore
validatorStatus
}
rankingScore {
status
previousStatus
rankingScore
stakeScore
performanceScore
votingPower
}
...ValidatorNode
}
}
}
@@ -3,6 +3,8 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ValidatorNodeFragment = { __typename?: 'Node', id: string, stakedTotal: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string, multisigScore: string, validatorScore: string, normalisedScore: string, validatorStatus: Types.ValidatorStatus } | null, rankingScore: { __typename?: 'RankingScore', status: Types.ValidatorStatus, previousStatus: Types.ValidatorStatus, rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string } };
export type PreviousEpochQueryVariables = Types.Exact<{
epochId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
@@ -10,7 +12,28 @@ export type PreviousEpochQueryVariables = Types.Exact<{
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, stakedTotal: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string, multisigScore: string, validatorScore: string, normalisedScore: string, validatorStatus: Types.ValidatorStatus } | null, rankingScore: { __typename?: 'RankingScore', status: Types.ValidatorStatus, previousStatus: Types.ValidatorStatus, rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string } } } | null> | null } | null } };
export const ValidatorNodeFragmentDoc = gql`
fragment ValidatorNode on Node {
id
stakedTotal
rewardScore {
rawValidatorScore
performanceScore
multisigScore
validatorScore
normalisedScore
validatorStatus
}
rankingScore {
status
previousStatus
rankingScore
stakeScore
performanceScore
votingPower
}
}
`;
export const PreviousEpochDocument = gql`
query PreviousEpoch($epochId: ID) {
epoch(id: $epochId) {
@@ -18,30 +41,13 @@ export const PreviousEpochDocument = gql`
validatorsConnection {
edges {
node {
id
stakedTotal
rewardScore {
rawValidatorScore
performanceScore
multisigScore
validatorScore
normalisedScore
validatorStatus
}
rankingScore {
status
previousStatus
rankingScore
stakeScore
performanceScore
votingPower
}
...ValidatorNode
}
}
}
}
}
`;
${ValidatorNodeFragmentDoc}`;
/**
* __usePreviousEpochQuery__
@@ -6,7 +6,6 @@ import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch';
import { ValidatorTables } from './validator-tables';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ENV } from '../../../config';
import { getMultisigStatusInfo } from '../../../lib/get-multisig-status-info';
import { MultisigIncorrectNotice } from '../../../components/multisig-incorrect-notice';
@@ -20,7 +19,6 @@ export const EpochData = () => {
refetch: nodesRefetch,
} = useNodesQuery();
const { delegationsPagination } = ENV;
const {
data: userStakingData,
error: userStakingError,
@@ -29,11 +27,9 @@ export const EpochData = () => {
} = useStakingQuery({
variables: {
partyId: pubKey || '',
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
delegationsPagination: {
first: 50,
},
},
});
const { data: previousEpochData } = usePreviousEpochQuery({
@@ -288,7 +288,7 @@ describe('Consensus validators table', () => {
expect(
grid.querySelector('[role="gridcell"][col-id="totalPenalties"]')
).toHaveTextContent('13.16%');
).toHaveTextContent('10.07%');
expect(
grid.querySelector('[role="gridcell"][col-id="normalisedVotingPower"]')
@@ -185,15 +185,19 @@ export const ConsensusValidatorsTable = ({
const { rawValidatorScore: previousEpochValidatorScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
const overstakingPenalty = calculateOverallPenalty(
const overstakingPenalty = calculateOverstakedPenalty(
id,
allNodesInPreviousEpoch
);
const totalPenalty = calculateOverstakedPenalty(
const totalPenalty = calculateOverallPenalty(
id,
allNodesInPreviousEpoch
);
const lastEpochDataForNode = allNodesInPreviousEpoch.find(
(node) => node.id === id
);
return {
id,
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
@@ -239,6 +243,12 @@ export const ConsensusValidatorsTable = ({
: undefined,
[ValidatorFields.MULTISIG_ERROR]:
multisigStatus?.showMultisigStatusError,
[ValidatorFields.MULTISIG_PENALTY]: formatNumberPercentage(
new BigNumber(1)
.minus(lastEpochDataForNode?.rewardScore?.multisigScore ?? 1)
.times(100),
2
),
};
}
);
@@ -378,7 +388,6 @@ export const ConsensusValidatorsTable = ({
headerTooltip: t('StakeDescription').toString(),
cellRenderer: TotalStakeRenderer,
width: 120,
sort: 'desc',
},
{
field: ValidatorFields.PENDING_STAKE,
@@ -400,6 +409,7 @@ export const ConsensusValidatorsTable = ({
headerTooltip: t('NormalisedVotingPowerDescription').toString(),
cellRenderer: VotingPowerRenderer,
width: 120,
sort: 'desc',
},
{
field: ValidatorFields.TOTAL_PENALTIES,
@@ -40,6 +40,7 @@ export enum ValidatorFields {
PENDING_USER_STAKE = 'pendingUserStake',
USER_STAKE_SHARE = 'userStakeShare',
MULTISIG_ERROR = 'multisigError',
MULTISIG_PENALTY = 'multisigPenalty',
}
export const addUserDataToValidator = (
@@ -327,7 +328,7 @@ interface TotalPenaltiesRendererProps {
overstakedAmount: string;
overstakingPenalty: string;
totalPenalties: string;
multisigError?: boolean;
multisigPenalty: string;
};
}
@@ -346,11 +347,9 @@ export const TotalPenaltiesRenderer = ({
<div data-testid="overstaked-penalty-tooltip">
{t('overstakedPenalty')}: {data.overstakingPenalty}
</div>
{data.multisigError && (
<div data-testid="multisig-error-tooltip">
{t('multisigPenalty')}: 100%
</div>
)}
<div data-testid="multisig-error-tooltip">
{t('multisigPenalty')}: {data.multisigPenalty}
</div>
</>
}
>
@@ -1,4 +1,3 @@
import { ENV } from '../../../config';
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useTranslation } from 'react-i18next';
@@ -28,16 +27,13 @@ export const NodeContainer = ({
}) => {
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
const { data, loading, error, refetch } = useStakingQuery({
variables: {
partyId: pubKey || '',
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
delegationsPagination: {
first: 50,
},
},
errorPolicy: 'all',
});
@@ -3,7 +3,6 @@ import * as Sentry from '@sentry/react';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { ENV } from '../../../config';
import { usePartyDelegationsLazyQuery } from './__generated__/PartyDelegations';
import { TokenInput } from '../../../components/token-input';
import { useAppState } from '../../../contexts/app-state/app-state-context';
@@ -79,7 +78,6 @@ export const StakingForm = ({
const [error, setError] = useState<Error | null>(null);
const [isDialogVisible, setIsDialogVisible] = useState(false);
const { t } = useTranslation();
const { delegationsPagination } = ENV;
const [action, setAction] = React.useState<StakeAction>(
params.action as StakeAction
);
@@ -154,11 +152,9 @@ export const StakingForm = ({
const [delegationSearch, { data }] = usePartyDelegationsLazyQuery({
variables: {
partyId: pubKey,
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
delegationsPagination: {
first: 50,
},
},
fetchPolicy: 'network-only',
});
@@ -104,6 +104,11 @@ export const ValidatorTable = ({
};
}, [node, previousEpochData?.epoch.validatorsConnection?.edges]);
const previousNodeData =
previousEpochData?.epoch.validatorsConnection?.edges?.find(
(e) => e?.node.id === node.id
);
return (
<>
<p className="mb-12">
@@ -281,6 +286,28 @@ export const ValidatorTable = ({
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span className="uppercase">{t('multisigPenalty')}</span>
<span
data-testid="multisig-penalty"
className="flex gap-2 items-baseline"
>
<Tooltip description={t('multisigPenaltyDescription')}>
<span>
{formatNumberPercentage(
new BigNumber(1)
.minus(
previousNodeData?.node.rewardScore?.multisigScore ??
1
)
.times(100),
2
)}
</span>
</Tooltip>
</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('TOTAL PENALTIES')}</strong>
+1 -1
View File
@@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
+1 -1
View File
@@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
# Cosmic elevator flags
+1 -1
View File
@@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
+1 -1
View File
@@ -15,7 +15,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
+1 -1
View File
@@ -16,7 +16,7 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
# Cosmic elevator flags
@@ -1,9 +1,12 @@
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '@sentry/react';
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
import { Intent, Loader, TradingButton } from '@vegaprotocol/ui-toolkit';
import { useGameCards } from '../../lib/hooks/use-game-cards';
import {
ExternalLink,
Intent,
Loader,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import { Link, useNavigate } from 'react-router-dom';
import { Links } from '../../lib/links';
@@ -18,6 +21,9 @@ import take from 'lodash/take';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { TeamCard } from '../../components/competitions/team-card';
import { useMyTeam } from '../../lib/hooks/use-my-team';
import { useRewards } from '../../lib/hooks/use-rewards';
import { Trans } from 'react-i18next';
import { DocsLinks } from '@vegaprotocol/environment';
export const CompetitionsHome = () => {
const t = useT();
@@ -28,9 +34,9 @@ export const CompetitionsHome = () => {
const { data: epochData } = useEpochInfoQuery();
const currentEpoch = Number(epochData?.epoch.id);
const { data: gamesData, loading: gamesLoading } = useGameCards({
const { data: gamesData, loading: gamesLoading } = useRewards({
onlyActive: true,
currentEpoch,
scopeToTeams: true,
});
const { data: teamsData, loading: teamsLoading } = useTeams();
@@ -45,10 +51,34 @@ export const CompetitionsHome = () => {
return (
<ErrorBoundary>
<CompetitionsHeader title={t('Competitions')}>
<p className="text-lg mb-3">
<Trans
i18nKey={
'Check the cards below to see what community-created, on-chain games are active and how to compete. Joining a team also lets you take part in the on-chain <0>referral program</0>.'
}
components={[
<Link className="underline" key="ref-prog" to={Links.REFERRALS()}>
referral program
</Link>,
]}
/>
</p>
<p className="text-lg mb-1">
{t(
'Be a team player! Participate in games and work together to rake in as much profit to win.'
)}
<Trans
i18nKey={
'Got an idea for a competition? Anyone can define and fund one -- <0>propose an on-chain game</0> yourself.'
}
components={[
<ExternalLink
className="underline"
key="propose"
href={DocsLinks?.ASSET_TRANSFER_PROPOSAL}
>
propose an on-chain game
</ExternalLink>,
]}
/>
{/** Docs: https://docs.vega.xyz/mainnet/tutorials/proposals/asset-transfer-proposal */}
</p>
</CompetitionsHeader>
@@ -75,7 +105,7 @@ export const CompetitionsHome = () => {
variant="A"
title={t('Create a team')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
'Create a new team, share your code with potential members, or set a whitelist for an exclusive group.'
)}
actionElement={
<TradingButton
@@ -94,7 +124,7 @@ export const CompetitionsHome = () => {
variant="B"
title={t('Solo team / lone wolf')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
'Want to compete but think the best team size is one? This is the option for you.'
)}
actionElement={
<TradingButton
@@ -112,7 +142,7 @@ export const CompetitionsHome = () => {
variant="C"
title={t('Join a team')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
'Browse existing public teams to find your perfect match.'
)}
actionElement={
<TradingButton
@@ -131,27 +161,57 @@ export const CompetitionsHome = () => {
)}
{/** List of available games */}
<h2 className="text-2xl mb-6">{t('Games')}</h2>
<h2 className="text-2xl mb-1">{t('Games')}</h2>
<p className="mb-6 text-sm">
<Trans
i18nKey={
'See all the live games on the cards below. Every on-chain game is community funded and designed. <0>Find out how to create one</0>.'
}
components={[
<ExternalLink
className="underline"
key="find-out"
href={DocsLinks?.ASSET_TRANSFER_PROPOSAL}
>
Find out how to create one
</ExternalLink>,
]}
/>
{/** Docs: https://docs.vega.xyz/mainnet/tutorials/proposals/asset-transfer-proposal */}
</p>
{gamesLoading ? (
<Loader size="small" />
) : (
<GamesContainer data={gamesData} currentEpoch={currentEpoch} />
)}
<div className="mb-12 flex">
{gamesLoading ? (
<Loader size="small" />
) : (
<GamesContainer data={gamesData} currentEpoch={currentEpoch} />
)}
</div>
{/** The teams ranking */}
<div className="mb-6 flex flex-row items-baseline justify-between">
<h2 className="text-2xl">{t('Leaderboard')}</h2>
<div className="mb-1 flex flex-row items-baseline gap-3 justify-between">
<h2 className="text-2xl">
<Link to={Links.COMPETITIONS_TEAMS()} className=" underline">
{t('Leaderboard')}
</Link>
</h2>
<Link to={Links.COMPETITIONS_TEAMS()} className="text-sm underline">
{t('View all teams')}
</Link>
</div>
<p className="mb-6 text-sm">
{t(
'Teams can earn rewards if they meet the goals set in the on-chain trading competitions. Track your earned rewards here, and see which teams are top of the leaderboard this month.'
)}
</p>
{teamsLoading ? (
<Loader size="small" />
) : (
<CompetitionsLeaderboard data={take(teamsData, 10)} />
)}
<div className="flex">
{teamsLoading ? (
<Loader size="small" />
) : (
<CompetitionsLeaderboard data={take(teamsData, 10)} />
)}
</div>
</ErrorBoundary>
);
};
@@ -10,11 +10,7 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
TransferStatus,
type Asset,
type RecurringTransfer,
} from '@vegaprotocol/types';
import { TransferStatus, type Asset } from '@vegaprotocol/types';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { Table } from '../../components/table';
@@ -44,11 +40,6 @@ import {
areTeamGames,
} from '../../lib/hooks/use-games';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import {
type EnrichedTransfer,
isScopedToTeams,
useGameCards,
} from '../../lib/hooks/use-game-cards';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
ActiveRewardCard,
@@ -56,6 +47,11 @@ import {
} from '../../components/rewards-container/active-rewards';
import { type MarketMap, useMarketsMapProvider } from '@vegaprotocol/markets';
import format from 'date-fns/format';
import {
type EnrichedRewardTransfer,
isScopedToTeams,
useRewards,
} from '../../lib/hooks/use-rewards';
export const CompetitionsTeam = () => {
const t = useT();
@@ -78,10 +74,9 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
const { data: games, loading: gamesLoading } = useGames(teamId);
const { data: epochData, loading: epochLoading } = useEpochInfoQuery();
const { data: transfersData, loading: transfersLoading } = useGameCards({
currentEpoch: Number(epochData?.epoch.id),
const { data: transfersData, loading: transfersLoading } = useRewards({
onlyActive: false,
scopeToTeams: true,
});
const { data: markets } = useMarketsMapProvider();
@@ -112,7 +107,7 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
games={areTeamGames(games) ? games : undefined}
gamesLoading={gamesLoading}
transfers={transfersData}
transfersLoading={epochLoading || transfersLoading}
transfersLoading={transfersLoading}
allMarkets={markets || undefined}
refetch={refetch}
/>
@@ -137,7 +132,7 @@ const TeamPage = ({
members?: Member[];
games?: TeamGame[];
gamesLoading?: boolean;
transfers?: EnrichedTransfer[];
transfers?: EnrichedRewardTransfer[];
transfersLoading?: boolean;
allMarkets?: MarketMap;
refetch: () => void;
@@ -211,7 +206,7 @@ const Games = ({
}: {
games?: TeamGame[];
gamesLoading?: boolean;
transfers?: EnrichedTransfer[];
transfers?: EnrichedRewardTransfer[];
transfersLoading?: boolean;
allMarkets?: MarketMap;
}) => {
@@ -451,7 +446,7 @@ const GameTypeCell = ({
transfer,
allMarkets,
}: {
transfer?: EnrichedTransfer;
transfer?: EnrichedRewardTransfer;
allMarkets?: MarketMap;
}) => {
const [open, setOpen] = useState(false);
@@ -474,7 +469,7 @@ const GameTypeCell = ({
ref={ref}
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left md:truncate md:max-w-[25vw]"
>
<DispatchMetricInfo transferNode={transfer} allMarkets={allMarkets} />
<DispatchMetricInfo reward={transfer} />
</button>
</>
);
@@ -490,7 +485,7 @@ const ActiveRewardCardDialog = ({
open: boolean;
onChange: (isOpen: boolean) => void;
trigger?: HTMLElement | null;
transfer: EnrichedTransfer;
transfer: EnrichedRewardTransfer;
allMarkets?: MarketMap;
}) => {
const t = useT();
@@ -516,8 +511,6 @@ const ActiveRewardCardDialog = ({
<ActiveRewardCard
transferNode={transfer}
currentEpoch={Number(data?.epoch.id)}
kind={transfer.transfer.kind as RecurringTransfer}
allMarkets={allMarkets}
/>
</div>
<div className="w-1/4">
@@ -75,6 +75,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
{[
'chart',
'orderbook',
'depth',
'trades',
'liquidity',
'fundingPayments',
@@ -30,8 +30,6 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
setStudies,
setStudySizes,
setOverlays,
state,
setState,
} = useChartSettings();
const pennantChart = (
@@ -68,10 +66,6 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
onIntervalChange={(newInterval) => {
setInterval(fromTradingViewResolution(newInterval));
}}
onAutoSaveNeeded={(data) => {
setState(data);
}}
state={state}
/>
);
}
@@ -9,7 +9,6 @@ type StudySizes = { [S in Study]?: number };
export type Chartlib = 'pennant' | 'tradingview';
interface StoredSettings {
state: object | undefined; // Don't see a better type provided from TradingView type definitions
chartlib: Chartlib;
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
// chart types easier and more consistent
@@ -30,7 +29,6 @@ const STUDY_ORDER: Study[] = [
];
export const DEFAULT_CHART_SETTINGS = {
state: undefined,
chartlib: 'pennant' as const,
interval: Interval.INTERVAL_I15M,
type: ChartType.CANDLE,
@@ -47,7 +45,6 @@ export const useChartSettingsStore = create<
setStudies: (studies?: Study[]) => void;
setStudySizes: (sizes: number[]) => void;
setChartlib: (lib: Chartlib) => void;
setState: (state: object) => void;
}
>()(
persist(
@@ -95,9 +92,6 @@ export const useChartSettingsStore = create<
state.chartlib = lib;
});
},
setState: (state) => {
set({ state });
},
})),
{
name: 'vega_candles_chart_store',
@@ -151,7 +145,5 @@ export const useChartSettings = () => {
setOverlays: settings.setOverlays,
setStudySizes: settings.setStudySizes,
setChartlib: settings.setChartlib,
state: settings.state,
setState: settings.setState,
};
};
@@ -1,5 +1,4 @@
import { Link } from 'react-router-dom';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { type useTeams } from '../../lib/hooks/use-teams';
import { useT } from '../../lib/use-t';
@@ -18,7 +17,11 @@ export const CompetitionsLeaderboard = ({
const num = (n?: number | string) => (!n ? '-' : formatNumber(n, 0));
if (!data || data.length === 0) {
return <Splash>{t('Could not find any teams')}</Splash>;
return (
<p className="text-sm">
{t('Currently no active teams on the network.')}
</p>
);
}
return (
@@ -1,28 +1,26 @@
import { ActiveRewardCard } from '../rewards-container/active-rewards';
import { useT } from '../../lib/use-t';
import { type EnrichedTransfer } from '../../lib/hooks/use-game-cards';
import { useMarketsMapProvider } from '@vegaprotocol/markets';
import { type EnrichedRewardTransfer } from '../../lib/hooks/use-rewards';
export const GamesContainer = ({
data,
currentEpoch,
}: {
data: EnrichedTransfer[];
data: EnrichedRewardTransfer[];
currentEpoch: number;
}) => {
const t = useT();
const { data: markets } = useMarketsMapProvider();
if (!data || data.length === 0) {
return (
<p className="mb-6 text-muted">
{t('There are currently no games available.')}
<p className="text-sm">
{t('Currently no active games on the network.')}
</p>
);
}
return (
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{data.map((game, i) => {
// TODO: Remove `kind` prop from ActiveRewardCard
const { transfer } = game;
@@ -37,8 +35,6 @@ export const GamesContainer = ({
key={i}
transferNode={game}
currentEpoch={currentEpoch}
kind={transfer.kind}
allMarkets={markets || undefined}
/>
);
})}
@@ -21,18 +21,13 @@ const useFeesTableColumnDefs = (): ColDef[] => {
pinned: 'left',
width: 150,
},
{
field: 'liquidityFee',
valueFormatter: ({ value }: { value: number }) => value + '%',
},
{
field: 'feeAfterDiscount',
headerName: t('Total fee after discount'),
valueFormatter: ({ value }: { value: number }) => value + '%',
},
{
field: 'totalFee',
headerName: t('Total fee before discount'),
field: 'liquidityFee',
valueFormatter: ({ value }: { value: number }) => value + '%',
},
{
@@ -43,6 +38,11 @@ const useFeesTableColumnDefs = (): ColDef[] => {
field: 'makerFee',
valueFormatter: ({ value }: { value: number }) => value + '%',
},
{
field: 'totalFee',
headerName: t('Total fee before discount'),
valueFormatter: ({ value }: { value: number }) => value + '%',
},
] as ColDef[],
[t]
);
@@ -4,7 +4,6 @@ import {
lpAggregatedDataProvider,
type Filter,
LiquidityTable,
liquidityProvisionsDataProvider,
} from '@vegaprotocol/liquidity';
import { getAsset, useMarket } from '@vegaprotocol/markets';
import {
@@ -71,7 +70,7 @@ export const LiquidityContainer = ({
const useReloadLiquidityData = (marketId: string | undefined) => {
const { reload } = useDataProvider({
dataProvider: liquidityProvisionsDataProvider,
dataProvider: lpAggregatedDataProvider,
variables: { marketId: marketId || '' },
update: () => true,
skip: !marketId,
@@ -7,7 +7,7 @@ import {
useMaliciousOracle,
} from '@vegaprotocol/markets';
import { useState } from 'react';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type ProposalFragment } from '@vegaprotocol/proposals';
import { MarketSuspendedBanner } from './market-suspended-banner';
import { MarketUpdateBanner } from './market-update-banner';
import { MarketUpdateStateBanner } from './market-update-state-banner';
@@ -22,17 +22,17 @@ import {
type UpdateMarketBanner = {
kind: 'UpdateMarket';
proposals: MarketViewProposalFieldsFragment[];
proposals: ProposalFragment[];
};
type UpdateMarketStateBanner = {
kind: 'UpdateMarketState';
proposals: MarketViewProposalFieldsFragment[];
proposals: ProposalFragment[];
};
type NewMarketBanner = {
kind: 'NewMarket'; // aka a proposal of NewMarket which succeeds the current market
proposals: MarketViewProposalFieldsFragment[];
proposals: ProposalFragment[];
};
type SettledBanner = {
@@ -1,5 +1,5 @@
import { Fragment } from 'react';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type ProposalFragment } from '@vegaprotocol/proposals';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { useT } from '../../lib/use-t';
@@ -7,7 +7,7 @@ import { useT } from '../../lib/use-t';
export const MarketSuccessorProposalBanner = ({
proposals,
}: {
proposals: MarketViewProposalFieldsFragment[];
proposals: ProposalFragment[];
}) => {
const t = useT();
const tokenLink = useLinks(DApp.Governance);
@@ -28,7 +28,7 @@ export const MarketSuccessorProposalBanner = ({
}
)}{' '}
{proposals.map((item, i) => {
if (item.terms.change.__typename !== 'NewMarket') {
if (item.terms?.change.__typename !== 'NewMarket') {
return null;
}
@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import { ProposalState } from '@vegaprotocol/types';
import { MarketUpdateBanner } from './market-update-banner';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type ProposalFragment } from '@vegaprotocol/proposals';
describe('MarketUpdateBanner', () => {
const change = {
@@ -34,9 +34,7 @@ describe('MarketUpdateBanner', () => {
it('renders content for a single open proposal', () => {
render(
<MarketUpdateBanner
proposals={[openProposal as MarketViewProposalFieldsFragment]}
/>
<MarketUpdateBanner proposals={[openProposal as ProposalFragment]} />
);
expect(
@@ -50,9 +48,7 @@ describe('MarketUpdateBanner', () => {
it('renders content for a single passed proposal', () => {
render(
<MarketUpdateBanner
proposals={[passedProposal as MarketViewProposalFieldsFragment]}
/>
<MarketUpdateBanner proposals={[passedProposal as ProposalFragment]} />
);
expect(
@@ -65,10 +61,7 @@ describe('MarketUpdateBanner', () => {
});
it('renders content for multiple passed proposals', () => {
const proposals = [
openProposal,
openProposal,
] as MarketViewProposalFieldsFragment[];
const proposals = [openProposal, openProposal] as ProposalFragment[];
render(<MarketUpdateBanner proposals={proposals} />);
@@ -6,7 +6,7 @@ import {
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type ProposalFragment } from '@vegaprotocol/proposals';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ProposalState } from '@vegaprotocol/types';
import { useT } from '../../lib/use-t';
@@ -14,17 +14,17 @@ import { useT } from '../../lib/use-t';
export const MarketUpdateBanner = ({
proposals,
}: {
proposals: MarketViewProposalFieldsFragment[];
proposals: ProposalFragment[];
}) => {
const governanceLink = useLinks(DApp.Governance);
const t = useT();
const openProposals = sortBy(
proposals.filter((p) => p.state === ProposalState.STATE_OPEN),
(p) => p.terms.enactmentDatetime
(p) => p.terms?.enactmentDatetime
);
const passedProposals = sortBy(
proposals.filter((p) => p.state === ProposalState.STATE_PASSED),
(p) => p.terms.enactmentDatetime
(p) => p.terms?.enactmentDatetime
);
let content = null;
@@ -49,7 +49,7 @@ export const MarketUpdateBanner = ({
content = (
<p>
{t('Proposal set to change market on {{date}}.', {
date: format(new Date(proposal.terms.enactmentDatetime), 'dd MMMM'),
date: format(new Date(proposal.terms?.enactmentDatetime), 'dd MMMM'),
})}
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>,
</p>
@@ -1,8 +1,8 @@
import { type ReactNode } from 'react';
import sortBy from 'lodash/sortBy';
import { format, formatDuration, intervalToDuration } from 'date-fns';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { ProposalState } from '@vegaprotocol/types';
import { type ProposalFragment } from '@vegaprotocol/proposals';
import { MarketUpdateType, ProposalState } from '@vegaprotocol/types';
import {
DApp,
TOKEN_PROPOSAL,
@@ -19,18 +19,29 @@ export const MarketUpdateStateBanner = ({
proposals,
}: {
market: Market;
proposals: MarketViewProposalFieldsFragment[];
proposals: ProposalFragment[];
}) => {
const t = useT();
const governanceLink = useLinks(DApp.Governance);
const openTradingProposals = sortBy(
proposals.filter(
(p) =>
p.terms &&
p.terms.change.__typename === 'UpdateMarketState' &&
p.terms.change.updateType ===
MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME
),
(p) => p.terms?.enactmentDatetime
);
const openProposals = sortBy(
proposals.filter((p) => p.state === ProposalState.STATE_OPEN),
(p) => p.terms.enactmentDatetime
(p) => p.terms?.enactmentDatetime
);
const passedProposals = sortBy(
proposals.filter((p) => p.state === ProposalState.STATE_PASSED),
(p) => p.terms.enactmentDatetime
(p) => p.terms?.enactmentDatetime
);
if (!passedProposals.length && !openProposals.length) {
@@ -45,12 +56,41 @@ export const MarketUpdateStateBanner = ({
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
: undefined;
const openTradingProposalsLink =
openTradingProposals[0]?.__typename === 'Proposal' &&
openTradingProposals[0]?.id
? governanceLink(
TOKEN_PROPOSAL.replace(':id', openTradingProposals[0].id)
)
: openTradingProposals[0]?.__typename === 'ProposalDetail' &&
openTradingProposals[0]?.batchId
? governanceLink(
TOKEN_PROPOSAL.replace(':id', openTradingProposals[0].batchId)
)
: undefined;
const proposalsLink =
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
let content: ReactNode;
if (passedProposals.length) {
if (openTradingProposals.length >= 1) {
content = (
<>
<p className="mb-1">
{t(
'Trading on market {{name}} was suspended by governance. There are open proposals to resume trading on this market.',
{ name }
)}
</p>
<p>
<ExternalLink href={openTradingProposalsLink}>
{t('View proposals')}
</ExternalLink>
</p>
</>
);
} else if (passedProposals.length) {
const { date, duration, price } = getMessageVariables(passedProposals[0]);
content = (
<>
@@ -116,20 +156,23 @@ export const MarketUpdateStateBanner = ({
return <div data-testid={`update-state-banner-${market.id}`}>{content}</div>;
};
const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
const date = format(enactmentDatetime, 'dd MMMM');
const duration = formatDuration(
intervalToDuration({
start: new Date(),
end: enactmentDatetime,
}),
{
format: ['days', 'hours'],
}
);
const getMessageVariables = (proposal: ProposalFragment) => {
const enactmentDatetime =
proposal.terms && new Date(proposal.terms.enactmentDatetime);
const date = enactmentDatetime && format(enactmentDatetime, 'dd MMMM');
const duration =
enactmentDatetime &&
formatDuration(
intervalToDuration({
start: new Date(),
end: enactmentDatetime,
}),
{
format: ['days', 'hours'],
}
);
const price =
proposal.terms.change.__typename === 'UpdateMarketState'
proposal.terms?.change.__typename === 'UpdateMarketState'
? proposal.terms.change.price
: '';
return {
@@ -1,10 +1,10 @@
import {
useMarketProposals,
type MarketViewProposalFieldsFragment,
type ProposalFragment,
} from '@vegaprotocol/proposals';
import { ProposalState, ProposalType } from '@vegaprotocol/types';
const isPending = (p: MarketViewProposalFieldsFragment) => {
const isPending = (p: ProposalFragment) => {
return [ProposalState.STATE_OPEN, ProposalState.STATE_PASSED].includes(
p.state
);
@@ -21,9 +21,10 @@ export const useUpdateMarketStateProposals = (
const proposals = data
? data.filter(isPending).filter((p) => {
const change = p.terms.change;
const change = p.terms?.change;
if (
change &&
change.__typename === 'UpdateMarketState' &&
change.market.id === marketId
) {
@@ -48,8 +49,9 @@ export const useUpdateMarketProposals = (
const proposals = data
? data.filter(isPending).filter((p) => {
const change = p.terms.change;
const change = p.terms?.change;
if (
change &&
change.__typename === 'UpdateMarket' &&
change.marketId === marketId
) {
@@ -73,8 +75,9 @@ export const useSuccessorMarketProposals = (
const proposals = data
? data.filter(isPending).filter((p) => {
const change = p.terms.change;
const change = p.terms?.change;
if (
change &&
change.__typename === 'NewMarket' &&
change.successorConfiguration?.parentMarketId === marketId
) {
@@ -12,6 +12,7 @@ import { useT } from '../../lib/use-t';
import classNames from 'classnames';
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
import { MarketMarkPrice } from '../market-mark-price';
import { MarketBanner } from '../market-banner';
/**
* This is only rendered for the mobile navigation
*/
@@ -108,8 +109,11 @@ export const MobileMarketHeader = () => {
}
>
{data && (
<div className="px-3 py-6 text-sm grid grid-cols-2 items-center gap-x-4 gap-y-6">
<MarketHeaderStats market={data} />
<div>
<MarketBanner market={data} />
<div className="px-3 py-6 text-sm grid grid-cols-2 items-center gap-x-4 gap-y-6">
<MarketHeaderStats market={data} />
</div>
</div>
)}
</FullScreenPopover>

Some files were not shown because too many files have changed in this diff Show More