Compare commits

..
Author SHA1 Message Date
Madalina Raicu 374461fc87 fix: update accounts test 2023-02-20 15:22:47 +00:00
Madalina Raicu ce94affc2d fix(trading): do not pin existing account 2023-02-20 15:11:39 +00:00
Madalina Raicu 6a7bd68fd9 fix(trading): do not pin existing account 2023-02-20 15:09:59 +00:00
Madalina Raicu d6778d555b Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into feature/2864-deposit-flow-from-collateral-tab 2023-02-20 13:57:15 +00:00
Madalina Raicu e9fa7f32b9 feat(trading): re-name to pinned asset 2023-02-17 14:17:14 +01:00
Madalina Raicu c35ddbdbe2 Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into feature/2864-deposit-flow-from-collateral-tab 2023-02-17 14:07:17 +01:00
Madalina Raicu 1381666b42 fix(trading): use same type in test 2023-02-16 16:37:42 +00:00
Madalina Raicu cd72adc01d fix(trading): use same type 2023-02-16 16:17:42 +00:00
Madalina Raicu 04885308ac fix: rename variable market asset row 2023-02-16 15:49:57 +00:00
Madalina Raicu 8acce06e01 feat(trading): add accounts test 2023-02-16 15:47:04 +00:00
Madalina Raicu e5bab738f2 feat(trading): add market asset type 2023-02-16 15:35:50 +00:00
Madalina Raicu e19b8f9d56 fix: revert markets.gql 2023-02-16 14:36:35 +00:00
Madalina Raicu d7716f4bc5 feat(trading): show deposit to trade button only when deposied is 0 2023-02-16 14:29:58 +00:00
Madalina Raicu b6525498e5 feat(trading): pin only extra row and leave row data as it is 2023-02-16 14:25:57 +00:00
Madalina Raicu b1cfd36f07 feat(trading): deposit to trade collateral tab pinned row 2023-02-15 20:15:58 +00:00
155 changed files with 3010 additions and 7303 deletions
-6
View File
@@ -18,12 +18,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
- name: Run Cypress tests
uses: cypress-io/github-action@v4
with:
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
node-version: 16.14.0
- name: Install root dependencies
run: yarn install
- name: Generate queries
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
node-version: 16.14.0
- name: Install root dependencies
run: yarn install
- name: Check PR title
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
node-version: 16.14.0
- name: Install root dependencies
run: yarn install
- name: Generate queries
-47
View File
@@ -1,47 +0,0 @@
name: Publish libs to npm
on:
workflow_dispatch:
inputs:
project:
description: 'Monorepo project to publish'
required: true
type: choice
options:
- ui-toolkit
- react-helpers
- tailwindcss-config
- types
jobs:
publish:
name: Build & Publish - Tag
runs-on: ubuntu-latest
permissions:
contents: 'read'
actions: 'read'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: User Node.js 16
id: Node
uses: actions/setup-node@v3
with:
node-version: 16.15.1
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: node_modules-${{ hashFiles('**/yarn.lock') }}
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Build project
run: yarn nx build ${{inputs.project}}
- name: Publish project to @vegaprotocol
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
package: dist/libs/${{inputs.project}}/package.json
access: public
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
id: Node
uses: actions/setup-node@v3
with:
node-version: 16.15.1
node-version: 16.14.0
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
+1 -1
View File
@@ -1 +1 @@
16.15.1
16.14.0
+12 -3
View File
@@ -4,6 +4,15 @@ context('Asset page', { tags: '@regression' }, () => {
describe('Verify elements on page', () => {
before('Navigate to assets page', () => {
cy.visit('/assets');
// Check we have enough enough assets
cy.getAssets().then((assets) => {
assert.isAtLeast(
Object.keys(assets).length,
5,
'Ensuring we have at least 5 assets to test'
);
});
});
it('should be able to see full assets list', () => {
@@ -31,7 +40,7 @@ context('Asset page', { tags: '@regression' }, () => {
});
});
it('should open details page when clicked on "View details"', () => {
it('should open details dialog when clicked on "View details"', () => {
cy.getAssets().then((assets) => {
Object.values(assets).forEach((asset) => {
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
@@ -40,8 +49,8 @@ context('Asset page', { tags: '@regression' }, () => {
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
.eq(0)
.click();
cy.getByTestId('asset-header').should('have.text', asset.name);
cy.go('back');
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('dialog-close').click();
});
});
});
@@ -1,35 +1,22 @@
import { render, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { assetsList } from '../../mocks/assets';
import { AssetsTable } from './assets-table';
describe('AssetsTable', () => {
it('shows loading message on first render', async () => {
const res = render(
<MemoryRouter>
<AssetsTable data={null} />
</MemoryRouter>
);
const res = render(<AssetsTable data={null} />);
expect(await res.findByText('Loading...')).toBeInTheDocument();
});
it('shows no data message if no assets found', async () => {
const res = render(
<MemoryRouter>
<AssetsTable data={[]} />
</MemoryRouter>
);
const res = render(<AssetsTable data={[]} />);
expect(
await res.findByText('This chain has no assets')
).toBeInTheDocument();
});
it('shows a table/list with all the assets', async () => {
const res = render(
<MemoryRouter>
<AssetsTable data={assetsList} />
</MemoryRouter>
);
const res = render(<AssetsTable data={assetsList} />);
await waitFor(() => {
const rowA1 = res.container.querySelector('[row-id="123"]');
expect(rowA1).toBeInTheDocument();
@@ -1,4 +1,5 @@
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/react-helpers';
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
@@ -8,14 +9,15 @@ import { AgGridColumn } from 'ag-grid-react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom';
import type { RowClickedEvent } from 'ag-grid-community';
type AssetsTableProps = {
data: AssetFieldsFragment[] | null;
};
export const AssetsTable = ({ data }: AssetsTableProps) => {
const navigate = useNavigate();
const openAssetDetailsDialog = useAssetDetailsDialogStore(
(state) => state.open
);
const ref = useRef<AgGridReact>(null);
const showColumnsOnDesktop = () => {
ref.current?.columnApi.setColumnsVisible(
@@ -47,23 +49,17 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
autoHeight: true,
}}
suppressCellFocus={true}
onRowClicked={({ data }: RowClickedEvent) => {
navigate(data.id);
onGridReady={() => {
showColumnsOnDesktop();
}}
>
<AgGridColumn headerName={t('Symbol')} field="symbol" />
<AgGridColumn headerName={t('Name')} field="name" />
<AgGridColumn
flex="2"
headerName={t('ID')}
field="id"
hide={window.innerWidth < BREAKPOINT_MD}
/>
<AgGridColumn flex="2" headerName={t('ID')} field="id" />
<AgGridColumn
colId="type"
headerName={t('Type')}
field="source.__typename"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetTypeMapping[value].value
}
@@ -71,7 +67,6 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
<AgGridColumn
headerName={t('Status')}
field="status"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetStatusMapping[value].value
}
@@ -88,13 +83,28 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
value,
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
value ? (
<ButtonLink
onClick={(e) => {
navigate(value);
}}
>
{t('View details')}
</ButtonLink>
<div className="pb-1">
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value, e.target as HTMLElement);
}}
>
{t('View details')}
</ButtonLink>{' '}
<span className="max-md:hidden">
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(
value,
e.target as HTMLElement,
true
);
}}
>
{t('View JSON')}
</ButtonLink>
</span>
</div>
) : (
''
)
@@ -1,50 +0,0 @@
query ExplorerEpoch($id: ID!) {
epoch(id: $id) {
id
timestamps {
start
end
firstBlock
lastBlock
}
}
}
query ExplorerFutureEpoch {
networkParameter(key: "validators.epoch.length") {
value
}
epoch {
id
timestamps {
start
}
}
}
# query ExplorerEpoch($id: ID!) {
#
##### This could be useful for calculating roughly when a future epoch will
##### occur, but epoch not exist results in a total error
# networkParameter(key: "validators.epoch.length") {
# value
# }
#
##### This could be useful for relating where we are in time, but as above
##### the total failure caused by epoch(id) not existing
##### means this is useful
# currentEpoch: epoch {
# id
# }
#
# epoch(id: $id) {
# id
# timestamps {
# start
# end
# firstBlock
# lastBlock
# }
# }
#}
@@ -1,99 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerEpochQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, firstBlock: string, lastBlock?: string | null } } };
export type ExplorerFutureEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerFutureEpochQuery = { __typename?: 'Query', networkParameter?: { __typename?: 'NetworkParameter', value: string } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null } } };
export const ExplorerEpochDocument = gql`
query ExplorerEpoch($id: ID!) {
epoch(id: $id) {
id
timestamps {
start
end
firstBlock
lastBlock
}
}
}
`;
/**
* __useExplorerEpochQuery__
*
* To run a query within a React component, call `useExplorerEpochQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerEpochQuery` 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 } = useExplorerEpochQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerEpochQuery(baseOptions: Apollo.QueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
}
export function useExplorerEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
}
export type ExplorerEpochQueryHookResult = ReturnType<typeof useExplorerEpochQuery>;
export type ExplorerEpochLazyQueryHookResult = ReturnType<typeof useExplorerEpochLazyQuery>;
export type ExplorerEpochQueryResult = Apollo.QueryResult<ExplorerEpochQuery, ExplorerEpochQueryVariables>;
export const ExplorerFutureEpochDocument = gql`
query ExplorerFutureEpoch {
networkParameter(key: "validators.epoch.length") {
value
}
epoch {
id
timestamps {
start
}
}
}
`;
/**
* __useExplorerFutureEpochQuery__
*
* To run a query within a React component, call `useExplorerFutureEpochQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerFutureEpochQuery` 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 } = useExplorerFutureEpochQuery({
* variables: {
* },
* });
*/
export function useExplorerFutureEpochQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
}
export function useExplorerFutureEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
}
export type ExplorerFutureEpochQueryHookResult = ReturnType<typeof useExplorerFutureEpochQuery>;
export type ExplorerFutureEpochLazyQueryHookResult = ReturnType<typeof useExplorerFutureEpochLazyQuery>;
export type ExplorerFutureEpochQueryResult = Apollo.QueryResult<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>;
@@ -1,56 +0,0 @@
import { render } from '@testing-library/react';
import { IconForEpoch } from './epoch';
const THE_PAST = 'Monday, 17 February 2022 11:44:09';
const THE_FUTURE = 'Monday, 17 February 3023 11:44:09';
describe('IconForEpoch', () => {
it('Handles malformed dates', () => {
const start = 'This is n0t a d4te';
const end = '📅';
const screen = render(<IconForEpoch start={start} end={end} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('defaults to a calendar icon', () => {
const start = null as unknown as string;
const end = null as unknown as string;
const screen = render(<IconForEpoch start={start} end={end} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('if start and end are both in the future, stick with calendar', () => {
const screen = render(<IconForEpoch start={THE_FUTURE} end={THE_FUTURE} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('if start is in the past and end is in the future, this is currently active', () => {
const screen = render(<IconForEpoch start={THE_PAST} end={THE_FUTURE} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'circle icon'
);
});
it('if start and end are in the paste, this is done', () => {
const screen = render(<IconForEpoch start={THE_PAST} end={THE_PAST} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'tick-circle icon'
);
});
});
@@ -1,125 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import EpochMissingOverview, { calculateEpochData } from './epoch-missing';
import { getSecondsFromInterval } from '@vegaprotocol/react-helpers';
const START_DATE_PAST = 'Monday, 17 February 2022 11:44:09';
describe('getSecondsFromInterval', () => {
it('returns 0 for bad data', () => {
expect(getSecondsFromInterval(null as unknown as string)).toEqual(0);
expect(getSecondsFromInterval('')).toEqual(0);
expect(getSecondsFromInterval('🧙')).toEqual(0);
expect(getSecondsFromInterval(2 as unknown as string)).toEqual(0);
});
it('parses out months from a capital M', () => {
expect(getSecondsFromInterval('2M')).toEqual(5184000);
});
it('parses out days from a capital D', () => {
expect(getSecondsFromInterval('1D')).toEqual(86400);
});
it('parses out hours from a lower case h', () => {
expect(getSecondsFromInterval('11h')).toEqual(39600);
});
it('parses out minutes from a lower case m', () => {
expect(getSecondsFromInterval('10m')).toEqual(600);
});
it('parses out seconds from a lower case s', () => {
expect(getSecondsFromInterval('99s')).toEqual(99);
});
it('parses complex examples', () => {
expect(getSecondsFromInterval('24h')).toEqual(86400);
expect(getSecondsFromInterval('1h30m')).toEqual(5400);
expect(getSecondsFromInterval('1D1h30m1s')).toEqual(91801);
});
});
describe('calculateEpochData', () => {
it('Handles bad data', () => {
const currentEpochId = null as unknown as string;
const missingEpochId = null as unknown as string;
const epochStart = null as unknown as string;
const epochLength = null as unknown as string;
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
expect(res).toHaveProperty('label', 'Missing data');
expect(res).toHaveProperty('isInFuture', false);
});
it('Calculates that a bigger epoch number is in the future from basic data', () => {
const currentEpochId = '10';
const missingEpochId = '20';
const epochStart = '';
const epochLength = '';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
expect(res).toHaveProperty('isInFuture', true);
});
it('If it has an epoch length and a start time, it provides an estimate', () => {
const currentEpochId = '10';
const missingEpochId = '20';
const epochStart = START_DATE_PAST;
const epochLength = '1s';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
expect(res).toHaveProperty('label');
expect(res.label).toMatch(/^Estimate/);
expect(res.label).toMatch(/in less than a minute$/);
});
it('Provide decent string for past', () => {
const currentEpochId = '20';
const missingEpochId = '10';
const epochStart = START_DATE_PAST;
const epochLength = '1s';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
expect(res).toHaveProperty('label');
expect(res.label).toMatch(/^Estimate/);
expect(res.label).toMatch(/less than a minute ago$/);
});
});
describe('EpochMissingOverview', () => {
function renderComponent(missingEpochId: string) {
return render(
<MockedProvider>
<EpochMissingOverview missingEpochId={missingEpochId} />
</MockedProvider>
);
}
it('renders a - if no id is provided', () => {
const n = null as unknown as string;
const screen = renderComponent(n);
expect(screen.getByTestId('empty')).toBeInTheDocument();
});
});
@@ -1,111 +0,0 @@
import { useExplorerFutureEpochQuery } from './__generated__/Epoch';
import addSeconds from 'date-fns/addSeconds';
import formatDistance from 'date-fns/formatDistance';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import isFuture from 'date-fns/isFuture';
import { isValidDate } from '@vegaprotocol/react-helpers';
import { getSecondsFromInterval } from '@vegaprotocol/react-helpers';
export type EpochMissingOverviewProps = {
missingEpochId?: string;
};
/**
* Renders a set of details for an epoch that has no representation in the
* data node. This is primarily for one of two reasons:
*
* 1. The epoch hasn't happened yet
* 2. The epoch happened before a snapshot, and thus the details don't exist
*
* This component is used when the API has responded with no data for an epoch
* by ID, so we already know that we can't display start time/block etc.
*
* We can detect 1 if the epoch is a higher number than the current epoch
* We can detect 2 if the epoch is in the past, but we still get no response.
*/
const EpochMissingOverview = ({
missingEpochId,
}: EpochMissingOverviewProps) => {
const { data, error, loading } = useExplorerFutureEpochQuery();
// This should not happen, but it's easily handled
if (!missingEpochId) {
return <span data-testid="empty">-</span>;
}
// No data should also not happen - we've requested the current epoch. This
// could happen at chain restart, but shouldn't. If it does, fallback.
if (!data || loading || error) {
return <span data-testid="empty">{missingEpochId}</span>;
}
// If we have enough information to predict a future or past block time, let's do it
if (
!missingEpochId ||
!data.epoch.id ||
!data.epoch.timestamps.start ||
!data?.networkParameter?.value
) {
return <span data-testid="empty">{missingEpochId}</span>;
}
const { label, isInFuture } = calculateEpochData(
data.epoch.id,
missingEpochId,
data.epoch.timestamps.start,
data.networkParameter.value
);
return (
<Tooltip description={<p className="text-xs m-2">{label}</p>}>
<p>
{isInFuture ? (
<Icon name="calendar" className="mr-1" />
) : (
<Icon name="outdated" className="mr-1" />
)}
{missingEpochId}
</p>
</Tooltip>
);
};
export function calculateEpochData(
currentEpochId: string,
missingEpochId: string,
epochStart: string,
epochLength: string
) {
// Blank string will be return 0 seconds from getSecondsFromInterval
const epochLengthInSeconds = getSecondsFromInterval(epochLength);
if (!epochStart || !epochLength) {
// Let's just take a guess
return {
label: 'Missing data',
isInFuture: parseInt(missingEpochId) > parseInt(currentEpochId),
};
}
const startFrom = new Date(epochStart);
const diff = parseInt(missingEpochId) - parseInt(currentEpochId);
const futureDate = addSeconds(startFrom, diff * epochLengthInSeconds);
const label =
isValidDate(futureDate) && isValidDate(startFrom)
? `Estimate: ${futureDate.toLocaleString()} - ${formatDistance(
futureDate,
startFrom,
{ addSuffix: true }
)}`
: 'Missing data';
return {
label,
isInFuture: isFuture(futureDate),
};
}
export default EpochMissingOverview;
@@ -1,127 +0,0 @@
import { useExplorerEpochQuery } from './__generated__/Epoch';
import { t } from '@vegaprotocol/react-helpers';
import { BlockLink } from '../links';
import { Time } from '../time';
import { TimeAgo } from '../time-ago';
import EpochMissingOverview from './epoch-missing';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import isPast from 'date-fns/isPast';
const borderClass =
'border-solid border-2 border-vega-dark-200 border-collapse';
export type EpochOverviewProps = {
id?: string;
};
/**
* Displays detailed information about an epoch, given an ID. This
* works for past epochs and current epochs - future epochs, and a
* few other situations (see epoch-missing) will not return us
* enough information to render this.
*
* The details are hidden in a tooltip, behind the epoch number
*/
const EpochOverview = ({ id }: EpochOverviewProps) => {
const { data, error, loading } = useExplorerEpochQuery({
variables: { id: id || '' },
});
const ti = data?.epoch.timestamps;
if (
error?.message &&
error.message.includes('no resource corresponding to this id')
) {
return <EpochMissingOverview missingEpochId={id} />;
}
if (!ti || loading || error) {
return <span>{id}</span>;
}
const description = (
<table className="text-xs m-2">
<thead>
<tr>
<th></th>
<th className={`text-center ${borderClass}`}>{t('Block')}</th>
<th className={`text-center ${borderClass}`}>{t('Time')}</th>
</tr>
</thead>
<tbody>
<tr>
<th className={`px-2 ${borderClass}`}>{t('Start')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.firstBlock ? <BlockLink height={ti.firstBlock} /> : '-'}
</td>
<td className={`px-2 ${borderClass}`}>
<Time date={ti.start} />
<br />
<TimeAgo date={ti.start} />
</td>
</tr>
<tr>
<th className={`px-2 ${borderClass}`}>{t('End')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.lastBlock ? (
<BlockLink height={ti.lastBlock} />
) : (
t('In progress')
)}
</td>
<td className={`px-2 ${borderClass}`}>
{ti.end ? (
<>
<Time date={ti.end} />
<br />
<TimeAgo date={ti.end} />
</>
) : (
<span>{t('-')}</span>
)}
</td>
</tr>
</tbody>
</table>
);
return (
<Tooltip description={description}>
<p>
<IconForEpoch start={ti.start} end={ti.end} />
{id}
</p>
</Tooltip>
);
};
export type IconForEpochProps = {
start: string;
end: string;
};
/**
* Chooses an icon to display next to the epoch number, representing
* when the epoch is relative to now (i.e. not yet started, started,
* finished)
*/
export function IconForEpoch({ start, end }: IconForEpochProps) {
const startHasPassed = start ? isPast(new Date(start)) : false;
const endHasPassed = end ? isPast(new Date(end)) : false;
let i: IconProps['name'] = 'calendar';
if (!startHasPassed && !endHasPassed) {
i = 'calendar';
} else if (startHasPassed && !endHasPassed) {
i = 'circle';
} else if (startHasPassed && endHasPassed) {
i = 'tick-circle';
}
return <Icon name={i} className="mr-2" />;
}
export default EpochOverview;
@@ -1,24 +1,17 @@
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
import { t, useScreenDimensions } from '@vegaprotocol/react-helpers';
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { useMemo, useState } from 'react';
import { ENV } from '../../config/env';
import { t } from '@vegaprotocol/react-helpers';
import { Link } from '@vegaprotocol/ui-toolkit';
import { useState } from 'react';
export const Footer = () => {
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['lg', 'xl'].includes(screenSize),
[screenSize]
);
return (
<>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
{GIT_COMMIT_HASH && (
<p data-testid="git-commit-hash">
{t('Version')}:{' '}
<Link
@@ -32,21 +25,15 @@ export const Footer = () => {
{GIT_COMMIT_HASH}
</Link>
</p>
</div>
)}
)}
</div>
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
<div className="flex pl-2 content-center">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
</div>
<div className="flex pl-2 content-center">
<ExternalLink href={ENV.addresses.feedback}>
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
</ExternalLink>
</div>
</div>
</footer>
<NodeSwitcherDialog
@@ -11,14 +11,14 @@ export const InfoBlock = ({ title, subtitle, tooltipInfo }: InfoBlockProps) => {
return (
<div className="flex flex-col text-center ">
<h3 className="text-4xl">{title}</h3>
<p className="text-vega-dark-100 dark:text-vega-light-200">
<p className="text-zinc-800 dark:text-zinc-300">
{subtitle}
{tooltipInfo ? (
<Tooltip description={tooltipInfo} align="center">
<span>
<Icon
name="info-sign"
className="ml-2 text-vega-light-300 dark:text-vega-dark-300"
className="ml-2 text-zinc-400 dark:text-zinc-600"
/>
</span>
</Tooltip>
@@ -44,12 +44,12 @@ export const InfoPanel = ({
text={id}
startChars={visibleChars}
endChars={visibleChars}
className="text-vega-dark-100 dark:text-vega-light-200"
className="text-black dark:text-zinc-200"
/>
) : (
<p
title={id}
className="text-vega-dark-100 dark:text-vega-light-200 truncate ..."
className="text-black dark:text-zinc-200 truncate ..."
>
{id}
</p>
@@ -70,7 +70,7 @@ export const InfoPanel = ({
</div>
{copy && (
<CopyWithTooltip text={id}>
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" />
</button>
</CopyWithTooltip>
@@ -5,13 +5,9 @@ import {
useAssetDataProvider,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { useNavigate } from 'react-router-dom';
import { Routes } from '../../../routes/route-names';
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
assetId: string;
asDialog?: boolean;
showAssetSymbol?: boolean;
};
/**
@@ -19,32 +15,17 @@ export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
* with a link to the assets modal. If the name does not come back
* it will use the ID instead.
*/
export const AssetLink = ({
assetId,
asDialog,
showAssetSymbol = false,
...props
}: AssetLinkProps) => {
export const AssetLink = ({ assetId, ...props }: AssetLinkProps) => {
const { data: asset } = useAssetDataProvider(assetId);
const open = useAssetDetailsDialogStore((state) => state.open);
const navigate = useNavigate();
const label = asset
? showAssetSymbol
? asset?.symbol
: asset?.name
: assetId;
const label = asset?.name ? asset.name : assetId;
return (
<ButtonLink
data-testid="asset-link"
disabled={!asset}
onClick={(e) => {
if (asDialog) {
open(assetId, e.target as HTMLElement);
} else {
navigate(`${Routes.ASSETS}/${asset?.id}`);
}
open(assetId, e.target as HTMLElement);
}}
{...props}
>
@@ -48,7 +48,7 @@ const MarketLink = ({
<Link
className="underline"
{...props}
to={`/${Routes.MARKETS}/${id}`}
to={`/${Routes.MARKETS}#${id}`}
title={id}
>
{label}
@@ -56,7 +56,7 @@ const MarketLink = ({
);
} else {
return (
<Link className="underline" {...props} to={`/${Routes.MARKETS}/${id}`}>
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
<Hash text={id} />
</Link>
);
@@ -1,39 +0,0 @@
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import PartyLink from './party-link';
describe('PartyLink', () => {
it('renders Network for 000.000 party', () => {
const zeroes =
'0000000000000000000000000000000000000000000000000000000000000000';
const screen = render(<PartyLink id={zeroes} />);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders Network for network party', () => {
const screen = render(<PartyLink id="network" />);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders ID with no link for invalid party', () => {
const screen = render(<PartyLink id="this-party-is-not-valid" />);
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
});
it('links a valid party to the party page', () => {
const aValidParty =
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
const screen = render(
<MemoryRouter>
<PartyLink id={aValidParty} />
</MemoryRouter>
);
const el = screen.getByText(aValidParty);
expect(el).toBeInTheDocument();
// The text should be a link that points to the party's page
expect(el.parentElement?.tagName).toEqual('A');
expect(el.parentElement?.getAttribute('href')).toContain(aValidParty);
});
});
@@ -3,47 +3,19 @@ import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import Hash from '../hash';
import { t } from '@vegaprotocol/react-helpers';
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
export const SPECIAL_CASE_NETWORK_ID =
'0000000000000000000000000000000000000000000000000000000000000000';
export const SPECIAL_CASE_NETWORK = 'network';
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
truncate?: boolean;
};
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
// Some transactions will involve the 'network' party, which is alias for '000...000'
// The party page does not handle this nicely, so in this case we render the word 'Network'
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
return (
<span className="font-mono" data-testid="network">
{t('Network')}
</span>
);
}
// If the party doesn't look correct, there's no point in linking to id. Just render
// the ID as it was given to us
if (!isValidPartyId(id)) {
return (
<span className="font-mono" data-testid="invalid-party">
{id}
</span>
);
}
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
return (
<Link
className="underline font-mono"
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
<Hash text={truncate ? truncateMiddle(id) : id} />
<Hash text={id} />
</Link>
);
};
@@ -22,7 +22,8 @@ export const MarketDetails = ({
}: {
market: MarketInfoNoCandlesQuery['market'];
}) => {
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
const assetSymbol =
market?.tradableInstrument.instrument.product?.settlementAsset.symbol;
const assetId = useMemo(
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
[market]
@@ -38,10 +39,6 @@ export const MarketDetails = ({
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
const liquidityPriceRange = formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
);
const panels = [
{
title: t('Key details'),
@@ -182,41 +179,32 @@ export const MarketDetails = ({
{
title: t('Liquidity price range'),
content: (
<>
<p className="text-xs mb-4">
{`For liquidity orders count towards a commitment they have to be
within either the liquidity or price monitoring bounds (whichever is
tighter).`}
</p>
<p className="text-xs mb-4">
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
price.`}
</p>
<MarketInfoTable
noBorder={false}
data={{
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
lowestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
highestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
}}
></MarketInfoTable>
</>
<MarketInfoTable
noBorder={false}
data={{
liquidityPriceRange: formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
),
LPVolumeMin:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${assetSymbol}`,
LPVolumeMax:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${assetSymbol}`,
}}
></MarketInfoTable>
),
},
{
@@ -13,7 +13,7 @@ export interface DeterministicOrderDetailsProps {
}
export const wrapperClasses =
'grid lg:grid-cols-1 flex items-center max-w-xl border border-vega-light-200 dark:border-vega-dark-150 rounded-md pv-2 ph-5 mb-5';
'grid lg:grid-cols-1 flex items-center max-w-xl border border-zinc-200 dark:border-zinc-800 rounded-md pv-2 ph-5 mb-5';
/**
* This component renders the *current* details for an order
@@ -42,7 +42,7 @@ const DeterministicOrderDetails = ({
<h2 className="text-3xl font-bold mb-4 display-5">
{t('Order not found')}
</h2>
<p className="text-vega-light-400 mb-12">
<p className="text-gray-500 mb-12">
{t('No order created from this transaction')}
</p>
</div>
@@ -38,7 +38,7 @@ export const PageHeader = ({
</h2>
{copy && (
<CopyWithTooltip data-testid="copy-to-clipboard" text={title}>
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" className="" />
</button>
</CopyWithTooltip>
@@ -8,7 +8,7 @@ interface PanelProps {
export const Panel = ({ children, className }: PanelProps) => (
<div
className={classNames(
'border border-vega-light-150 dark:border-vega-dark-150 rounded-md p-5 mb-5',
'border border-zinc-200 dark:border-zinc-800 rounded-md p-5 mb-5',
className
)}
>
@@ -1,227 +0,0 @@
import type { ProposalListFieldsFragment } from '@vegaprotocol/governance';
import { VoteProgress } from '@vegaprotocol/governance';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/ui-toolkit';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { RowClickedEvent } from 'ag-grid-community';
import {
getDateTimeFormat,
NetworkParams,
t,
useNetworkParams,
} from '@vegaprotocol/react-helpers';
import { ProposalStateMapping } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
type ProposalTermsDialog = {
open: boolean;
title: string;
content: unknown;
};
type ProposalsTableProps = {
data: ProposalListFieldsFragment[] | null;
};
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const tokenLink = useLinks(DApp.Token);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => {
const showColumnsOnDesktop = () => {
gridRef.current?.columnApi.setColumnsVisible(
['voting', 'cDate', 'eDate', 'type'],
window.innerWidth > BREAKPOINT_MD
);
gridRef.current?.columnApi.setColumnWidth(
'actions',
window.innerWidth > BREAKPOINT_MD ? 221 : 80
);
};
window.addEventListener('resize', showColumnsOnDesktop);
return () => {
window.removeEventListener('resize', showColumnsOnDesktop);
};
}, []);
const [dialog, setDialog] = useState<ProposalTermsDialog>({
open: false,
title: '',
content: null,
});
return (
<>
<AgGrid
ref={gridRef}
rowData={data}
getRowId={({ data }: { data: ProposalListFieldsFragment }) =>
data.id || data.rationale.title
}
overlayNoRowsTemplate={t('This chain has no markets')}
domLayout="autoHeight"
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
suppressCellFocus={true}
onRowClicked={({ data, event }: RowClickedEvent) => {
if (
(event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON'
) {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data.id)
);
window.open(proposalPage, '_blank');
}
}}
>
<AgGridColumn
colId="title"
headerName={t('Title')}
field="rationale.title"
flex={2}
wrapText={true}
/>
<AgGridColumn
colId="type"
maxWidth={180}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Type')}
field="terms.change.__typename"
/>
<AgGridColumn
maxWidth={100}
headerName={t('State')}
field="state"
valueFormatter={({
value,
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
return value ? ProposalStateMapping[value] : '-';
}}
/>
<AgGridColumn
colId="voting"
maxWidth={100}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Voting')}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="uppercase flex h-full items-center justify-center pt-2">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
}}
/>
<AgGridColumn
colId="cDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Closing date')}
field="terms.closingDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.closingDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="eDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Enactment date')}
field="terms.enactmentDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.enactmentDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="actions"
minWidth={window.innerWidth > BREAKPOINT_MD ? 221 : 80}
maxWidth={221}
sortable={false}
filter={false}
resizable={false}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data?.id || '')
);
const openDialog = () => {
if (!data) return;
setDialog({
open: true,
title: data.rationale.title,
content: data.terms,
});
};
return (
<div className="pb-1">
<button
className="underline max-md:hidden"
onClick={openDialog}
>
{t('View terms')}
</button>{' '}
<ExternalLink className="max-md:hidden" href={proposalPage}>
{t('Open in Governance')}
</ExternalLink>
<ExternalLink className="md:hidden" href={proposalPage}>
{t('Open')}
</ExternalLink>
</div>
);
}}
/>
</AgGrid>
<JsonViewerDialog
open={dialog.open}
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
title={dialog.title}
content={dialog.content}
/>
</>
);
};
@@ -1,43 +0,0 @@
import { useAssetDataProvider } from '@vegaprotocol/assets';
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { AssetLink } from '../links';
export type DecimalSource = 'ASSET';
export type SizeInAssetProps = {
assetId: string;
size?: string | number;
decimalSource?: DecimalSource;
};
/**
* Given a market ID and an order size it will fetch the market
* order size, and format the size accordingly
*/
const SizeInAsset = ({
assetId,
size,
decimalSource = 'ASSET',
}: SizeInAssetProps) => {
const { data } = useAssetDataProvider(assetId);
if (!size) {
return <span>-</span>;
}
let label = size;
if (data) {
if (decimalSource === 'ASSET' && data.decimals) {
label = addDecimalsFormatNumber(size, data.decimals);
}
}
return (
<p>
<span>{label}</span>&nbsp;
<AssetLink assetId={assetId} showAssetSymbol={true} asDialog={true} />
</p>
);
};
export default SizeInAsset;
@@ -3,7 +3,7 @@ import { useExplorerMarketQuery } from '../links/market-link/__generated__/Marke
export type DecimalSource = 'MARKET';
export type SizeInMarketProps = {
export type PriceInMarketProps = {
marketId: string;
size?: string | number;
decimalSource?: DecimalSource;
@@ -17,7 +17,7 @@ const SizeInMarket = ({
marketId,
size,
decimalSource = 'MARKET',
}: SizeInMarketProps) => {
}: PriceInMarketProps) => {
const { data } = useExplorerMarketQuery({
variables: { id: marketId },
fetchPolicy: 'cache-first',
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
type Deposit = components['schemas']['vegaBuiltinAssetDeposit'];
const fullMock: Deposit = {
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
partyId: 'party123',
vegaAssetId: 'asset123',
amount: 'amount123',
};
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
type Withdrawal = components['schemas']['vegaBuiltinAssetWithdrawal'];
const fullMock: Withdrawal = {
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
partyId: 'party123',
vegaAssetId: 'asset123',
amount: 'amount123',
};
@@ -67,12 +67,11 @@ describe('Chain Event: Builtin asset withdrawal', () => {
expect(screen.getByText(`${fullMock.amount}`)).toBeInTheDocument();
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
const partyLink = screen.getByText(`${fullMock.partyId}`);
expect(partyLink).toBeInTheDocument();
if (!partyLink.parentElement) {
throw new Error('Party link does not exist');
}
expect(partyLink.parentElement.tagName).toEqual('A');
expect(partyLink.parentElement.getAttribute('href')).toEqual(
`/parties/${fullMock.partyId}`
@@ -10,12 +10,10 @@ import { TxDetailsChainEventDeposit } from './tx-erc20-deposit';
type Deposit = components['schemas']['vegaERC20Deposit'];
const fullMock: Deposit = {
vegaAssetId:
'0000000000000000000000000000000000000000000000000000000000000002',
vegaAssetId: 'asset123',
amount: 'amount123',
sourceEthereumAddress: 'eth123',
targetPartyId:
'0000000000000000000000000000000000000000000000000000000000000001',
targetPartyId: 'vega123',
};
describe('Chain Event: ERC20 asset deposit', () => {
@@ -13,8 +13,7 @@ const fullMock: Deposit = {
amount: 'amount123',
blockTime: 'block123',
ethereumAddress: 'eth123',
vegaPublicKey:
'0000000000000000000000000000000000000000000000000000000000000001',
vegaPublicKey: 'vega123',
};
describe('Chain Event: Stake deposit', () => {
@@ -13,8 +13,7 @@ const fullMock: Remove = {
amount: 'amount123',
blockTime: 'block123',
ethereumAddress: 'eth123',
vegaPublicKey:
'0000000000000000000000000000000000000000000000000000000000000001',
vegaPublicKey: 'vega123',
};
describe('Chain Event: Stake remove', () => {
@@ -36,7 +36,7 @@ export const ChainResponseCode = ({
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
return (
<div title={`Response code: ${code} - ${label}`} className="inline-block">
<div title={`Response code: ${code} - ${label}`}>
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
@@ -14,14 +14,10 @@ interface TxDetailsSharedProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
// A transitional property used in some complex TX types to display more detailed type information
// than the shared component can derive
hideTypeRow?: boolean;
}
// Applied to all header cells
export const sharedHeaderProps = {
const sharedHeaderProps = {
// Ensures that multi line contents still have the header aligned to the first line
className: 'align-top',
};
@@ -35,7 +31,6 @@ export const TxDetailsShared = ({
txData,
pubKey,
blockData,
hideTypeRow = false,
}: TxDetailsSharedProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
@@ -46,12 +41,10 @@ export const TxDetailsShared = ({
return (
<>
{hideTypeRow === false ? (
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{txData.type}</TableCell>
</TableRow>
) : null}
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{txData.type}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Hash')}</TableCell>
<TableCell>
@@ -1,138 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { PartyLink } from '../../../../links';
import {
SPECIAL_CASE_NETWORK,
SPECIAL_CASE_NETWORK_ID,
} from '../../../../links/party-link/party-link';
import SizeInAsset from '../../../../size-in-asset/size-in-asset';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { AccountType } from '@vegaprotocol/types';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { Transfer } from '../transfer-details';
interface TransferParticipantsProps {
transfer: Transfer;
from: string;
}
/**
* Renders a box containing the To, From and amount of a
* transfer. This is shown for all transfers, including
* recurring and reward transfers.
*
* @param transfer A recurring transfer object
* @param from The sender is not in the transaction, but comes from the Transaction submitter
*/
export function TransferParticipants({
transfer,
from,
}: TransferParticipantsProps) {
// This mapping is required as the global account types require a type to be set, while
// the underlying protobufs allow for every field to be undefined.
const fromAcct =
transfer.fromAccountType &&
transfer.fromAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? AccountType[transfer.fromAccountType]
: AccountType.ACCOUNT_TYPE_GENERAL;
const fromAccountTypeLabel = transfer.fromAccountType
? AccountTypeMapping[fromAcct]
: 'Unknown';
const toAcct =
transfer.toAccountType &&
transfer.toAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? AccountType[transfer.toAccountType]
: AccountType.ACCOUNT_TYPE_GENERAL;
const toAccountTypeLabel = transfer.fromAccountType
? AccountTypeMapping[toAcct]
: 'Unknown';
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Transfer')}</h2>
<div className="relative block rounded-lg py-6 text-center">
<PartyLink id={from} truncate={true} />
<Tooltip
description={
<p>{`${t('From account')}: ${fromAccountTypeLabel}`}</p>
}
>
<span>
<Icon className="ml-3" name={'bank-account'} />
</span>
</Tooltip>
<br />
{/* This block of divs is used to render the inset arrow containing the transfer amount */}
<div className="bg-vega-light-200 dark:vega-dark-200 flex items-center justify-center my-4 relative">
<div className="bg-vega-light-200 dark:bg-vega-dark-200 border w-full pt-5 pb-3 px-3 border-vega-light-200 dark:border-vega-dark-150 relative">
<div className="text-xs z-20 relative leading-none">
{transfer.asset ? (
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
) : null}
</div>
{/* Empty divs for the top arrow and the bottom arrow of the transfer inset */}
<div className="z-10 absolute top-[-1px] left-1/2 w-4 h-4">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-black"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
</div>
<div className="z-10 absolute bottom-[-16px] left-1/2 w-4 h-4">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-vega-dark-200"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
</div>
{/*
<div className="z-10 absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rotate-45 w-4 h-4 dark:border-vega-dark-200 border-vega-light-200 bg-white dark:bg-black border-r border-b"></div>
<div className="z-10 absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-1/2 rotate-45 w-4 h-4 border-vega-light-200 dark:border-vega-dark-200 bg-vega-light-200 dark:bg-vega-dark-200 border-r border-b"></div>
*/}
</div>
</div>
<TransferRecurringRecipient to={transfer.to} />
<Tooltip
description={<p>{`${t('To account')}: ${toAccountTypeLabel}`}</p>}
>
<span>
<Icon className="ml-3" name={'bank-account'} />
</span>
</Tooltip>
<br />
</div>
</div>
);
}
interface TransferRecurringRecipientProps {
to?: string;
}
/**
* If the transfer is to 000...000, then this is a transfer to the
* Rewards Pool rather than the network. This component saves this
* logic from complicating the To section of the participants block
*
* @param markets String[] IDs of markets for this dispatch strategy
*/
export function TransferRecurringRecipient({
to,
}: TransferRecurringRecipientProps) {
if (to === SPECIAL_CASE_NETWORK || to === SPECIAL_CASE_NETWORK_ID) {
return <span>{t('Rewards pool')}</span>;
} else if (to) {
return <PartyLink id={to} truncate={true} />;
}
// Fallback should not happen
return null;
}
@@ -1,86 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { Icon } from '@vegaprotocol/ui-toolkit';
import EpochOverview from '../../../../epoch-overview/epoch';
import { useExplorerFutureEpochQuery } from '../../../../epoch-overview/__generated__/Epoch';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import type { Recurring } from '../transfer-details';
interface TransferRepeatProps {
recurring: Recurring;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferRepeat({ recurring }: TransferRepeatProps) {
const { data } = useExplorerFutureEpochQuery();
if (!recurring) {
return null;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Active epochs')}</h2>
<div className="relative block rounded-lg py-6 text-center p-6">
<p>
<EpochOverview id={recurring.startEpoch} />
</p>
<p className="leading-10 my-2">
<IconForEpoch
start={recurring.startEpoch}
end={recurring.endEpoch}
current={data?.epoch.id}
/>
</p>
<p>
{recurring.endEpoch ? (
<EpochOverview id={recurring.endEpoch} />
) : (
<span>{t('Forever')}</span>
)}
</p>
</div>
</div>
);
}
export type IconForTransferProps = {
current?: string;
start?: string;
end?: string;
};
/**
* Pick an icon rto represent the state of the repetition for this recurring
* transfer. It can be unstarted, in progress, or complete.
*
* @param start The epoch in which the transfer first occurs
* @param end The last epoch in which the transfer occurs
* @param current The current epoch
*/
function IconForEpoch({ start, end, current }: IconForTransferProps) {
let i: IconProps['name'] = 'repeat';
if (current && start && end) {
const startEpoch = parseInt(start);
const endEpoch = parseInt(end);
const currentEpoch = parseInt(current);
if (currentEpoch > endEpoch) {
// If we've finished
i = 'updated';
} else if (startEpoch > currentEpoch) {
// If we haven't yet started
i = 'time';
} else if (startEpoch < currentEpoch && endEpoch > currentEpoch) {
i = 'repeat';
}
}
return <Icon name={i} className="mr-2" />;
}
@@ -1,97 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { AssetLink, MarketLink } from '../../../../links';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { components } from '../../../../../../types/explorer';
import type { Recurring } from '../transfer-details';
import { DispatchMetricLabels } from '@vegaprotocol/types';
export type Metric = components['schemas']['vegaDispatchMetric'];
export type Strategy = components['schemas']['vegaDispatchStrategy'];
const metricLabels = {
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
...DispatchMetricLabels,
};
interface TransferRewardsProps {
recurring: Recurring;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferRewards({ recurring }: TransferRewardsProps) {
const metric =
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
if (!recurring || !recurring.dispatchStrategy) {
return null;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Reward metrics')}</h2>
<ul className="relative block rounded-lg py-6 text-center p-6">
{recurring.dispatchStrategy.assetForMetric ? (
<li>
<strong>{t('Asset')}</strong>:{' '}
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {metricLabels[metric]}
</li>
{recurring.dispatchStrategy.markets &&
recurring.dispatchStrategy.markets.length > 0 ? (
<li>
<strong>{t('Markets in scope')}</strong>:
<ul>
{recurring.dispatchStrategy.markets.map((m) => (
<li key={m}>
<MarketLink id={m} />
</li>
))}
</ul>
</li>
) : null}
<li>
<strong>{t('Factor')}</strong>: {recurring.factor}
</li>
</ul>
</div>
);
}
interface TransferRecurringStrategyProps {
strategy: Strategy;
}
/**
* Simple renderer for a dispatch strategy in a recurring transfer
*
* @param strategy Dispatch strategy object
*/
export function TransferRecurringStrategy({
strategy,
}: TransferRecurringStrategyProps) {
if (!strategy) {
return null;
}
return (
<>
{strategy.assetForMetric ? (
<li>
<strong>{t('Asset for metric')}</strong>:{' '}
<AssetLink assetId={strategy.assetForMetric} />
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {strategy.metric}
</li>
</>
);
}
@@ -1,39 +0,0 @@
import type { components } from '../../../../../types/explorer';
import { TransferRepeat } from './blocks/transfer-repeat';
import { TransferRewards } from './blocks/transfer-rewards';
import { TransferParticipants } from './blocks/transfer-participants';
export type Recurring = components['schemas']['v1RecurringTransfer'];
export type Metric = components['schemas']['vegaDispatchMetric'];
export const wrapperClasses =
'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha';
export type Transfer = components['schemas']['commandsv1Transfer'];
interface TransferDetailsProps {
transfer: Transfer;
from: string;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferDetails({ transfer, from }: TransferDetailsProps) {
const recurring = transfer.recurring;
return (
<div className="flex gap-5 flex-wrap">
<TransferParticipants from={from} transfer={transfer} />
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
{recurring && recurring.dispatchStrategy ? (
<TransferRewards recurring={transfer.recurring} />
) : null}
</div>
);
}
@@ -23,7 +23,6 @@ import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
import { TxDetailsNodeAnnounce } from './tx-node-announce';
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
import { TxDetailsTransfer } from './tx-transfer';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -109,8 +108,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsUndelegate;
case 'State Variable Proposal':
return TxDetailsStateVariable;
case 'Transfer Funds':
return TxDetailsTransfer;
default:
return TxDetailsGeneric;
}
@@ -1,125 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
import { TableRow, TableCell, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import { PartyLink } from '../../links';
import SizeInAsset from '../../size-in-asset/size-in-asset';
import { TransferDetails } from './transfer/transfer-details';
import {
SPECIAL_CASE_NETWORK,
SPECIAL_CASE_NETWORK_ID,
} from '../../links/party-link/party-link';
type Transfer = components['schemas']['commandsv1Transfer'];
interface TxDetailsNodeAnnounceProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Displays the details of a transfer. Broadly there are three distinct
* types of transfer, listed below in order of complexity:
*
* - A one off transfer
* - A recurring transfer
* - A recurring rewards pool transfer
*
* One off transfers are simple, really the important data is the amount
* and who sent it to whom. This is rendered as one distinct box.
*
* A recurring transfer has two components - the same as above, and an
* additional box that shows details about how it repeats. This is defined
* as a start epoch and and end epoch. The Epoch/MissingEpoch components
* render slightly differently depending on if the epoch is in the past,
* current or in the future.
*
* Finally rewards pool transfers get the two boxes above, and an additional
* one that describes how the reward is distributed.
*
* The information is split up in to three boxes to allow for the reuse across
* all the types of transfer above.
*/
export const TxDetailsTransfer = ({
txData,
pubKey,
blockData,
}: TxDetailsNodeAnnounceProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const transfer: Transfer = txData.command.transfer;
if (!transfer) {
return <>{t('Transfer data missing')}</>;
}
const from = txData.submitter;
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{getTypeLabelForTransfer(transfer)}</TableCell>
</TableRow>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
hideTypeRow={true}
/>
{from ? (
<TableRow modifier="bordered">
<TableCell>{t('From')}</TableCell>
<TableCell>
<PartyLink id={from} />
</TableCell>
</TableRow>
) : null}
{transfer.to ? (
<TableRow modifier="bordered">
<TableCell>{t('To')}</TableCell>
<TableCell>
<PartyLink id={transfer.to} />
</TableCell>
</TableRow>
) : null}
{transfer.asset && transfer.amount ? (
<TableRow modifier="bordered">
<TableCell>{t('Amount')}</TableCell>
<TableCell>
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
</TableCell>
</TableRow>
) : null}
</TableWithTbody>
<TransferDetails from={from} transfer={transfer} />
</>
);
};
/**
* Gets a string description of this transfer
* @param txData A full transfer
* @returns string Transfer label
*/
export function getTypeLabelForTransfer(tx: Transfer) {
if (tx.to === SPECIAL_CASE_NETWORK || tx.to === SPECIAL_CASE_NETWORK_ID) {
if (tx.recurring && tx.recurring.dispatchStrategy) {
return 'Reward top up transfer';
}
// Else: we don't know that it's a reward transfer, so let's not guess
} else if (tx.recurring) {
return 'Recurring transfer';
} else if (tx.oneOff) {
// Currently redundant, but could be used to indicate something more specific
return 'Transfer';
}
return 'Transfer';
}
@@ -14,17 +14,14 @@ interface StringMap {
// Using https://github.com/vegaprotocol/protos/blob/e0f646ce39aab1fc66a9200ceec0262306d3beb3/commands/transaction.go#L93 as a reference
const displayString: StringMap = {
OrderSubmission: 'Order Submission',
'Submit Order': 'Order',
OrderCancellation: 'Order Cancellation',
OrderAmendment: 'Order Amendment',
VoteSubmission: 'Vote Submission',
WithdrawSubmission: 'Withdraw Submission',
Withdraw: 'Withdraw Request',
LiquidityProvisionSubmission: 'LP order',
'Liquidity Provision Order': 'LP order',
LiquidityProvisionCancellation: 'LP cancel',
LiquidityProvisionAmendment: 'LP update',
'Amend LiquidityProvision Order': 'Amend LP',
LiquidityProvisionSubmission: 'Liquidity Provision',
LiquidityProvisionCancellation: 'Liquidity Cancellation',
LiquidityProvisionAmendment: 'Liquidity Amendment',
ProposalSubmission: 'Governance Proposal',
AnnounceNode: 'Node Announcement',
NodeVote: 'Node Vote',
@@ -34,11 +31,10 @@ const displayString: StringMap = {
DelegateSubmission: 'Delegation',
UndelegateSubmission: 'Undelegation',
KeyRotateSubmission: 'Key Rotation',
StateVariableProposal: 'State Variable',
StateVariableProposal: 'State Variable Proposal',
Transfer: 'Transfer',
CancelTransfer: 'Cancel Transfer',
ValidatorHeartbeat: 'Heartbeat',
'Batch Market Instructions': 'Batch',
ValidatorHeartbeat: 'Validator Heartbeat',
};
/**
@@ -136,8 +132,7 @@ export function getLabelForChainEvent(
export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
let type = displayString[orderType] || orderType;
let colours =
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
let colours = 'text-white dark:text-white bg-zinc-800 dark:bg-zinc-800';
// This will get unwieldy and should probably produce a different colour of tag
if (type === 'Chain Event' && !!command?.chainEvent) {
@@ -1,58 +0,0 @@
import { getTypeLabelForTransfer } from './details/tx-transfer';
import type { components } from '../../../types/explorer';
type Transfer = components['schemas']['commandsv1Transfer'];
describe('TX: Transfer: getLabelForTransfer', () => {
it('renders reward top up label if the TO party is 000', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000000',
recurring: {
dispatchStrategy: {},
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Reward top up transfer');
});
it('renders reward top up label if the TO party is network', () => {
const mock = {
to: 'network',
recurring: {
dispatchStrategy: {},
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Reward top up transfer');
});
it('renders recurring label if the tx has a recurring property', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000001',
recurring: {
startEpoch: '0',
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Recurring transfer');
});
it('renders one off label if the tx has a oneOff property', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000001',
oneOff: {
deliverOn: '0',
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
});
it('renders one off label otherwise', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000001',
};
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
});
});
@@ -37,9 +37,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="tx-hash"
>
<span className="xl:hidden uppercase text-vega-dark-300">
ID:&nbsp;
</span>
<span className="xl:hidden uppercase text-zinc-500">ID:&nbsp;</span>
<TruncatedLink
to={`/${Routes.TX}/${toHex(hash)}`}
text={hash}
@@ -51,9 +49,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="pub-key"
>
<span className="xl:hidden uppercase text-vega-dark-300">
By:&nbsp;
</span>
<span className="xl:hidden uppercase text-zinc-500">By:&nbsp;</span>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
@@ -68,9 +64,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-3 xl:col-span-1 leading-none flex items-center"
data-testid="tx-block"
>
<span className="xl:hidden uppercase text-vega-dark-300">
Block:&nbsp;
</span>
<span className="xl:hidden uppercase text-zinc-500">Block:&nbsp;</span>
<TruncatedLink
to={`/${Routes.BLOCKS}/${block}`}
text={block}
@@ -82,7 +76,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-2 xl:col-span-1 leading-none flex items-center"
data-testid="tx-success"
>
<span className="xl:hidden uppercase text-vega-dark-300">
<span className="xl:hidden uppercase text-zinc-500">
Success:&nbsp;
</span>
{isNumber(code) ? (
@@ -94,7 +94,7 @@ export const TxsInfiniteList = ({
return (
<div className={className} data-testid="transactions-list">
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-zinc-500 uppercase">
<div className="col-span-3">
<span className="hidden xl:inline">Transaction &nbsp;</span>
<span>ID</span>
-3
View File
@@ -33,7 +33,4 @@ export const ENV = {
parties: truthy.includes(windowOrDefault('NX_EXPLORER_PARTIES')),
validators: truthy.includes(windowOrDefault('NX_EXPLORER_VALIDATORS')),
},
addresses: {
feedback: windowOrDefault('NX_GITHUB_FEEDBACK_URL'),
},
};
@@ -1,50 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { RouteTitle } from '../../components/route-title';
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { useParams } from 'react-router-dom';
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
import { useState } from 'react';
export const AssetPage = () => {
useDocumentTitle(['Assets']);
useScrollToLocation();
const { assetId } = useParams<{ assetId: string }>();
const { data, loading, error } = useAssetDataProvider(assetId || '');
const title = data ? data.name : error ? t('Asset not found') : '';
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
return (
<>
<section className="relative">
<RouteTitle data-testid="asset-header">{title}</RouteTitle>
<AsyncRenderer
noDataMessage={t('Asset not found')}
data={data}
loading={loading}
error={error}
>
<div className="absolute top-0 right-0">
<Button size="xs" onClick={() => setDialogOpen(true)}>
{t('View JSON')}
</Button>
</div>
<div className="h-full relative">
<AssetDetailsTable asset={data as AssetFieldsFragment} />
</div>
</AsyncRenderer>
</section>
<JsonViewerDialog
open={dialogOpen}
onChange={(isOpen) => setDialogOpen(isOpen)}
title={data?.name || ''}
content={data}
/>
</>
);
};
@@ -6,7 +6,7 @@ import { useDocumentTitle } from '../../hooks/use-document-title';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { AssetsTable } from '../../components/assets/assets-table';
export const AssetsPage = () => {
export const Assets = () => {
useDocumentTitle(['Assets']);
useScrollToLocation();
@@ -1,2 +1 @@
export * from './assets-page';
export * from './asset-page';
export * from './assets';
@@ -0,0 +1,82 @@
query ExplorerProposals {
proposalsConnection {
edges {
node {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
instrument {
name
}
}
... on UpdateMarket {
marketId
}
... on NewAsset {
__typename
symbol
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
}
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
}
}
votes {
yes {
totalTokens
totalNumber
votes {
value
party {
id
stakingSummary {
currentStakeAvailable
}
}
datetime
}
}
no {
totalTokens
totalNumber
votes {
value
party {
id
stakingSummary {
currentStakeAvailable
}
}
datetime
}
}
}
}
}
}
}
@@ -0,0 +1,122 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerProposalsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'NewAsset', symbol: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string } } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, votes?: Array<{ __typename?: 'Vote', value: Types.VoteValue, datetime: any, party: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } }> | null }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, votes?: Array<{ __typename?: 'Vote', value: Types.VoteValue, datetime: any, party: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } }> | null } } } } | null> | null } | null };
export const ExplorerProposalsDocument = gql`
query ExplorerProposals {
proposalsConnection {
edges {
node {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
instrument {
name
}
}
... on UpdateMarket {
marketId
}
... on NewAsset {
__typename
symbol
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
}
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
}
}
votes {
yes {
totalTokens
totalNumber
votes {
value
party {
id
stakingSummary {
currentStakeAvailable
}
}
datetime
}
}
no {
totalTokens
totalNumber
votes {
value
party {
id
stakingSummary {
currentStakeAvailable
}
}
datetime
}
}
}
}
}
}
}
`;
/**
* __useExplorerProposalsQuery__
*
* To run a query within a React component, call `useExplorerProposalsQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerProposalsQuery` 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 } = useExplorerProposalsQuery({
* variables: {
* },
* });
*/
export function useExplorerProposalsQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>(ExplorerProposalsDocument, options);
}
export function useExplorerProposalsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>(ExplorerProposalsDocument, options);
}
export type ExplorerProposalsQueryHookResult = ReturnType<typeof useExplorerProposalsQuery>;
export type ExplorerProposalsLazyQueryHookResult = ReturnType<typeof useExplorerProposalsLazyQuery>;
export type ExplorerProposalsQueryResult = Apollo.QueryResult<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>;
@@ -1 +1,63 @@
export * from './proposals-page';
import { t } from '@vegaprotocol/react-helpers';
import React from 'react';
import { RouteTitle } from '../../components/route-title';
import { SubHeading } from '../../components/sub-heading';
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalsQuery } from './__generated__/Proposals';
import { useDocumentTitle } from '../../hooks/use-document-title';
import EmptyList from '../../components/empty-list/empty-list';
const Governance = () => {
const { data, loading } = useExplorerProposalsQuery({
errorPolicy: 'ignore',
});
useDocumentTitle();
if (!data || !data.proposalsConnection || !data.proposalsConnection.edges) {
if (!loading) {
return (
<section>
<RouteTitle data-testid="governance-header">
{t('Governance Proposals')}
</RouteTitle>
<EmptyList
heading={t('This chain has no proposals')}
label={t('0 proposals')}
/>
</section>
);
} else {
return <Loader />;
}
}
const proposals = data?.proposalsConnection?.edges.map((e) => {
return e?.node;
});
return (
<section>
<RouteTitle data-testid="governance-header">
{t('Governance Proposals')}
</RouteTitle>
{proposals.map((p) => {
if (!p || !p.id) {
return null;
}
return (
<React.Fragment key={p.id}>
<SubHeading>
{p.rationale.title || p.rationale.description}
</SubHeading>
<SyntaxHighlighter data={p} />
</React.Fragment>
);
})}
</section>
);
};
export default Governance;
@@ -1,33 +0,0 @@
import { proposalsDataProvider } from '@vegaprotocol/governance';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { ProposalsTable } from '../../components/proposals/proposals-table';
import { RouteTitle } from '../../components/route-title';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
export const Proposals = () => {
useScrollToLocation();
const { data, loading, error } = useDataProvider({
dataProvider: proposalsDataProvider,
});
useDocumentTitle([t('Governance Proposals')]);
return (
<section>
<RouteTitle data-testid="proposals-heading">
{t('Governance proposals')}
</RouteTitle>
<AsyncRenderer
noDataMessage={t('This chain has no proposals')}
data={data}
loading={loading}
error={error}
>
<ProposalsTable data={data} />
</AsyncRenderer>
</section>
);
};
@@ -68,7 +68,7 @@ const Party = () => {
return (
<section>
<h1
className="font-alpha uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
className="font-alpha uppercase font-xl mb-4 text-zinc-800 dark:text-zinc-200"
data-testid="parties-header"
>
{t('Public key')}
+4 -13
View File
@@ -1,6 +1,6 @@
import { AssetPage, AssetsPage } from './assets';
import { Assets } from './assets';
import BlockPage from './blocks';
import { Proposals } from './governance';
import Governance from './governance';
import Home from './home';
import OraclePage from './oracles';
import Oracles from './oracles/home';
@@ -53,16 +53,7 @@ const assetsRoutes: Route[] = flags.assets
path: Routes.ASSETS,
text: t('Assets'),
name: 'Assets',
children: [
{
index: true,
element: <AssetsPage />,
},
{
path: ':assetId',
element: <AssetPage />,
},
],
element: <Assets />,
},
]
: [];
@@ -84,7 +75,7 @@ const governanceRoutes: Route[] = flags.governance
path: Routes.GOVERNANCE,
name: 'Governance proposals',
text: t('Governance Proposals'),
element: <Proposals />,
element: <Governance />,
},
]
: [];
+21 -62
View File
@@ -15,7 +15,6 @@ type OneOf<T extends any[]> = T extends [infer Only]
? OneOf<[XOR<A, B>, ...Rest]>
: never;
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface paths {
'/info': {
/**
@@ -41,6 +40,8 @@ export interface paths {
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/**
@@ -65,7 +66,7 @@ export interface components {
| 'OPERATOR_LESS_THAN'
| 'OPERATOR_LESS_THAN_OR_EQUAL';
/**
* The supported oracle sources
* The supported Oracle sources
* @description - ORACLE_SOURCE_UNSPECIFIED: The default value
* - ORACLE_SOURCE_OPEN_ORACLE: Specifies that the payload will be base64 encoded JSON conforming to the Open Oracle standard
* - ORACLE_SOURCE_JSON: Specifies that the payload will be base64 encoded JSON, but does not specify the shape of the data
@@ -173,7 +174,7 @@ export interface components {
readonly '@type'?: string;
[key: string]: unknown | undefined;
};
/** Used to announce a node as a new pending validator */
/** Used announce a node as a new pending validator */
readonly v1AnnounceNode: {
/** AvatarURL of the validator */
readonly avatarUrl?: string;
@@ -268,13 +269,13 @@ export interface components {
readonly v1ETHAddress: {
readonly address?: string;
};
/** A transaction to allow a validator to rotate their ethereum keys */
/** A transaction to allow validator to rotate their ethereum keys */
readonly v1EthereumKeyRotateSubmission: {
/** Currently used public address */
readonly currentAddress?: string;
/** Signature that can be verified using the new ethereum address */
readonly ethereumSignature?: components['schemas']['v1Signature'];
/** The new address to rotate to */
/** The new adress to rotate to */
readonly newAddress?: string;
/** Ethereum public key to use as a submitter to allow automatic signature generation */
readonly submitterAddress?: string;
@@ -308,9 +309,7 @@ export interface components {
readonly version?: string;
};
readonly v1InputData: {
/** A command used by a node operator to announce its node as a pending validator */
readonly announceNode?: components['schemas']['v1AnnounceNode'];
/** A command to submit a batch of order instructions to a market */
readonly batchMarketInstructions?: components['schemas']['v1BatchMarketInstructions'];
/**
* Format: uint64
@@ -324,35 +323,17 @@ export interface components {
* `block_height` prevents replay attacks in conjunction with `nonce` (see above).
*/
readonly blockHeight?: string;
/** A command to request cancelling a recurring transfer */
readonly cancelTransfer?: components['schemas']['v1CancelTransfer'];
/**
* Command used by a validator to submit an event forwarded to the Vega network to provide information
* on events happening on other networks, to be used by a foreign chain
* to recognise a decision taken by the Vega network
*/
readonly chainEvent?: components['schemas']['v1ChainEvent'];
/** Command to delegate tokens to a validator */
readonly delegateSubmission?: components['schemas']['v1DelegateSubmission'];
/** Command used by a validator to allow given validator to rotate their Ethereum keys */
readonly ethereumKeyRotateSubmission?: components['schemas']['v1EthereumKeyRotateSubmission'];
/** Command used by a validator to submit signatures to a smart contract */
readonly issueSignatures?: components['schemas']['v1IssueSignatures'];
/** Command used by a validator to allow given validator to rotate their Vega keys */
readonly keyRotateSubmission?: components['schemas']['v1KeyRotateSubmission'];
/** Command to request amending a liquidity commitment */
readonly liquidityProvisionAmendment?: components['schemas']['v1LiquidityProvisionAmendment'];
/** Command to request cancelling a liquidity commitment */
readonly liquidityProvisionCancellation?: components['schemas']['v1LiquidityProvisionCancellation'];
/** Command to submit a liquidity commitment */
readonly liquidityProvisionSubmission?: components['schemas']['v1LiquidityProvisionSubmission'];
/** Command used by a validator to submit a signature, to be used by a foreign chain to recognise a decision taken by the Vega network */
readonly nodeSignature?: components['schemas']['v1NodeSignature'];
/**
* Validator commands
* Command used by a validator when a node votes for validating that a given resource exists or is valid,
* for example, an ERC20 deposit is valid and exists on ethereum
*/
/** Validator commands */
readonly nodeVote?: components['schemas']['v1NodeVote'];
/**
* Format: uint64
@@ -368,50 +349,30 @@ export interface components {
* slightly differently, causing a different hash.
*/
readonly nonce?: string;
/**
* Oracles
* Command to submit new oracle data from third party providers
*/
/** Oracles */
readonly oracleDataSubmission?: components['schemas']['v1OracleDataSubmission'];
/** Command to amend an order */
readonly orderAmendment?: components['schemas']['v1OrderAmendment'];
/**
* User commands
* Command to cancel an order
*/
readonly orderCancellation?: components['schemas']['v1OrderCancellation'];
/** A command for submitting an order */
/** User commands */
readonly orderSubmission?: components['schemas']['v1OrderSubmission'];
/** Command to submit a governance proposal */
readonly proposalSubmission?: components['schemas']['v1ProposalSubmission'];
/** Command used by a validator to propose a protocol upgrade */
readonly protocolUpgradeProposal?: components['schemas']['v1ProtocolUpgradeProposal'];
/** Command used by a validator to submit a floating point value */
readonly stateVariableProposal?: components['schemas']['v1StateVariableProposal'];
/** Command to submit a transfer */
readonly transfer?: components['schemas']['commandsv1Transfer'];
/** Command to remove tokens delegated to a validator */
readonly undelegateSubmission?: components['schemas']['v1UndelegateSubmission'];
/**
* Command used by a validator to signal they are still online and validating blocks
* or ready to validate blocks when they are still a pending validator
*/
readonly validatorHeartbeat?: components['schemas']['v1ValidatorHeartbeat'];
/** Command to submit a vote on a governance proposal */
readonly voteSubmission?: components['schemas']['v1VoteSubmission'];
/** Command to submit a withdrawal */
readonly withdrawSubmission?: components['schemas']['v1WithdrawSubmission'];
};
/** A transaction for a validator to submit signatures to a smart contract */
readonly v1IssueSignatures: {
/** The kind of signatures to generate, namely for whether a signer is being added or removed */
readonly kind?: components['schemas']['v1NodeSignatureKind'];
/** The ethereum address which will submit the signatures to the smart contract */
/** The ethereum address which will submit the signatures to the smart-contract */
readonly submitter?: string;
/** The ID of the node that will be signed in or out of the smart contract */
/** The ID of the node that will be signed in or out of the smartcontract */
readonly validatorNodeId?: string;
};
/** A transaction to allow a validator to rotate their Vega keys */
/** A transaction to allow validator to rotate their Vega keys */
readonly v1KeyRotateSubmission: {
/** Hash of currently used public key */
readonly currentPubKeyHash?: string;
@@ -535,8 +496,8 @@ export interface components {
/** Specific details for a one off transfer */
readonly v1OneOffTransfer: {
/**
* A unix timestamp in seconds. Time at which the
* transfer should be delivered into the To account
* A unix timestamp in second. Time at which the
* transfer should be delivered in the to account
* Format: int64
*/
readonly deliverOn?: string;
@@ -551,7 +512,7 @@ export interface components {
readonly payload?: string;
/**
* @description The source from which the data is coming from. Must be base64 encoded.
* Oracle data is a type of external data source data.
* Oracle data a type of external data source data.
*/
readonly source?: components['schemas']['OracleDataSubmissionOracleSource'];
};
@@ -642,7 +603,7 @@ export interface components {
/** Type for the order, required field - See `Order.Type` */
readonly type?: components['schemas']['vegaOrderType'];
};
/** @description PropertyKey describes the property key contained in data source data. */
/** @description PropertyKey describes the property key contained in an data source data. */
readonly v1PropertyKey: {
/** @description name is the name of the property. */
readonly name?: string;
@@ -689,7 +650,6 @@ export interface components {
/** Proposal configuration and the actual change that is meant to be executed when proposal is enacted */
readonly terms?: components['schemas']['vegaProposalTerms'];
};
/** A transaction for a validator to suggest a protocol upgrade */
readonly v1ProtocolUpgradeProposal: {
/**
* The block height at which to perform the upgrade
@@ -748,7 +708,6 @@ export interface components {
*/
readonly pubKey?: components['schemas']['v1PubKey'];
};
/** A transaction for a validator to submit a floating point value */
readonly v1StateVariableProposal: {
/** The state value proposal details */
readonly proposal?: components['schemas']['vegaStateValueProposal'];
@@ -1010,7 +969,7 @@ export interface components {
readonly sourceEthereumAddress?: string;
/** The Vega network internal identifier of the asset */
readonly vegaAssetId?: string;
/** The updated withdrawal threshold */
/** The updated withdraw threshold */
readonly withdrawThreshold?: string;
};
/** An asset allow-listing for an ERC20 token */
@@ -1105,7 +1064,7 @@ export interface components {
/** The ethereum address of the old signer */
readonly oldSigner?: string;
};
/** The threshold has been updated on the multisig control */
/** The threshold have been updated on the multisigcontrol */
readonly vegaERC20ThresholdSet: {
/**
* Format: int64
@@ -1119,13 +1078,13 @@ export interface components {
* Format: int64
*/
readonly newThreshold?: number;
/** The nonce created by the Vega network */
/** The nonce create by the vega network */
readonly nonce?: string;
};
readonly vegaERC20Update: {
/**
* The lifetime limits deposit per address.
* This will be interpreted against the asset decimals.
* This is will be interpreted against the asset decimals.
* note: this is a temporary measure that can be changed by governance
*/
readonly lifetimeLimit?: string;
@@ -1278,7 +1237,7 @@ export interface components {
* price levels over which automated liquidity provision orders will be deployed
*/
readonly lpPriceRange?: string;
/** Optional new market metadata, tags */
/** Optional new market meta data, tags */
readonly metadata?: readonly string[];
/**
* Decimal places for order sizes, sets what size the smallest order / position on the market can be
File diff suppressed because it is too large Load Diff
@@ -201,21 +201,12 @@ context('Staking Page - verify elements on page', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
if ($stakePercentage != '-') {
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?!0+(?:\.0+)?%)(?:\d|[1-9]\d|100)(?:(?<!100)\.\d+)?%/
);
}
});
});
@@ -1,4 +1,4 @@
import { act } from '@testing-library/react';
import { act } from '@testing-library/react-hooks';
import { usePendingBalancesStore } from './use-pending-balances-manager';
import type { Event } from 'ethers';
@@ -1,6 +1,7 @@
import uniqBy from 'lodash/uniqBy';
import { create } from 'zustand';
import type { Event } from 'ethers';
import uniqBy from 'lodash/uniqBy';
import create from 'zustand';
export type PendingTxsStore = {
pendingBalances: Event[];
+1 -12
View File
@@ -432,7 +432,6 @@
"associatedWithVegaKeys": "Associated with Vega keys",
"thisEpoch": "This Epoch",
"nextEpoch": "Next epoch",
"toSeeYourRewardsConnectYourWallet": "TO SEE YOUR REWARDS, CONNECT YOUR WALLET",
"rewardsIntro": "Earn rewards and infrastructure fees for trading and maintaining the network.",
"rewardsCallout": "Rewards are credited {{duration}} after the epoch ends.",
"rewardsCalloutDetail": "This delay is set by a network parameter",
@@ -458,7 +457,6 @@
"rewardsColMarketCreationHeader": "MARKET CREATION",
"rewardsColMarketCreationTooltip": "Market creation rewards are paid out to the creator of any market that exceeds a set threshold of cumulative volume in a given epoch, currently [rewards.marketCreationQuantumMultiple]",
"rewardsColTotalHeader": "TOTAL",
"ofTotalDistributed": "of total distributed",
"checkBackSoon": "Check back soon",
"yourStake": "Your stake",
"reward": "Reward",
@@ -744,22 +742,13 @@
"Proposals": "Proposals",
"Validators": "Validators",
"Redeem": "Redeem",
"validatorFormIntro": "To learn more about validators and how scores are calculated,",
"readMoreValidatorForm": "read about staking on Vega",
"StakeDescription": "The total amount $VEGA staked to this validator including self-stake and all delegation.",
"NormalisedVotingPowerDescription": "Voting power is the relative weighting given to the validator in tendermint consensus. It is calculated based on the stake share of the validator minus any penalties due to overstaking or performance.",
"StakeShareDescription": "The stake a validator represents as a share of total stake across the network.",
"TotalPenaltiesDescription": "Total of penalties taking into account performance (considering proportion of blocks proposed against the number of blocks the validator was expected to propose) and any overstaking.",
"PendingStakeDescription": "The amount of stake that will be added or removed from the validator from the next epoch.",
"StakeNeededForPromotionStandbyDescription": "{{prefix}} additional stake needed for promotion to consensus, assuming constant performance in line with previous epoch.",
"StakeNeededForPromotionCandidateDescription": "{{prefix}} additional stake needed for promotion to standby, assuming constant performance in line with previous epoch.",
"StakedByOperatorDescription": "The stake provided as self-stake by the node operator, must be at least the minimum stake as defined by network parameter",
"AboutThisValidatorDescription": "External URL provided by the validator linking to information about themselves",
"ValidatorStatusDescription": "Consensus, Standby or Pending (Candidate), depending on how much stake the validator has attracted",
"StakedByDelegatesDescription": "The stake delegated to the node by other users",
"OverstakedPenaltyDescription": "A penalty applied for having more stake than the optimal stake for the network. Designed to avoid concentration of voting power with a small number of validators",
"PerformancePenaltyDescription": "Performance score is a measure of how often a validator proposed blocks in the last epoch relative to how many they should be expected to propose based on their voting power. Performance penalty is applied for having a performance score of less than 1",
"UnnormalisedVotingPowerDescription": "The voting power of the validator based on their final validator score after all penalties have been applied",
"NormalisedVotingPowerDescription": "The voting power of the validator, adjusted to ensure all validator scores sum to 1, used for distribution of rewards",
"Score": "Score",
"performancePenalty": "Performance penalty",
"overstaked": "Overstaked",
@@ -137,24 +137,6 @@ query Proposal($proposalId: ID!) {
}
}
}
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters {
timeWindow
scalingFactor
}
}
positionDecimalPlaces
lpPriceRange
linearSlippageFactor
quadraticSlippageFactor
}
... on UpdateMarket {
marketId
File diff suppressed because one or more lines are too long
@@ -1,25 +0,0 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../contexts/app-state/app-state-provider';
import { ConnectToSeeRewards } from './connect-to-see-rewards';
describe('ConnectToSeeRewards', () => {
it('should render button correctly', () => {
const { getByTestId } = render(
<AppStateProvider>
<ConnectToSeeRewards />
</AppStateProvider>
);
expect(getByTestId('connect-to-vega-wallet-btn')).toBeInTheDocument();
});
it('should render the correct text', () => {
const { getByText } = render(
<AppStateProvider>
<ConnectToSeeRewards />
</AppStateProvider>
);
expect(
getByText('TO SEE YOUR REWARDS, CONNECT YOUR WALLET')
).toBeInTheDocument();
});
});
@@ -1,40 +0,0 @@
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Button } from '@vegaprotocol/ui-toolkit';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { SubHeading } from '../../components/heading';
export const ConnectToSeeRewards = () => {
const { appDispatch } = useAppState();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { t } = useTranslation();
const classes = classNames(
'flex flex-col items-center justify-center h-[300px] w-full',
'border border-vega-dark-200'
);
return (
<div className={classes}>
<SubHeading title={t('toSeeYourRewardsConnectYourWallet')} />
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
{t('connectVegaWallet')}
</Button>
</div>
);
};
@@ -5,10 +5,9 @@ import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
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';
import { RewardTable } from './reward-table';
export const EpochIndividualRewards = () => {
export const RewardInfo = () => {
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
@@ -31,10 +30,13 @@ export const EpochIndividualRewards = () => {
return removePaginationWrapper(data.party.rewardsConnection.edges);
}, [data]);
const epochIndividualRewardSummaries = useMemo(() => {
if (!data?.party) return [];
return generateEpochIndividualRewardsList(rewards);
}, [data?.party, rewards]);
const delegations = useMemo(() => {
if (!data?.party || !data.party.delegationsConnection?.edges?.length) {
return [];
}
return removePaginationWrapper(data.party.delegationsConnection.edges);
}, [data]);
return (
<AsyncRenderer
@@ -43,18 +45,20 @@ export const EpochIndividualRewards = () => {
data={data}
render={() => (
<div>
<p className="mb-10">
{t('Connected Vega key')}:{' '}
<span className="text-white">{pubKey}</span>
<p>
{t('Connected Vega key')}: {pubKey}
</p>
{epochIndividualRewardSummaries.length ? (
epochIndividualRewardSummaries.map(
(epochIndividualRewardSummary) => (
<EpochIndividualRewardsTable
data={epochIndividualRewardSummary}
{rewards.length ? (
rewards.map((reward, i) => {
if (!reward) return null;
return (
<RewardTable
key={i}
reward={reward}
delegations={delegations || []}
/>
)
)
);
})
) : (
<p>{t('noRewards')}</p>
)}
@@ -0,0 +1,79 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import { BigNumber } from '../../../lib/bignumber';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import { format } from 'date-fns';
import { DATE_FORMAT_DETAILED } from '../../../lib/date-formats';
import type {
DelegationFieldsFragment,
RewardFieldsFragment,
} from '../home/__generated__/Rewards';
interface RewardTableProps {
reward: RewardFieldsFragment;
delegations: DelegationFieldsFragment[] | [];
}
export const RewardTable = ({ reward, delegations }: RewardTableProps) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
// Get your stake for epoch in which you have rewards
const stakeForEpoch = useMemo(() => {
if (!delegations.length) return '0';
const delegationsForEpoch = delegations
.filter((d) => d.epoch.toString() === reward.epoch.id)
.map((d) => toBigNum(d.amount, decimals));
if (delegationsForEpoch.length) {
return BigNumber.sum.apply(null, [
new BigNumber(0),
...delegationsForEpoch,
]);
}
return new BigNumber(0);
}, [decimals, delegations, reward.epoch.id]);
return (
<div className="mb-24">
<h3 className="text-lg text-white mb-4">
{t('Epoch')} {reward.epoch.id}
</h3>
<KeyValueTable>
<KeyValueTableRow>
{t('rewardType')}
<span>{reward.rewardType}</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('yourStake')}
<span>{stakeForEpoch.toString()}</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('reward')}
<span>
{formatNumber(toBigNum(reward.amount, decimals))}{' '}
{reward.asset.symbol}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('shareOfReward')}
<span>
{new BigNumber(reward.percentageOfTotal).dp(2).toString()}%
</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('received')}
<span>
{format(new Date(reward.receivedAt), DATE_FORMAT_DETAILED)}
</span>
</KeyValueTableRow>
</KeyValueTable>
</div>
);
};
@@ -1,63 +0,0 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
const mockData = {
epoch: '4441',
rewards: [
{
asset: 'tDAI',
totalAmount: '5',
rewardTypes: {
ACCOUNT_TYPE_GLOBAL_REWARD: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: {
amount: '5',
percentageOfTotal: '0.00305237260923',
},
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_FEES_LIQUIDITY: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
};
describe('EpochIndividualRewardsTable', () => {
it('should render correctly', () => {
const { getByTestId } = render(
<AppStateProvider>
<EpochIndividualRewardsTable data={mockData} />
</AppStateProvider>
);
expect(getByTestId('epoch-individual-rewards-table')).toBeInTheDocument();
expect(getByTestId('individual-rewards-asset')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES')
).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
).toBeInTheDocument();
});
});
@@ -1,111 +0,0 @@
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import {
rowGridItemStyles,
RewardsTable,
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list';
import { useTranslation } from 'react-i18next';
interface EpochIndividualRewardsGridProps {
data: EpochIndividualReward;
}
interface RewardItemProps {
value: string;
percentageOfTotal?: string;
dataTestId: string;
last?: boolean;
}
const DisplayReward = ({
reward,
percentageOfTotal,
}: {
reward: string;
percentageOfTotal?: string;
}) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
if (Number(reward) === 0) {
return <span className="text-vega-dark-300">-</span>;
}
return (
<Tooltip
description={
<div className="flex flex-col items-start">
<span>{formatNumber(toBigNum(reward, decimals), decimals)}</span>
{percentageOfTotal && (
<span className="text-vega-dark-300">
({percentageOfTotal}% {t('ofTotalDistributed')})
</span>
)}
</div>
}
>
<button>
<div className="flex flex-col items-start">
<span>{formatNumber(toBigNum(reward, decimals))}</span>
{percentageOfTotal && (
<span className="text-vega-dark-300">
({formatNumber(toBigNum(percentageOfTotal, 4)).toString()}%)
</span>
)}
</div>
</button>
</Tooltip>
);
};
const RewardItem = ({
value,
percentageOfTotal,
dataTestId,
last,
}: RewardItemProps) => (
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
<div className="overflow-auto p-5">
<DisplayReward reward={value} percentageOfTotal={percentageOfTotal} />
</div>
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
</div>
);
export const EpochIndividualRewardsTable = ({
data,
}: EpochIndividualRewardsGridProps) => {
return (
<RewardsTable
dataTestId="epoch-individual-rewards-table"
epoch={Number(data.epoch)}
>
{data.rewards.map(({ asset, rewardTypes, totalAmount }, i) => (
<div className="contents" key={i}>
<div
data-testid="individual-rewards-asset"
className={`${rowGridItemStyles()} p-5`}
>
{asset}
</div>
{Object.entries(rewardTypes).map(
([key, { amount, percentageOfTotal }]) => (
<RewardItem
key={key}
value={amount}
percentageOfTotal={percentageOfTotal}
dataTestId={key}
/>
)
)}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
</RewardsTable>
);
};
@@ -1,235 +0,0 @@
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
import { AccountType } from '@vegaprotocol/types';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
describe('generateEpochIndividualRewardsList', () => {
const reward1: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
const reward2: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
const reward3: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'gbp', symbol: 'GBP' },
party: { id: 'blah' },
epoch: { id: '2' },
};
const reward4: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
const rewardWrongType: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
it('should return an empty array if no rewards are provided', () => {
expect(generateEpochIndividualRewardsList([])).toEqual([]);
});
it('should filter out any rewards of the wrong type', () => {
const result = generateEpochIndividualRewardsList([rewardWrongType]);
expect(result).toEqual([]);
});
it('should return reward in the correct format', () => {
const result = generateEpochIndividualRewardsList([reward1]);
expect(result[0]).toEqual({
epoch: '1',
rewards: [
{
asset: 'USD',
totalAmount: '100',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
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',
},
},
},
],
});
});
it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList(rewards);
expect(result1[0].epoch).toEqual('2');
expect(result1[1].epoch).toEqual('1');
const reorderedRewards = [reward4, reward3, reward2, reward1];
const result2 = generateEpochIndividualRewardsList(reorderedRewards);
expect(result2[0].epoch).toEqual('2');
expect(result2[1].epoch).toEqual('1');
});
it('correctly calculates the total value of rewards for an asset', () => {
const rewards = [reward1, reward4];
const result = generateEpochIndividualRewardsList(rewards);
expect(result[0].rewards[0].totalAmount).toEqual('200');
});
it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4];
const result = generateEpochIndividualRewardsList(rewards);
expect(result).toEqual([
{
epoch: '2',
rewards: [
{
asset: 'EUR',
totalAmount: '50',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
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',
},
},
},
{
asset: 'GBP',
totalAmount: '200',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
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',
},
},
},
],
},
{
epoch: '1',
rewards: [
{
asset: 'USD',
totalAmount: '200',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
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,79 +0,0 @@
import { BigNumber } from '../../../lib/bignumber';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
import type { AccountType } from '@vegaprotocol/types';
export interface EpochIndividualReward {
epoch: string;
rewards: {
asset: string;
totalAmount: string;
rewardTypes: {
[key in AccountType]?: {
amount: string;
percentageOfTotal: string;
};
};
}[];
}
const accountTypes = Object.keys(RowAccountTypes);
const emptyRowAccountTypes = accountTypes.map((type) => [
type,
{
amount: '0',
percentageOfTotal: '0',
},
]);
export const generateEpochIndividualRewardsList = (
rewards: RewardFieldsFragment[]
) => {
// We take the rewards and aggregate them by epoch and asset.
const epochIndividualRewards = rewards.reduce((map, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.symbol;
const rewardType = reward.rewardType;
const amount = reward.amount;
const percentageOfTotal = reward.percentageOfTotal;
// if the rewardType is not of a type we display in the table, we skip it.
if (!accountTypes.includes(rewardType)) {
return map;
}
if (!map.has(epochId)) {
map.set(epochId, { epoch: epochId, rewards: [] });
}
const epoch = map.get(epochId);
let asset = epoch?.rewards.find((r) => r.asset === assetName);
if (!asset) {
asset = {
asset: assetName,
totalAmount: '0',
rewardTypes: Object.fromEntries(emptyRowAccountTypes),
};
epoch?.rewards.push(asset);
}
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
// totalAmount is the sum of all rewardTypes amounts
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
(sum, rewardType) => {
return new BigNumber(sum).plus(rewardType.amount).toString();
},
'0'
);
return map;
}, new Map<string, EpochIndividualReward>());
return Array.from(epochIndividualRewards.values()).sort(
(a, b) => Number(b.epoch) - Number(a.epoch)
);
};
@@ -5,7 +5,7 @@ import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-li
import { NoRewards } from '../no-rewards';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
export const EpochTotalRewards = () => {
export const EpochRewards = () => {
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
variables: {
epochRewardSummariesPagination: {
@@ -15,7 +15,7 @@ export const EpochTotalRewards = () => {
});
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
const epochTotalRewardSummaries = generateEpochTotalRewardsList(data) || [];
const epochRewardSummaries = generateEpochTotalRewardsList(data) || [];
return (
<AsyncRenderer
@@ -27,12 +27,12 @@ export const EpochTotalRewards = () => {
className="max-w-full overflow-auto"
data-testid="epoch-rewards-total"
>
{epochTotalRewardSummaries.length === 0 ? (
{epochRewardSummaries.length === 0 ? (
<NoRewards />
) : (
<>
{epochTotalRewardSummaries.map((epochTotalSummary, index) => (
<EpochTotalRewardsTable data={epochTotalSummary} key={index} />
{epochRewardSummaries.map((aggregatedEpochSummary) => (
<EpochTotalRewardsTable data={aggregatedEpochSummary} />
))}
</>
)}
@@ -1,5 +1,4 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
import { AccountType } from '@vegaprotocol/types';
@@ -11,30 +10,10 @@ const mockData = {
'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '295',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '295',
},
@@ -43,25 +22,15 @@ const mockData = {
describe('EpochTotalRewardsTable', () => {
it('should render correctly', () => {
const { getByTestId } = render(
<AppStateProvider>
<EpochTotalRewardsTable data={mockData} />
</AppStateProvider>
);
const { getByTestId } = render(<EpochTotalRewardsTable data={mockData} />);
expect(getByTestId('epoch-total-rewards-table')).toBeInTheDocument();
expect(getByTestId('asset')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES')
).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
).toBeInTheDocument();
expect(getByTestId('global')).toBeInTheDocument();
expect(getByTestId('infra')).toBeInTheDocument();
expect(getByTestId('taker')).toBeInTheDocument();
expect(getByTestId('maker')).toBeInTheDocument();
expect(getByTestId('liquidity')).toBeInTheDocument();
expect(getByTestId('market-maker')).toBeInTheDocument();
expect(getByTestId('total')).toBeInTheDocument();
});
});
@@ -1,14 +1,19 @@
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import {
rowGridItemStyles,
RewardsTable,
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { EpochTotalSummary } from './generate-epoch-total-rewards-list';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { formatNumber } from '@vegaprotocol/react-helpers';
import { Tooltip, Icon } from '@vegaprotocol/ui-toolkit';
import { AccountType } from '@vegaprotocol/types';
import { SubHeading } from '../../../components/heading';
import type { AggregatedEpochSummary } from './generate-epoch-total-rewards-list';
interface EpochTotalRewardsGridProps {
data: EpochTotalSummary;
data: AggregatedEpochSummary;
}
interface ColumnHeaderProps {
title: string;
tooltipContent?: string;
className?: string;
}
interface RewardItemProps {
@@ -17,28 +22,61 @@ interface RewardItemProps {
last?: boolean;
}
const DisplayReward = ({ reward }: { reward: string }) => {
const {
appState: { decimals },
} = useAppState();
const displayReward = (reward: string) => {
if (Number(reward) === 0) {
return <span className="text-vega-dark-300">-</span>;
return <span className="text-vega-dark-300">0</span>;
}
return (
<Tooltip description={formatNumber(toBigNum(reward, decimals), decimals)}>
<button>{formatNumber(toBigNum(reward, decimals))}</button>
</Tooltip>
);
if (reward.split('.')[1] && reward.split('.')[1].length > 4) {
return (
<Tooltip description={formatNumber(reward)}>
<button>{formatNumber(Number(reward).toFixed(4))}</button>
</Tooltip>
);
}
return <span>{formatNumber(reward)}</span>;
};
const gridStyles = classNames(
'grid grid-cols-[repeat(8,minmax(100px,auto))] max-w-full overflow-auto',
`border-t border-vega-dark-200`,
'text-sm'
);
const headerGridItemStyles = (last = false) =>
classNames('border-r border-b border-b-vega-dark-200', 'py-3 px-5', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
const rowGridItemStyles = (last = false) =>
classNames('relative', 'border-r border-b border-b-vega-dark-150', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
const ColumnHeader = ({
title,
tooltipContent,
className,
}: ColumnHeaderProps) => (
<div className={className}>
<h2 className="mb-1 text-sm text-vega-dark-300">{title}</h2>
{tooltipContent && (
<Tooltip description={tooltipContent}>
<button>
<Icon name={'info-sign'} className="text-vega-dark-200" />
</button>
</Tooltip>
)}
</div>
);
const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
<div className="overflow-auto p-5">
<DisplayReward reward={value} />
</div>
<div className="overflow-auto p-5">{displayReward(value)}</div>
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
</div>
);
@@ -46,19 +84,129 @@ const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
export const EpochTotalRewardsTable = ({
data,
}: EpochTotalRewardsGridProps) => {
const { t } = useTranslation();
const rowData = data.assetRewards.map(({ name, rewards, totalAmount }) => ({
name,
ACCOUNT_TYPE_GLOBAL_REWARD:
rewards
.filter((r) => r.rewardType === AccountType.ACCOUNT_TYPE_GLOBAL_REWARD)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_FEES_INFRASTRUCTURE:
rewards
.filter(
(r) => r.rewardType === AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE
)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES:
rewards
.filter(
(r) =>
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES
)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES:
rewards
.filter(
(r) =>
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES
)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_FEES_LIQUIDITY:
rewards
.filter((r) => r.rewardType === AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS:
rewards
.filter(
(r) =>
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS
)
.map((r) => r.amount)[0] || '0',
totalAmount: totalAmount,
}));
return (
<RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}>
{data.assetRewards.map(({ name, rewards, totalAmount }, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{name}
<div data-testid="epoch-total-rewards-table" className="mb-12">
<SubHeading title={`EPOCH ${data.epoch}`} />
<div className={gridStyles}>
<ColumnHeader
title={t('rewardsColAssetHeader')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColStakingHeader')}
tooltipContent={t('rewardsColStakingTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColInfraHeader')}
tooltipContent={t('rewardsColInfraTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColPriceTakingHeader')}
tooltipContent={t('rewardsColPriceTakingTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColPriceMakingHeader')}
tooltipContent={t('rewardsColPriceMakingTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColLiquidityProvisionHeader')}
tooltipContent={t('rewardsColLiquidityProvisionTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColMarketCreationHeader')}
tooltipContent={t('rewardsColMarketCreationTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColTotalHeader')}
className={headerGridItemStyles(true)}
/>
{rowData.map((row, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{row.name}
</div>
<RewardItem
dataTestId="global"
value={row.ACCOUNT_TYPE_GLOBAL_REWARD}
/>
<RewardItem
dataTestId="infra"
value={row.ACCOUNT_TYPE_FEES_INFRASTRUCTURE}
/>
<RewardItem
dataTestId="taker"
value={row.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES}
/>
<RewardItem
dataTestId="maker"
value={row.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES}
/>
<RewardItem
dataTestId="liquidity"
value={row.ACCOUNT_TYPE_FEES_LIQUIDITY}
/>
<RewardItem
dataTestId="market-maker"
value={row.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS}
/>
<RewardItem
dataTestId="total"
value={row.totalAmount}
last={true}
/>
</div>
{rewards.map(({ rewardType, amount }, i) => (
<RewardItem key={i} dataTestId={rewardType} value={amount} />
))}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
</RewardsTable>
))}
</div>
</div>
);
};
@@ -61,7 +61,7 @@ describe('generateEpochAssetRewardsList', () => {
expect(result).toEqual([]);
});
it('should return an array of unnamed assets if no asset names are provided (should not happen)', () => {
it('should return an array of unnamed assets if no assets are provided (should not happen)', () => {
const epochData = {
assetsConnection: {
edges: [],
@@ -72,10 +72,18 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 1,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '123',
},
},
{
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
},
],
},
epoch: {
@@ -96,34 +104,30 @@ describe('generateEpochAssetRewardsList', () => {
name: '',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '123',
},
],
},
{
epoch: 2,
assetRewards: [
{
assetId: '1',
name: '',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
],
totalAmount: '5',
},
],
},
]);
});
@@ -151,7 +155,7 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 1,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '123',
},
},
@@ -163,6 +167,22 @@ describe('generateEpochAssetRewardsList', () => {
amount: '100',
},
},
{
node: {
epoch: 1,
assetId: '2',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '17.9873',
},
},
{
node: {
epoch: 1,
assetId: '2',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '1',
},
},
{
node: {
epoch: 2,
@@ -191,31 +211,30 @@ describe('generateEpochAssetRewardsList', () => {
name: 'Asset 1',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
],
totalAmount: '223',
},
{
assetId: '2',
name: 'Asset 2',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '17.9873',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
amount: '1',
},
],
totalAmount: '223',
totalAmount: '18.9873',
},
],
},
@@ -226,30 +245,10 @@ describe('generateEpochAssetRewardsList', () => {
assetId: '1',
name: 'Asset 1',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '5',
},
@@ -3,8 +3,6 @@ import type {
EpochRewardSummaryFieldsFragment,
} from '../home/__generated__/Rewards';
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { AccountType } from '@vegaprotocol/types';
interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment {
name: string;
@@ -20,16 +18,11 @@ export interface AggregatedEpochRewardSummary {
totalAmount: string;
}
export interface EpochTotalSummary {
export interface AggregatedEpochSummary {
epoch: EpochRewardSummaryFieldsFragment['epoch'];
assetRewards: AggregatedEpochRewardSummary[];
}
const emptyRowAccountTypes = Object.keys(RowAccountTypes).map((type) => ({
rewardType: type as AccountType,
amount: '0',
}));
export const generateEpochTotalRewardsList = (
epochData: EpochAssetsRewardsQuery | undefined
) => {
@@ -65,7 +58,7 @@ export const generateEpochTotalRewardsList = (
}, [] as EpochSummaryWithNamedReward[][]);
// Now aggregate the array of arrays of epoch summaries by asset rewards.
const epochTotalRewards: EpochTotalSummary[] =
const aggregatedEpochSummaries: AggregatedEpochSummary[] =
aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => {
const assetRewards = epochSummaries.reduce((acc, epochSummary) => {
const assetRewardIndex = acc.findIndex(
@@ -79,36 +72,18 @@ export const generateEpochTotalRewardsList = (
assetId: epochSummary.assetId,
name: epochSummary.name,
rewards: [
...emptyRowAccountTypes.map((emptyRowAccountType) => {
if (
emptyRowAccountType.rewardType === epochSummary.rewardType
) {
return {
rewardType: epochSummary.rewardType,
amount: epochSummary.amount,
};
} else {
return emptyRowAccountType;
}
}),
{
rewardType: epochSummary.rewardType,
amount: epochSummary.amount,
},
],
totalAmount: epochSummary.amount,
});
} else {
acc[assetRewardIndex].rewards = acc[assetRewardIndex].rewards.map(
(reward) => {
if (reward.rewardType === epochSummary.rewardType) {
return {
rewardType: epochSummary.rewardType,
amount: (
Number(reward.amount) + Number(epochSummary.amount)
).toString(),
};
} else {
return reward;
}
}
);
acc[assetRewardIndex].rewards.push({
rewardType: epochSummary.rewardType,
amount: epochSummary.amount,
});
acc[assetRewardIndex].totalAmount = (
Number(acc[assetRewardIndex].totalAmount) +
Number(epochSummary.amount)
@@ -124,5 +99,5 @@ export const generateEpochTotalRewardsList = (
};
});
return epochTotalRewards;
return aggregatedEpochSummaries;
};
@@ -1,30 +1,34 @@
// @ts-ignore No types available for duration-js
import Duration from 'duration-js';
import { useMemo, useState } from 'react';
import React, { useMemo, useState } from 'react';
import { formatDistance } from 'date-fns';
import { useTranslation } from 'react-i18next';
import {
Button,
Callout,
Intent,
AsyncRenderer,
Toggle,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import {
useNetworkParams,
NetworkParams,
createDocsLinks,
} from '@vegaprotocol/react-helpers';
import {
AppStateActionType,
useAppState,
} from '../../../contexts/app-state/app-state-context';
import { useEpochQuery } from './__generated__/Rewards';
import { EpochCountdown } from '../../../components/epoch-countdown';
import { Heading, SubHeading } from '../../../components/heading';
import { EpochIndividualRewards } from '../epoch-individual-rewards/epoch-individual-rewards';
import { RewardInfo } from '../epoch-individual-awards/reward-info';
import { EpochRewards } from '../epoch-total-rewards/epoch-rewards';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useEnvironment } from '@vegaprotocol/environment';
import { ConnectToSeeRewards } from '../connect-to-see-rewards';
import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards';
type RewardsView = 'total' | 'individual';
@@ -35,14 +39,10 @@ export const RewardsPage = () => {
const [toggleRewardsView, setToggleRewardsView] =
useState<RewardsView>('total');
const {
data: epochData,
loading: epochLoading,
error: epochError,
refetch,
} = useEpochQuery();
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { appDispatch } = useAppState();
const {
params,
@@ -50,6 +50,14 @@ export const RewardsPage = () => {
error: paramsError,
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
const {
data: epochData,
loading: epochLoading,
error: epochError,
refetch,
} = useEpochQuery();
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
const payoutDuration = useMemo(() => {
if (!params) {
return 0;
@@ -95,8 +103,8 @@ export const RewardsPage = () => {
</div>
) : null}
{epochData?.epoch &&
epochData.epoch?.id &&
{epochData &&
epochData.epoch.id &&
epochData.epoch.timestamps.start &&
epochData.epoch.timestamps.expiry && (
<section className="mb-16">
@@ -140,13 +148,26 @@ export const RewardsPage = () => {
</section>
{toggleRewardsView === 'total' ? (
<EpochTotalRewards />
<EpochRewards />
) : (
<section>
{pubKey && pubKeys?.length ? (
<EpochIndividualRewards />
<RewardInfo />
) : (
<ConnectToSeeRewards />
<div>
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
{t('connectVegaWallet')}
</Button>
</div>
)}
</section>
)}
@@ -1,125 +0,0 @@
import classNames from 'classnames';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import type { ReactNode } from 'react';
import { SubHeading } from '../../../components/heading';
import { AccountType } from '@vegaprotocol/types';
// This is the data structure that matters for defining which Account types
// are displayed in the rewards tables. It sets column titles and tooltips,
// and is used to filter the data that is passed to functions to generate
// the table rows. It's important to preserve the order.
export const RowAccountTypes = {
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
columnTitle: 'rewardsColStakingHeader',
description: 'rewardsColStakingTooltip',
},
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
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_FEES_LIQUIDITY]: {
columnTitle: 'rewardsColLiquidityProvisionHeader',
description: 'rewardsColLiquidityProvisionTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
columnTitle: 'rewardsColMarketCreationHeader',
description: 'rewardsColMarketCreationTooltip',
},
};
interface ColumnHeaderProps {
title: string;
tooltipContent?: string;
className?: string;
}
const headerGridItemStyles = (last = false) =>
classNames('border-r border-b border-b-vega-dark-200', 'py-3 px-5', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
export const rowGridItemStyles = (last = false) =>
classNames('relative', 'border-r border-b border-b-vega-dark-150', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
const gridStyles = classNames(
'grid grid-cols-[repeat(8,minmax(100px,auto))] max-w-full overflow-auto',
`border-t border-vega-dark-200`,
'text-sm'
);
const ColumnHeader = ({
title,
tooltipContent,
className,
}: ColumnHeaderProps) => (
<div className={className}>
<h2 className="mb-1 text-sm text-vega-dark-300">{title}</h2>
{tooltipContent && (
<Tooltip description={tooltipContent}>
<button>
<Icon name={'info-sign'} className="text-vega-dark-200" />
</button>
</Tooltip>
)}
</div>
);
const ColumnHeaders = () => {
const { t } = useTranslation();
return (
<div className="contents">
<ColumnHeader
title={t('rewardsColAssetHeader')}
className={headerGridItemStyles()}
/>
{Object.values(RowAccountTypes).map(({ columnTitle, description }) => (
<ColumnHeader
key={columnTitle}
title={t(columnTitle)}
tooltipContent={t(description)}
className={headerGridItemStyles()}
/>
))}
<ColumnHeader
title={t('rewardsColTotalHeader')}
className={headerGridItemStyles(true)}
/>
</div>
);
};
export interface RewardTableProps {
dataTestId: string;
epoch: number;
children: ReactNode;
}
// Rewards table children will be the row items. Make sure they contain
// the same number of columns and map to the data of the ColumnHeaders component.
export const RewardsTable = ({
dataTestId,
epoch,
children,
}: RewardTableProps) => (
<div data-testid={dataTestId} className="mb-12">
<SubHeading title={`EPOCH ${epoch}`} />
<div className={gridStyles}>
<ColumnHeaders />
{children}
</div>
</div>
);
@@ -363,7 +363,6 @@ export const ConsensusValidatorsTable = ({
customThemeParams={NODE_LIST_GRID_STYLES}
getRowHeight={(params: RowHeightParams) => getRowHeight(params)}
defaultColDef={defaultColDef}
tooltipShowDelay={0}
animateRows={true}
suppressCellFocus={true}
overlayNoRowsTemplate={t('noValidators')}
@@ -59,6 +59,8 @@ export const defaultColDef = {
comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b),
cellStyle: { margin: '10px 0', padding: '0 12px' },
tooltipComponent: TooltipCellComponent,
tooltipShowDelay: 0,
tooltipHideDelay: 0,
};
interface ValidatorRendererProps {
@@ -236,7 +236,6 @@ export const StandbyPendingValidatorsTable = ({
customThemeParams={NODE_LIST_GRID_STYLES}
rowHeight={52}
defaultColDef={defaultColDef}
tooltipShowDelay={0}
animateRows={true}
suppressCellFocus={true}
overlayNoRowsTemplate={t('noValidators')}
@@ -1,145 +0,0 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { ValidatorTable } from './validator-table';
import { ValidatorStatus } from '@vegaprotocol/types';
import countryData from '../../../components/country-selector/country-data';
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
const mockNode = {
id: 'bb1822715aa86ce0e205aa4c78e9b71cdeaec94596ce72d366f0d50589eb1bf5',
name: 'Marvin',
pubkey: 'f45085a3bbce4e8197910448a0f22e7d5e79f8234336053bc11b177b0d70e785',
infoUrl: 'https://en.wikipedia.org/wiki/Marvin_the_Paranoid_Android',
location: 'AQ',
ethereumAddress: '0x6ae2ff81b4a00f2edbed1fe3551ee0e3d81aa4f4',
stakedByOperator: '3000000000000000000000',
stakedByDelegates: '1280000000000000000',
stakedTotal: '3001280000000000000000',
pendingStake: '0',
epochData: null,
rankingScore: {
rankingScore: '0.3999466965166174',
stakeScore: '0.1999733482583087',
performanceScore: '1',
votingPower: '2000',
status: ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
},
};
const mockStakedTotal = '15008.4';
const decimals = 18;
const renderComponent = () =>
render(
<AppStateProvider>
<ValidatorTable node={mockNode} stakedTotal={mockStakedTotal} />
</AppStateProvider>
);
describe('ValidatorTable', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should render a link to the staking guide', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-table-staking-guide-link')).toBeTruthy();
});
it('should render the correct node id', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-id')).toHaveTextContent(mockNode.id);
});
it('should render the validator description url', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-description-url')).toHaveAttribute(
'href',
mockNode.infoUrl
);
});
it('should render the correct validator status', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-status')).toHaveTextContent('Consensus');
});
it('should render a link to the validator forum', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-forum-link')).toBeTruthy();
});
it('should render the pubkey', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-public-key')).toHaveTextContent(
mockNode.pubkey
);
});
it('should render the server location', () => {
const location = countryData.find((c) => c.code === mockNode.location);
const { getByTestId } = renderComponent();
expect(getByTestId('validator-server-location')).toHaveTextContent(
// @ts-ignore - location is not null
location.name
);
});
it('should render the ethereum address', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-eth-address')).toHaveTextContent(
mockNode.ethereumAddress
);
});
it('should render the staked by operator', () => {
const stakedByOperator = formatNumber(
toBigNum(mockNode.stakedByOperator, decimals)
);
const { getByTestId } = renderComponent();
expect(getByTestId('staked-by-operator')).toHaveTextContent(
stakedByOperator
);
});
it('should render the staked by delegates', () => {
const stakedByDelegates = formatNumber(
toBigNum(mockNode.stakedByDelegates, decimals)
);
const { getByTestId } = renderComponent();
expect(getByTestId('staked-by-delegates')).toHaveTextContent(
stakedByDelegates
);
});
it('should render the total stake', () => {
const stakedTotal = formatNumber(toBigNum(mockNode.stakedTotal, decimals));
const { getByTestId } = renderComponent();
expect(getByTestId('total-stake')).toHaveTextContent(stakedTotal);
});
it('should render the pending stake', () => {
const pendingStake = formatNumber(
toBigNum(mockNode.pendingStake, decimals)
);
const { getByTestId } = renderComponent();
expect(getByTestId('pending-stake')).toHaveTextContent(pendingStake);
});
it('should render the values calculated by the helper functions', () => {
// these functions are all tested in shared.spec.ts
const { getByTestId } = renderComponent();
expect(getByTestId('stake-percentage')).toBeInTheDocument();
expect(getByTestId('overstaking-penalty')).toBeInTheDocument();
expect(getByTestId('performance-penalty')).toBeInTheDocument();
expect(getByTestId('total-penalties')).toBeInTheDocument();
expect(getByTestId('unnormalised-voting-power')).toBeInTheDocument();
expect(getByTestId('normalised-voting-power')).toBeInTheDocument();
});
});
@@ -1,25 +1,19 @@
import React, { useMemo } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import countryData from '../../../components/country-selector/country-data';
import { Link as UTLink, Link } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import {
createDocsLinks,
ExternalLinks,
toBigNum,
} from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
import {
Link as UTLink,
Link,
Tooltip,
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { BigNumber } from '../../../lib/bignumber';
import { formatNumber } from '../../../lib/format-number';
import { ExternalLinks, toBigNum } from '@vegaprotocol/react-helpers';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import countryData from '../../../components/country-selector/country-data';
import * as Schema from '@vegaprotocol/types';
import { SubHeading } from '../../../components/heading';
import {
getLastEpochScoreAndPerformance,
@@ -29,7 +23,6 @@ import {
getPerformancePenalty,
getTotalPenalties,
getUnnormalisedVotingPower,
getStakePercentage,
} from '../shared';
import type { ReactNode } from 'react';
import type { StakingNodeFieldsFragment } from './__generated__/Staking';
@@ -69,7 +62,7 @@ export const ValidatorTable = ({
stakedTotal,
previousEpochData,
}: ValidatorTableProps) => {
const { ETHERSCAN_URL, VEGA_DOCS_URL } = useEnvironment();
const { ETHERSCAN_URL } = useEnvironment();
const { t } = useTranslation();
const {
appState: { decimals },
@@ -88,7 +81,10 @@ export const ValidatorTable = ({
node.stakedTotal
);
const stakePercentage = getStakePercentage(total, stakedOnNode);
const stakePercentage =
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
? '-'
: stakedOnNode.dividedBy(total).times(100).dp(2).toString() + '%';
const totalPenaltiesAmount = getTotalPenalties(
rawValidatorScore,
@@ -98,207 +94,158 @@ export const ValidatorTable = ({
);
return (
<>
<p className="mb-12">
{t('validatorFormIntro')}{' '}
{VEGA_DOCS_URL && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
target="_blank"
data-testid="validator-table-staking-guide-link"
className="text-white"
>
{t('readMoreValidatorForm')}
</ExternalLink>
)}
</p>
<div className="my-12" data-testid="validator-table">
<SubHeading title={t('profile')} />
<RoundedWrapper>
<KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow>
<span>{t('id')}</span>
<ValidatorTableCell dataTestId="validator-id">
{node.id}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('ABOUT THIS VALIDATOR')}</span>
<span>
<a href={node.infoUrl}>{node.infoUrl}</a>
</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('STATUS')}</strong>
</span>
<span data-testid="validator-status">
<strong>
{t(statusTranslationKey(node.rankingScore.status))}
</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<div className="my-12" data-testid="validator-table">
<SubHeading title={t('profile')} />
<RoundedWrapper>
<KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow>
<span>{t('id')}</span>
<ValidatorTableCell dataTestId="validator-id">
{node.id}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('ABOUT THIS VALIDATOR')}</span>
<Tooltip description={t('AboutThisValidatorDescription')}>
<a data-testid="validator-description-url" href={node.infoUrl}>
{node.infoUrl}
</a>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('STATUS')}</strong>
</span>
<Tooltip description={t('ValidatorStatusDescription')}>
<span data-testid="validator-status">
<strong>
{t(statusTranslationKey(node.rankingScore.status))}
</strong>
</span>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<div className="mb-10">
{t('validatorTableIntro')}{' '}
<UTLink
href={ExternalLinks.VALIDATOR_FORUM}
target="_blank"
data-testid="validator-forum-link"
>
{t('onTheForum')}
</UTLink>
</div>
<SubHeading title={t('ADDRESS')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
<ValidatorTableCell dataTestId="validator-public-key">
{node.pubkey}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('SERVER LOCATION')}</span>
<ValidatorTableCell dataTestId="validator-server-location">
{countryData.find((c) => c.code === node.location)?.name ||
t('not available')}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('ETHEREUM ADDRESS')}</span>
<span data-testid="validator-eth-address">
<Link
title={t('View on Etherscan (opens in a new tab)')}
href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
target="_blank"
>
{node.ethereumAddress}
</Link>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('STAKE')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span>
<Tooltip description={t('StakedByOperatorDescription')}>
<span data-testid="staked-by-operator">
{formatNumber(toBigNum(node.stakedByOperator, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('STAKED BY DELEGATES')}</span>
<Tooltip description={t('StakedByDelegatesDescription')}>
<span data-testid="staked-by-delegates">
{formatNumber(toBigNum(node.stakedByDelegates, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
<strong>{t('TOTAL STAKE')}</strong>
</span>
<span data-testid="total-stake">
<strong>
{formatNumber(toBigNum(node.stakedTotal, decimals))}
</strong>
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PENDING STAKE')}</span>
<Tooltip description={t('PendingStakeDescription')}>
<span data-testid="pending-stake">
{formatNumber(toBigNum(node.pendingStake, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('STAKE SHARE')}</span>
<Tooltip description={t('StakeShareDescription')}>
<span data-testid="stake-percentage">{stakePercentage}</span>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('PENALTIES')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-penalties">
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
<Tooltip description={t('OverstakedPenaltyDescription')}>
<span data-testid="overstaking-penalty">
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE PENALTY')}</span>
<Tooltip description={t('PerformancePenaltyDescription')}>
<span data-testid="performance-penalty">
{getPerformancePenalty(performanceScore)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('TOTAL PENALTIES')}</strong>
</span>
<span data-testid="total-penalties">
<strong>{totalPenaltiesAmount}</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('VOTING POWER')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-voting-power">
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
<Tooltip description={t('UnnormalisedVotingPowerDescription')}>
<span data-testid="unnormalised-voting-power">
{getUnnormalisedVotingPower(rawValidatorScore)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('NORMALISED VOTING POWER')}</strong>
</span>
<Tooltip description={t('NormalisedVotingPowerDescription')}>
<strong data-testid="normalised-voting-power">
{getNormalisedVotingPower(node.rankingScore.votingPower)}
</strong>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<div className="mb-10">
{t('validatorTableIntro')}{' '}
<UTLink
href={ExternalLinks.VALIDATOR_FORUM}
target="_blank"
data-testid="validator-forum-link"
>
{t('onTheForum')}
</UTLink>
</div>
</>
<SubHeading title={t('ADDRESS')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
<ValidatorTableCell dataTestId="validator-public-key">
{node.pubkey}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('SERVER LOCATION')}</span>
<ValidatorTableCell>
{countryData.find((c) => c.code === node.location)?.name ||
t('not available')}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('ETHEREUM ADDRESS')}</span>
<span>
<Link
title={t('View on Etherscan (opens in a new tab)')}
href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
target="_blank"
>
{node.ethereumAddress}
</Link>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('STAKE')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span>
<span data-testid="staked-by-operator">
{formatNumber(toBigNum(node.stakedByOperator, decimals))}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('STAKED BY DELEGATES')}</span>
<span data-testid="staked-by-delegates">
{formatNumber(toBigNum(node.stakedByDelegates, decimals))}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
<strong>{t('TOTAL STAKE')}</strong>
</span>
<span data-testid="total-stake">
<strong>
{formatNumber(toBigNum(node.stakedTotal, decimals))}
</strong>
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PENDING STAKE')}</span>
<span data-testid="pending-stake">
{formatNumber(toBigNum(node.pendingStake, decimals))}
</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('STAKE SHARE')}</span>
<span data-testid="stake-percentage">{stakePercentage}</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('PENALTIES')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-penalties">
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
<span>
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE PENALTY')}</span>
<span>{getPerformancePenalty(performanceScore)}</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('TOTAL PENALTIES')}</strong>
</span>
<span>
<strong>{totalPenaltiesAmount}</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('VOTING POWER')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-voting-power">
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
<span>{getUnnormalisedVotingPower(rawValidatorScore)}</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('NORMALISED VOTING POWER')}</strong>
</span>
<span>
<strong>
{getNormalisedVotingPower(node.rankingScore.votingPower)}
</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
);
};
@@ -8,7 +8,6 @@ import {
getFormattedPerformanceScore,
getPerformancePenalty,
getTotalPenalties,
getStakePercentage,
} from './shared';
describe('getLastEpochScoreAndPerformance', () => {
@@ -140,23 +139,3 @@ describe('getTotalPenalties', () => {
expect(getTotalPenalties('0.25', '0.5', '1000', '10000')).toEqual('0.00%');
});
});
describe('getStakePercentage', () => {
it('should return the stake percentage', () => {
expect(
getStakePercentage(new BigNumber('1000'), new BigNumber('100'))
).toEqual('10%');
expect(
getStakePercentage(new BigNumber('1000'), new BigNumber('500'))
).toEqual('50%');
expect(
getStakePercentage(new BigNumber('1000'), new BigNumber('257.5'))
).toEqual('25.75%');
});
it('should return "0%" if the total stake is 0', () => {
expect(getStakePercentage(new BigNumber('0'), new BigNumber('0'))).toEqual(
'0%'
);
});
});
-5
View File
@@ -95,8 +95,3 @@ export const getTotalPenalties = (
2
);
};
export const getStakePercentage = (total: BigNumber, stakedOnNode: BigNumber) =>
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
? '0%'
: stakedOnNode.dividedBy(total).times(100).dp(2).toString() + '%';
-1
View File
@@ -22,7 +22,6 @@ module.exports = defineConfig({
viewportHeight: 900,
responseTimeout: 50000,
requestTimeout: 20000,
retries: 2,
},
env: {
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
@@ -72,11 +72,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
cy.getByTestId('approve-warning').should(
'contain.text',
`Deposits of ${btcSymbol} not approved`
);
cy.getByTestId('deposit-submit').click();
cy.getByTestId('deposit-approve-submit').click();
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
cy.get('[data-testid="Return to deposit"]').click();
cy.get(amountField).clear().type('10');
@@ -411,7 +407,7 @@ describe('capsule', { tags: '@slow' }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
cy.get(assetSelectField, txTimeout).select(vegaName, { force: true });
cy.getByTestId('deposit-submit').click();
cy.getByTestId('deposit-approve-submit').click();
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
cy.get('[data-testid="Return to deposit"]').click();
cy.get(amountField).clear().type('10000');
+6 -132
View File
@@ -1,5 +1,3 @@
import { removeDecimal } from '@vegaprotocol/cypress';
import { ethers } from 'ethers';
import { connectEthereumWallet } from '../support/ethereum-wallet';
import { selectAsset } from '../support/helpers';
@@ -11,7 +9,7 @@ const formFieldError = 'input-error-text';
const ASSET_EURO = 1;
describe('deposit form validation', { tags: '@smoke' }, () => {
function openDepositForm() {
before(() => {
cy.mockWeb3Provider();
cy.mockSubscription();
cy.mockTradingPage();
@@ -22,14 +20,10 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
cy.getByTestId('deposit-button').click();
cy.wait('@Assets');
connectEthereumWallet('MetaMask');
}
before(() => {
openDepositForm();
cy.getByTestId('deposit-submit').click();
});
it('handles empty fields', () => {
cy.getByTestId('deposit-submit').click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 2);
});
@@ -50,13 +44,6 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
});
it('invalid amount', () => {
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
// Deposit amount smaller than minimum viable for selected asset
// Select an amount so that we have a known decimal places value to work with
selectAsset(ASSET_EURO);
@@ -69,126 +56,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
});
it('insufficient funds', () => {
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
// 1001-DEPO-005
// Deposit amount is valid, but less than approved. This will always be the case because our
// CI wallet wont have approved any assets
cy.get(amountField)
.clear()
.type('850')
.type('100')
.next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Insufficient amount in Ethereum wallet');
});
});
describe('deposit actions', { tags: '@smoke' }, () => {
before(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.wait('@MarketsCandles');
cy.getByTestId('dialog-close').click();
});
it('Deposit to trade is visble', () => {
cy.getByTestId('Collateral').click();
cy.contains('[data-testid="deposit"]', 'Deposit to trade').should(
'be.visible'
);
cy.contains('[data-testid="deposit"]', 'Deposit to trade').click();
connectEthereumWallet('MetaMask');
cy.getByTestId('deposit-submit').should('be.visible');
});
});
function mockWeb3DepositCalls({
allowance,
depositLifetimeLimit,
balance,
deposited,
dps,
}: {
allowance: string;
depositLifetimeLimit: string;
balance: string;
deposited: string;
dps: number;
}) {
const assetContractAddress = '0x0158031158bb4df2ad02eaa31e8963e84ea978a4';
const collateralBridgeAddress = '0x7fe27d970bc8afc3b11cc8d9737bfb66b1efd799';
const toResult = (value: string, dps: number) => {
const rawValue = removeDecimal(value, dps);
return ethers.utils.hexZeroPad(
ethers.utils.hexlify(parseInt(rawValue)),
32
);
};
cy.intercept('POST', 'http://localhost:8545', (req) => {
// Mock chainId call
if (req.body.method === 'eth_chainId') {
req.alias = 'eth_chainId';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: '0xaa36a7', // 11155111 for sepolia chain id
});
}
// Mock deposited amount
if (req.body.method === 'eth_getStorageAt') {
req.alias = 'eth_getStorageAt';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(deposited, dps),
});
}
if (req.body.method === 'eth_call') {
// Mock approved amount for asset on collateral bridge
if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0xdd62ed3e000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f0000000000000000000000007fe27d970bc8afc3b11cc8d9737bfb66b1efd799'
) {
req.alias = 'eth_call_allowance';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(allowance, dps),
});
}
// Mock balance of asset in Ethereum wallet
else if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0x70a08231000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f'
) {
req.alias = 'eth_call_balanceOf';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(balance, dps),
});
}
// Mock deposit lifetime limit
else if (
req.body.params[0].to === collateralBridgeAddress &&
req.body.params[0].data ===
'0x354a897a0000000000000000000000000158031158bb4df2ad02eaa31e8963e84ea978a4'
) {
req.alias = 'eth_call_get_deposit_maximum'; // deposit lifetime limit
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(depositLifetimeLimit, dps),
});
}
}
});
}
@@ -25,38 +25,12 @@ const generateProposal = (code: string): ProposalListFieldsFragment => ({
totalWeight: '',
},
},
requiredMajority: '',
party: {
__typename: 'Party',
id: '',
},
rationale: {
__typename: 'ProposalRationale',
description: '',
title: '',
},
requiredParticipation: '',
errorDetails: '',
rejectionReason: null,
requiredLpMajority: '',
requiredLpParticipation: '',
terms: {
__typename: 'ProposalTerms',
closingDatetime: '',
enactmentDatetime: undefined,
change: {
__typename: 'NewMarket',
decimalPlaces: 1,
lpPriceRange: '',
riskParameters: {
__typename: 'SimpleRiskModel',
params: {
__typename: 'SimpleRiskModelParams',
factorLong: 0,
factorShort: 1,
},
},
metadata: [],
instrument: {
__typename: 'InstrumentConfiguration',
code: code,
@@ -68,34 +42,6 @@ const generateProposal = (code: string): ProposalListFieldsFragment => ({
id: 'A',
name: 'A',
symbol: 'A',
decimals: 1,
quantum: '',
},
quoteName: '',
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: '',
tradingTerminationProperty: '',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
sourceType: {
__typename: 'DataSourceSpecConfigurationTime',
conditions: [],
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
sourceType: {
__typename: 'DataSourceSpecConfigurationTime',
conditions: [],
},
},
},
},
},
@@ -29,7 +29,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(0, 'Mark Price', '0.05749');
validateMarketDataRow(1, 'Best Bid Price', '6.81765 ');
validateMarketDataRow(2, 'Best Offer Price', '6.81769 ');
validateMarketDataRow(3, 'Quote Unit', 'BTC');
});
it('market volume displayed', () => {
@@ -169,9 +168,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
it('liquidity price range displayed', () => {
cy.getByTestId(marketTitle).contains('Liquidity price range').click();
validateMarketDataRow(0, 'Liquidity Price Range', '2.00% of mid price');
validateMarketDataRow(1, 'Lowest Price', '0.05634 BTC');
validateMarketDataRow(2, 'Highest Price', '0.05864 BTC');
validateMarketDataRow(0, 'Liquidity Price Range', '2.00%');
validateMarketDataRow(1, 'LP Volume Min', '0.05634 tBTC');
validateMarketDataRow(2, 'LP Volume Max', '0.05864 tBTC');
});
it('oracle displayed', () => {
+4 -10
View File
@@ -4,7 +4,6 @@ import { marketsQuery } from '@vegaprotocol/mock';
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
const dialogCloseBtn = 'dialog-close';
const popoverTrigger = 'popover-trigger';
describe('markets table', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -174,13 +173,8 @@ describe('markets table', { tags: '@smoke' }, () => {
});
function openMarketDropDown() {
cy.getByTestId(dialogCloseBtn).then((button) => {
if (button.is(':visible')) {
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
cy.getByTestId(dialogCloseBtn).click();
}
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
cy.getByTestId(popoverTrigger).click({ force: true });
cy.contains('Loading market data...').should('not.exist');
});
cy.getByTestId(dialogCloseBtn).should('be.visible');
cy.getByTestId(dialogCloseBtn).click();
cy.getByTestId('popover-trigger').click();
cy.contains('Loading market data...').should('not.exist');
}
@@ -1,12 +1,11 @@
import { mockConnectWallet } from '@vegaprotocol/cypress';
beforeEach(() => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
cy.wait('@MarketsCandles');
cy.getByTestId('dialog-close').click();
});
@@ -20,11 +19,11 @@ describe('Desktop view', { tags: '@smoke' }, () => {
cy.getByTestId('navbar')
.find(`[data-testid="navbar-links"] a[data-testid=${link}]`)
.then((element) => {
cy.contains('Loading...').should('not.exist');
cy.wrap(element).click();
cy.wrap(element)
.get('span.absolute.md\\:h-1.w-full')
.should('exist');
cy.contains('No market').should('not.exist');
cy.location('hash').should('equal', hashes[index]);
});
});
@@ -70,6 +69,7 @@ describe('Mobile view', { tags: '@smoke' }, () => {
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').within((el) => {
cy.wrap(el).getByTestId('Trading').click();
cy.contains('No market').should('not.exist');
cy.location('hash').should('equal', '#/markets/market-1');
});
});
+4 -35
View File
@@ -17,50 +17,19 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
});
it('can connect', () => {
// Mock authentication
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
body: {
token: 'test-token',
},
});
// Mock getting keys from wallet
cy.intercept('GET', 'https://wallet.testnet.vega.xyz/api/v1/keys', {
body: {
keys: [
{
algorithm: {
name: 'algo',
version: 1,
},
index: 0,
meta: [],
pub: 'HOSTED_PUBKEY',
tainted: false,
},
],
},
});
cy.getByTestId(connectVegaBtn).click();
mockConnectWallet();
cy.contains('Connect Vega wallet');
cy.contains('Hosted Fairground wallet');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-hosted"]')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.getByTestId(form).find('#wallet').click().type('user');
cy.getByTestId(form).find('#passphrase').click().type('pass');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.wait('@walletReq');
cy.getByTestId(manageVegaBtn).should('exist');
});
it('doesnt connect with invalid credentials', () => {
// Mock incorrect username/password
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
body: {
error: 'No wallet',
},
statusCode: 403, // 403 forbidden invalid crednetials
});
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-hosted"]')
@@ -71,7 +40,7 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
cy.getByTestId('form-error').should('have.text', 'Invalid credentials');
});
it('doesnt connect with empty fields', () => {
it('doesnt connect with invalid fields', () => {
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-hosted"]')
@@ -3,14 +3,12 @@ query AccountHistory(
$assetId: ID!
$accountTypes: [AccountType!]
$dateRange: DateRange
$marketIds: [ID!]
) {
balanceChanges(
filter: {
partyIds: [$partyId]
accountTypes: $accountTypes
assetId: $assetId
marketIds: $marketIds
}
dateRange: $dateRange
) {
@@ -8,7 +8,6 @@ export type AccountHistoryQueryVariables = Types.Exact<{
assetId: Types.Scalars['ID'];
accountTypes?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
dateRange?: Types.InputMaybe<Types.DateRange>;
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
}>;
@@ -24,9 +23,9 @@ export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: {
export const AccountHistoryDocument = gql`
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) {
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange) {
balanceChanges(
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds}
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId}
dateRange: $dateRange
) {
edges {
@@ -59,7 +58,6 @@ export const AccountHistoryDocument = gql`
* assetId: // value for 'assetId'
* accountTypes: // value for 'accountTypes'
* dateRange: // value for 'dateRange'
* marketIds: // value for 'marketIds'
* },
* });
*/
@@ -7,7 +7,7 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact';
import type { ChangeEvent } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
import * as Schema from '@vegaprotocol/types';
@@ -25,10 +25,8 @@ import {
import { AccountTypeMapping } from '@vegaprotocol/types';
import { PriceChart } from 'pennant';
import 'pennant/dist/style.css';
import type { Account } from '@vegaprotocol/accounts';
import { accountsDataProvider } from '@vegaprotocol/accounts';
import { accountsOnlyDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import type { Market } from '@vegaprotocol/market-list';
const DateRange = {
RANGE_1D: '1D',
@@ -99,7 +97,7 @@ const AccountHistoryManager = ({
);
const { data: accounts } = useDataProvider({
dataProvider: accountsDataProvider,
dataProvider: accountsOnlyDataProvider,
variables: variablesForOneTimeQuery,
skip: !pubKey,
});
@@ -120,45 +118,6 @@ const AccountHistoryManager = ({
const [range, setRange] = useState<typeof DateRange[keyof typeof DateRange]>(
DateRange.RANGE_1M
);
const [market, setMarket] = useState<Market | null>(null);
const marketFilterCb = useCallback(
(item: Market) =>
!asset?.id ||
item.tradableInstrument.instrument.product.settlementAsset.id ===
asset?.id,
[asset?.id]
);
const markets = useMemo<Market[] | null>(() => {
const arr =
accounts
?.filter((item: Account) => Boolean(item && item.market))
.map<Market>((item) => item.market as Market) ?? null;
return arr
? Array.from(
new Set(
arr
.filter(marketFilterCb)
.sort((a, b) =>
a.tradableInstrument.instrument.code.localeCompare(
b.tradableInstrument.instrument.code
)
)
)
)
: null;
}, [accounts, marketFilterCb]);
const resolveMarket = useCallback(
(m: Market) => {
setMarket(m);
const newAssetId =
m.tradableInstrument.instrument.product.settlementAsset.id;
const newAsset = assets.find((item) => item.id === newAssetId);
if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) {
setAsset(newAsset);
}
},
[asset, assets]
);
const variables = useMemo(
() => ({
@@ -167,110 +126,62 @@ const AccountHistoryManager = ({
accountTypes: accountType ? [accountType] : undefined,
dateRange:
range === 'All' ? undefined : { start: calculateStartDate(range) },
marketIds: market?.id ? [market.id] : undefined,
}),
[pubKey, asset, accountType, range, market?.id]
[pubKey, asset, accountType, range]
);
const { data } = useAccountHistoryQuery({
variables,
skip: !asset || !pubKey,
});
const accountTypeMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<DropdownMenuItem
key={type}
onClick={() => setAccountType(type as Schema.AccountType)}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [accountType]);
const assetsMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{asset ? asset.symbol : t('Select asset')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{assets.map((a) => (
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
{a.symbol}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [assets, asset]);
const marketsMenu = useMemo(() => {
return accountType === Schema.AccountType.ACCOUNT_TYPE_MARGIN &&
markets?.length ? (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{markets?.map((m) => (
<DropdownMenuItem key={m.id} onClick={() => resolveMarket(m)}>
{m.tradableInstrument.instrument.code}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : null;
}, [markets, market, accountType, resolveMarket]);
useEffect(() => {
if (
accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN ||
(asset &&
market &&
market.tradableInstrument.instrument.product.settlementAsset.id !==
asset.id)
) {
setMarket(null);
}
}, [accountType, asset, market]);
return (
<div className="h-full w-full flex flex-col gap-8">
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
<div className="flex items-center gap-4 shrink-0">
<>
{accountTypeMenu}
{assetsMenu}
{marketsMenu}
</>
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<DropdownMenuItem
key={type}
onClick={() => setAccountType(type as Schema.AccountType)}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{asset ? asset.symbol : t('Select asset')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{assets.map((a) => (
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
{a.symbol}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="pt-1 justify-items-end">
<Toggle
@@ -123,7 +123,7 @@ export const MarketLiquiditySupplied = ({
{showMessage && (
<p className="mt-4">
{t(
'The market has sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
'The market is in an auction because there are no priced limit orders, which are required to deploy liquidity commitment pegged orders. This means the order book is empty on one or both sides.'
)}
</p>
)}
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { proposalsDataProvider } from '@vegaprotocol/governance';
import { proposalsListDataProvider } from '@vegaprotocol/governance';
import take from 'lodash/take';
import * as Types from '@vegaprotocol/types';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
@@ -19,7 +19,7 @@ export const ProposedMarkets = () => {
};
}, []);
const { data } = useDataProvider({
dataProvider: proposalsDataProvider,
dataProvider: proposalsListDataProvider,
variables,
skipUpdates: true,
});

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