Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c53ac3b73a | ||
|
|
1489fe9e16 | ||
|
|
80cb921713 | ||
|
|
1f26c1494b | ||
|
|
49f6849c01 | ||
|
|
f68c6aabad | ||
|
|
c50b06100c | ||
|
|
efeccc7972 | ||
|
|
95f4e489b2 | ||
|
|
0033f3c5f5 | ||
|
|
abf69786ba | ||
|
|
0a5fb9d917 | ||
|
|
8e82bf43fb | ||
|
|
4bb57e9c47 | ||
|
|
ce6d4cb35d | ||
|
|
647f04656f | ||
|
|
67186bf4c0 |
@@ -18,9 +18,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 16.15.1
|
||||
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v4
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.14.0
|
||||
node-version: 16.15.1
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
- name: Generate queries
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.14.0
|
||||
node-version: 16.15.1
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
- name: Check PR title
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.14.0
|
||||
node-version: 16.15.1
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
- name: Generate queries
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
id: Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.14.0
|
||||
node-version: 16.15.1
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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
|
||||
# }
|
||||
# }
|
||||
#}
|
||||
@@ -0,0 +1,99 @@
|
||||
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>;
|
||||
@@ -0,0 +1,56 @@
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
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;
|
||||
@@ -0,0 +1,127 @@
|
||||
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;
|
||||
@@ -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-zinc-800 dark:text-zinc-300">
|
||||
<p className="text-vega-dark-100 dark:text-vega-light-200">
|
||||
{subtitle}
|
||||
{tooltipInfo ? (
|
||||
<Tooltip description={tooltipInfo} align="center">
|
||||
<span>
|
||||
<Icon
|
||||
name="info-sign"
|
||||
className="ml-2 text-zinc-400 dark:text-zinc-600"
|
||||
className="ml-2 text-vega-light-300 dark:text-vega-dark-300"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -44,12 +44,12 @@ export const InfoPanel = ({
|
||||
text={id}
|
||||
startChars={visibleChars}
|
||||
endChars={visibleChars}
|
||||
className="text-black dark:text-zinc-200"
|
||||
className="text-vega-dark-100 dark:text-vega-light-200"
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
title={id}
|
||||
className="text-black dark:text-zinc-200 truncate ..."
|
||||
className="text-vega-dark-100 dark:text-vega-light-200 truncate ..."
|
||||
>
|
||||
{id}
|
||||
</p>
|
||||
@@ -70,7 +70,7 @@ export const InfoPanel = ({
|
||||
</div>
|
||||
{copy && (
|
||||
<CopyWithTooltip text={id}>
|
||||
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
|
||||
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
|
||||
<Icon name="duplicate" />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Routes } from '../../../routes/route-names';
|
||||
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
|
||||
assetId: string;
|
||||
asDialog?: boolean;
|
||||
showAssetSymbol?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -18,12 +19,22 @@ 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, ...props }: AssetLinkProps) => {
|
||||
export const AssetLink = ({
|
||||
assetId,
|
||||
asDialog,
|
||||
showAssetSymbol = false,
|
||||
...props
|
||||
}: AssetLinkProps) => {
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
|
||||
const open = useAssetDetailsDialogStore((state) => state.open);
|
||||
const navigate = useNavigate();
|
||||
const label = asset?.name ? asset.name : assetId;
|
||||
const label = asset
|
||||
? showAssetSymbol
|
||||
? asset?.symbol
|
||||
: asset?.name
|
||||
: assetId;
|
||||
|
||||
return (
|
||||
<ButtonLink
|
||||
data-testid="asset-link"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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,19 +3,47 @@ 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, ...props }: PartyLinkProps) => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="underline font-mono"
|
||||
{...props}
|
||||
to={`/${Routes.PARTIES}/${id}`}
|
||||
>
|
||||
<Hash text={id} />
|
||||
<Hash text={truncate ? truncateMiddle(id) : id} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface DeterministicOrderDetailsProps {
|
||||
}
|
||||
|
||||
export const wrapperClasses =
|
||||
'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';
|
||||
'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';
|
||||
|
||||
/**
|
||||
* 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-gray-500 mb-12">
|
||||
<p className="text-vega-light-400 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-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
|
||||
<button className="bg-vega-light-100 dark:bg-vega-dark-100 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-zinc-200 dark:border-zinc-800 rounded-md p-5 mb-5',
|
||||
'border border-vega-light-150 dark:border-vega-dark-150 rounded-md p-5 mb-5',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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>
|
||||
<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 PriceInMarketProps = {
|
||||
export type SizeInMarketProps = {
|
||||
marketId: string;
|
||||
size?: string | number;
|
||||
decimalSource?: DecimalSource;
|
||||
@@ -17,7 +17,7 @@ const SizeInMarket = ({
|
||||
marketId,
|
||||
size,
|
||||
decimalSource = 'MARKET',
|
||||
}: PriceInMarketProps) => {
|
||||
}: SizeInMarketProps) => {
|
||||
const { data } = useExplorerMarketQuery({
|
||||
variables: { id: marketId },
|
||||
fetchPolicy: 'cache-first',
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
type Deposit = components['schemas']['vegaBuiltinAssetDeposit'];
|
||||
|
||||
const fullMock: Deposit = {
|
||||
partyId: 'party123',
|
||||
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
vegaAssetId: 'asset123',
|
||||
amount: 'amount123',
|
||||
};
|
||||
|
||||
+3
-2
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
type Withdrawal = components['schemas']['vegaBuiltinAssetWithdrawal'];
|
||||
|
||||
const fullMock: Withdrawal = {
|
||||
partyId: 'party123',
|
||||
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
vegaAssetId: 'asset123',
|
||||
amount: 'amount123',
|
||||
};
|
||||
@@ -67,11 +67,12 @@ 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,10 +10,12 @@ import { TxDetailsChainEventDeposit } from './tx-erc20-deposit';
|
||||
type Deposit = components['schemas']['vegaERC20Deposit'];
|
||||
|
||||
const fullMock: Deposit = {
|
||||
vegaAssetId: 'asset123',
|
||||
vegaAssetId:
|
||||
'0000000000000000000000000000000000000000000000000000000000000002',
|
||||
amount: 'amount123',
|
||||
sourceEthereumAddress: 'eth123',
|
||||
targetPartyId: 'vega123',
|
||||
targetPartyId:
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
};
|
||||
|
||||
describe('Chain Event: ERC20 asset deposit', () => {
|
||||
|
||||
@@ -13,7 +13,8 @@ const fullMock: Deposit = {
|
||||
amount: 'amount123',
|
||||
blockTime: 'block123',
|
||||
ethereumAddress: 'eth123',
|
||||
vegaPublicKey: 'vega123',
|
||||
vegaPublicKey:
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
};
|
||||
|
||||
describe('Chain Event: Stake deposit', () => {
|
||||
|
||||
@@ -13,7 +13,8 @@ const fullMock: Remove = {
|
||||
amount: 'amount123',
|
||||
blockTime: 'block123',
|
||||
ethereumAddress: 'eth123',
|
||||
vegaPublicKey: 'vega123',
|
||||
vegaPublicKey:
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
};
|
||||
|
||||
describe('Chain Event: Stake remove', () => {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ export const ChainResponseCode = ({
|
||||
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
|
||||
|
||||
return (
|
||||
<div title={`Response code: ${code} - ${label}`}>
|
||||
<div title={`Response code: ${code} - ${label}`} className="inline-block">
|
||||
<span
|
||||
className="mr-2"
|
||||
aria-label={isSuccess ? 'Success' : 'Warning'}
|
||||
|
||||
@@ -14,10 +14,14 @@ 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
|
||||
const sharedHeaderProps = {
|
||||
export const sharedHeaderProps = {
|
||||
// Ensures that multi line contents still have the header aligned to the first line
|
||||
className: 'align-top',
|
||||
};
|
||||
@@ -31,6 +35,7 @@ export const TxDetailsShared = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
hideTypeRow = false,
|
||||
}: TxDetailsSharedProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
@@ -41,10 +46,12 @@ export const TxDetailsShared = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||
<TableCell>{txData.type}</TableCell>
|
||||
</TableRow>
|
||||
{hideTypeRow === false ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||
<TableCell>{txData.type}</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Hash')}</TableCell>
|
||||
<TableCell>
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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" />;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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,6 +23,7 @@ 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;
|
||||
@@ -108,6 +109,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
return TxDetailsUndelegate;
|
||||
case 'State Variable Proposal':
|
||||
return TxDetailsStateVariable;
|
||||
case 'Transfer Funds':
|
||||
return TxDetailsTransfer;
|
||||
default:
|
||||
return TxDetailsGeneric;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
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,14 +14,17 @@ 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: 'Liquidity Provision',
|
||||
LiquidityProvisionCancellation: 'Liquidity Cancellation',
|
||||
LiquidityProvisionAmendment: 'Liquidity Amendment',
|
||||
LiquidityProvisionSubmission: 'LP order',
|
||||
'Liquidity Provision Order': 'LP order',
|
||||
LiquidityProvisionCancellation: 'LP cancel',
|
||||
LiquidityProvisionAmendment: 'LP update',
|
||||
'Amend LiquidityProvision Order': 'Amend LP',
|
||||
ProposalSubmission: 'Governance Proposal',
|
||||
AnnounceNode: 'Node Announcement',
|
||||
NodeVote: 'Node Vote',
|
||||
@@ -31,10 +34,11 @@ const displayString: StringMap = {
|
||||
DelegateSubmission: 'Delegation',
|
||||
UndelegateSubmission: 'Undelegation',
|
||||
KeyRotateSubmission: 'Key Rotation',
|
||||
StateVariableProposal: 'State Variable Proposal',
|
||||
StateVariableProposal: 'State Variable',
|
||||
Transfer: 'Transfer',
|
||||
CancelTransfer: 'Cancel Transfer',
|
||||
ValidatorHeartbeat: 'Validator Heartbeat',
|
||||
ValidatorHeartbeat: 'Heartbeat',
|
||||
'Batch Market Instructions': 'Batch',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -132,7 +136,8 @@ export function getLabelForChainEvent(
|
||||
export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
||||
let type = displayString[orderType] || orderType;
|
||||
|
||||
let colours = 'text-white dark:text-white bg-zinc-800 dark:bg-zinc-800';
|
||||
let colours =
|
||||
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
|
||||
|
||||
// This will get unwieldy and should probably produce a different colour of tag
|
||||
if (type === 'Chain Event' && !!command?.chainEvent) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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,7 +37,9 @@ 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-zinc-500">ID: </span>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
ID:
|
||||
</span>
|
||||
<TruncatedLink
|
||||
to={`/${Routes.TX}/${toHex(hash)}`}
|
||||
text={hash}
|
||||
@@ -49,7 +51,9 @@ 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-zinc-500">By: </span>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
By:
|
||||
</span>
|
||||
<TruncatedLink
|
||||
to={`/${Routes.PARTIES}/${submitter}`}
|
||||
text={submitter}
|
||||
@@ -64,7 +68,9 @@ 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-zinc-500">Block: </span>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
Block:
|
||||
</span>
|
||||
<TruncatedLink
|
||||
to={`/${Routes.BLOCKS}/${block}`}
|
||||
text={block}
|
||||
@@ -76,7 +82,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-zinc-500">
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
Success:
|
||||
</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-zinc-500 uppercase">
|
||||
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
|
||||
<div className="col-span-3">
|
||||
<span className="hidden xl:inline">Transaction </span>
|
||||
<span>ID</span>
|
||||
|
||||
@@ -68,7 +68,7 @@ const Party = () => {
|
||||
return (
|
||||
<section>
|
||||
<h1
|
||||
className="font-alpha uppercase font-xl mb-4 text-zinc-800 dark:text-zinc-200"
|
||||
className="font-alpha uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
|
||||
data-testid="parties-header"
|
||||
>
|
||||
{t('Public key')}
|
||||
|
||||
+62
-21
@@ -15,6 +15,7 @@ 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': {
|
||||
/**
|
||||
@@ -40,8 +41,6 @@ export interface paths {
|
||||
};
|
||||
}
|
||||
|
||||
export type webhooks = Record<string, never>;
|
||||
|
||||
export interface components {
|
||||
schemas: {
|
||||
/**
|
||||
@@ -66,7 +65,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
|
||||
@@ -174,7 +173,7 @@ export interface components {
|
||||
readonly '@type'?: string;
|
||||
[key: string]: unknown | undefined;
|
||||
};
|
||||
/** Used announce a node as a new pending validator */
|
||||
/** Used to announce a node as a new pending validator */
|
||||
readonly v1AnnounceNode: {
|
||||
/** AvatarURL of the validator */
|
||||
readonly avatarUrl?: string;
|
||||
@@ -269,13 +268,13 @@ export interface components {
|
||||
readonly v1ETHAddress: {
|
||||
readonly address?: string;
|
||||
};
|
||||
/** A transaction to allow validator to rotate their ethereum keys */
|
||||
/** A transaction to allow a 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 adress to rotate to */
|
||||
/** The new address to rotate to */
|
||||
readonly newAddress?: string;
|
||||
/** Ethereum public key to use as a submitter to allow automatic signature generation */
|
||||
readonly submitterAddress?: string;
|
||||
@@ -309,7 +308,9 @@ 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
|
||||
@@ -323,17 +324,35 @@ 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 */
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
readonly nodeVote?: components['schemas']['v1NodeVote'];
|
||||
/**
|
||||
* Format: uint64
|
||||
@@ -349,30 +368,50 @@ export interface components {
|
||||
* slightly differently, causing a different hash.
|
||||
*/
|
||||
readonly nonce?: string;
|
||||
/** Oracles */
|
||||
/**
|
||||
* Oracles
|
||||
* Command to submit new oracle data from third party providers
|
||||
*/
|
||||
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'];
|
||||
/** User commands */
|
||||
/** A command for submitting an order */
|
||||
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 smartcontract */
|
||||
/** The ID of the node that will be signed in or out of the smart contract */
|
||||
readonly validatorNodeId?: string;
|
||||
};
|
||||
/** A transaction to allow validator to rotate their Vega keys */
|
||||
/** A transaction to allow a validator to rotate their Vega keys */
|
||||
readonly v1KeyRotateSubmission: {
|
||||
/** Hash of currently used public key */
|
||||
readonly currentPubKeyHash?: string;
|
||||
@@ -496,8 +535,8 @@ export interface components {
|
||||
/** Specific details for a one off transfer */
|
||||
readonly v1OneOffTransfer: {
|
||||
/**
|
||||
* A unix timestamp in second. Time at which the
|
||||
* transfer should be delivered in the to account
|
||||
* A unix timestamp in seconds. Time at which the
|
||||
* transfer should be delivered into the To account
|
||||
* Format: int64
|
||||
*/
|
||||
readonly deliverOn?: string;
|
||||
@@ -512,7 +551,7 @@ export interface components {
|
||||
readonly payload?: string;
|
||||
/**
|
||||
* @description The source from which the data is coming from. Must be base64 encoded.
|
||||
* Oracle data a type of external data source data.
|
||||
* Oracle data is a type of external data source data.
|
||||
*/
|
||||
readonly source?: components['schemas']['OracleDataSubmissionOracleSource'];
|
||||
};
|
||||
@@ -603,7 +642,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 an data source data. */
|
||||
/** @description PropertyKey describes the property key contained in data source data. */
|
||||
readonly v1PropertyKey: {
|
||||
/** @description name is the name of the property. */
|
||||
readonly name?: string;
|
||||
@@ -650,6 +689,7 @@ 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
|
||||
@@ -708,6 +748,7 @@ 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'];
|
||||
@@ -969,7 +1010,7 @@ export interface components {
|
||||
readonly sourceEthereumAddress?: string;
|
||||
/** The Vega network internal identifier of the asset */
|
||||
readonly vegaAssetId?: string;
|
||||
/** The updated withdraw threshold */
|
||||
/** The updated withdrawal threshold */
|
||||
readonly withdrawThreshold?: string;
|
||||
};
|
||||
/** An asset allow-listing for an ERC20 token */
|
||||
@@ -1064,7 +1105,7 @@ export interface components {
|
||||
/** The ethereum address of the old signer */
|
||||
readonly oldSigner?: string;
|
||||
};
|
||||
/** The threshold have been updated on the multisigcontrol */
|
||||
/** The threshold has been updated on the multisig control */
|
||||
readonly vegaERC20ThresholdSet: {
|
||||
/**
|
||||
* Format: int64
|
||||
@@ -1078,13 +1119,13 @@ export interface components {
|
||||
* Format: int64
|
||||
*/
|
||||
readonly newThreshold?: number;
|
||||
/** The nonce create by the vega network */
|
||||
/** The nonce created by the Vega network */
|
||||
readonly nonce?: string;
|
||||
};
|
||||
readonly vegaERC20Update: {
|
||||
/**
|
||||
* The lifetime limits deposit per address.
|
||||
* This is will be interpreted against the asset decimals.
|
||||
* This will be interpreted against the asset decimals.
|
||||
* note: this is a temporary measure that can be changed by governance
|
||||
*/
|
||||
readonly lifetimeLimit?: string;
|
||||
@@ -1237,7 +1278,7 @@ export interface components {
|
||||
* price levels over which automated liquidity provision orders will be deployed
|
||||
*/
|
||||
readonly lpPriceRange?: string;
|
||||
/** Optional new market meta data, tags */
|
||||
/** Optional new market metadata, tags */
|
||||
readonly metadata?: readonly string[];
|
||||
/**
|
||||
* Decimal places for order sizes, sets what size the smallest order / position on the market can be
|
||||
|
||||
@@ -2761,7 +2761,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "86666.297",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "68026.0050809900766642912",
|
||||
"locked_amount": "67847.5938683683107992492",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "86666.297",
|
||||
@@ -2827,7 +2827,7 @@
|
||||
"tranche_end": "2023-06-01T00:00:00.000Z",
|
||||
"total_added": "2500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1366.694488960114",
|
||||
"locked_amount": "1356.37321301383775",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "2500",
|
||||
@@ -3214,8 +3214,8 @@
|
||||
"tranche_start": "2023-02-01T00:00:00.000Z",
|
||||
"tranche_end": "2023-08-01T00:00:00.000Z",
|
||||
"total_added": "37500",
|
||||
"total_removed": "3009.936598125",
|
||||
"locked_amount": "33251.80085558624625",
|
||||
"total_removed": "3143.644010625",
|
||||
"locked_amount": "33096.1263620319225",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -3249,6 +3249,11 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x6f125819654942fd6c08c0929e7f9e9629eb65c2cfa92acae0f8db9685a43537"
|
||||
},
|
||||
{
|
||||
"amount": "133.7074125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xe6d2a4fbaaba23872bffd24f3300875c90e280b3b8f1bf1c8beb33d7c2fab25f"
|
||||
},
|
||||
{
|
||||
"amount": "183.137181525",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -3290,6 +3295,12 @@
|
||||
"tranche_id": 34,
|
||||
"tx": "0x6f125819654942fd6c08c0929e7f9e9629eb65c2cfa92acae0f8db9685a43537"
|
||||
},
|
||||
{
|
||||
"amount": "133.7074125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 34,
|
||||
"tx": "0xe6d2a4fbaaba23872bffd24f3300875c90e280b3b8f1bf1c8beb33d7c2fab25f"
|
||||
},
|
||||
{
|
||||
"amount": "183.137181525",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -3304,8 +3315,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "7500",
|
||||
"withdrawn_tokens": "734.823415425",
|
||||
"remaining_tokens": "6765.176584575"
|
||||
"withdrawn_tokens": "868.530827925",
|
||||
"remaining_tokens": "6631.469172075"
|
||||
},
|
||||
{
|
||||
"address": "0x0B4e6fcE839B01ef43DA6F890FAC7B1Afb004600",
|
||||
@@ -3337,7 +3348,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "129999.45",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "67963.9399107138444471",
|
||||
"locked_amount": "67785.69147587502055098",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129999.45",
|
||||
@@ -3403,7 +3414,7 @@
|
||||
"tranche_end": "2023-09-03T00:00:00.000Z",
|
||||
"total_added": "62600",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "33185.77934424150344",
|
||||
"locked_amount": "33056.91100329781984",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10000",
|
||||
@@ -3596,7 +3607,7 @@
|
||||
"tranche_end": "2023-09-17T00:00:00.000Z",
|
||||
"total_added": "5000",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "2842.40217529173",
|
||||
"locked_amount": "2832.1091768138",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "5000",
|
||||
@@ -3807,7 +3818,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "97499.58",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "9888.4955479431840552672",
|
||||
"locked_amount": "9713.6504379590992984068",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "97499.58",
|
||||
@@ -3840,7 +3851,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "135173.4239508",
|
||||
"total_removed": "98230.390980249184455396",
|
||||
"locked_amount": "13515.866236501447216654821204",
|
||||
"locked_amount": "13276.88315690216652689038794",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "135173.4239508",
|
||||
@@ -3886,7 +3897,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "32499.86",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "4159.9193118355352539772",
|
||||
"locked_amount": "4086.3649934787768574158",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "32499.86",
|
||||
@@ -3919,7 +3930,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "10833.29",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1354.013369100967212235",
|
||||
"locked_amount": "1330.0721522300519402316",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10833.29",
|
||||
@@ -3952,7 +3963,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "22749.93",
|
||||
"total_removed": "4720.860935375",
|
||||
"locked_amount": "5061.6043871236901291745",
|
||||
"locked_amount": "4972.1067712859949945882",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "6500",
|
||||
@@ -4103,8 +4114,8 @@
|
||||
"tranche_start": "2022-11-01T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-01T00:00:00.000Z",
|
||||
"total_added": "22500",
|
||||
"total_removed": "4546.97235645",
|
||||
"locked_amount": "8514.6164249539599",
|
||||
"total_removed": "4680.6740139",
|
||||
"locked_amount": "8421.2117288213625",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -4133,6 +4144,11 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xb40f1c9a538e1998da74acd7fceea8a718e97b2af8e903277e16a4b729628a52"
|
||||
},
|
||||
{
|
||||
"amount": "133.70165745",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x95c8bfd256f138974dcc7c0db2164496b30ca09f30b858453b41d70e47036d6f"
|
||||
},
|
||||
{
|
||||
"amount": "167.6680479",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -4234,6 +4250,12 @@
|
||||
"tranche_id": 33,
|
||||
"tx": "0xb40f1c9a538e1998da74acd7fceea8a718e97b2af8e903277e16a4b729628a52"
|
||||
},
|
||||
{
|
||||
"amount": "133.70165745",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 33,
|
||||
"tx": "0x95c8bfd256f138974dcc7c0db2164496b30ca09f30b858453b41d70e47036d6f"
|
||||
},
|
||||
{
|
||||
"amount": "167.6680479",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -4320,8 +4342,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "7500",
|
||||
"withdrawn_tokens": "4546.97235645",
|
||||
"remaining_tokens": "2953.02764355"
|
||||
"withdrawn_tokens": "4680.6740139",
|
||||
"remaining_tokens": "2819.3259861"
|
||||
},
|
||||
{
|
||||
"address": "0x2539b51EbDE65a75672aBcfE9439a706a99D18D1",
|
||||
@@ -4346,7 +4368,7 @@
|
||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||
"total_added": "1939928.38",
|
||||
"total_removed": "928642.9598472029154",
|
||||
"locked_amount": "534119.9961293638279815094",
|
||||
"locked_amount": "530126.4601568372440486808",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1852091.69",
|
||||
@@ -36638,7 +36660,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "3732368.4671",
|
||||
"total_removed": "609657.626547646980493",
|
||||
"locked_amount": "845257.797948570775371065827",
|
||||
"locked_amount": "839121.029033631540661543897",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1998.95815",
|
||||
@@ -37975,8 +37997,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "15870102.715470999700000001",
|
||||
"total_removed": "572826.70366554650274952",
|
||||
"locked_amount": "8296917.466428724395809164572318349628278",
|
||||
"total_removed": "575133.08469788626314452",
|
||||
"locked_amount": "8275156.8778745719866654003456575909002433",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "16249.93",
|
||||
@@ -38535,6 +38557,16 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xe5fb2af901c286e061280cff77fcf8056e52dffb23b9fc7bba3dfb15abee8abd"
|
||||
},
|
||||
{
|
||||
"amount": "627.592612816272125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xaf8d0b715946e8724a7013e1bbc17ace8f1ad153f37a9ae2f63ac02323d3f6a1"
|
||||
},
|
||||
{
|
||||
"amount": "1678.78841952348827",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tx": "0x8484839ab3c62c0f75b415b468f8297f24655622fb9fb94db7652f00093c9cec"
|
||||
},
|
||||
{
|
||||
"amount": "856.08784586478614",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
@@ -40267,6 +40299,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0xe5fb2af901c286e061280cff77fcf8056e52dffb23b9fc7bba3dfb15abee8abd"
|
||||
},
|
||||
{
|
||||
"amount": "627.592612816272125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xaf8d0b715946e8724a7013e1bbc17ace8f1ad153f37a9ae2f63ac02323d3f6a1"
|
||||
},
|
||||
{
|
||||
"amount": "966.75883976995675",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -41409,8 +41447,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "259998.8875",
|
||||
"withdrawn_tokens": "123657.945723010546375",
|
||||
"remaining_tokens": "136340.941776989453625"
|
||||
"withdrawn_tokens": "124285.5383358268185",
|
||||
"remaining_tokens": "135713.3491641731815"
|
||||
},
|
||||
{
|
||||
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
|
||||
@@ -41637,6 +41675,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0x4870598253a2a664d9c96af37904f08ac772ee9620b5975560ef7e7fb51c9a7f"
|
||||
},
|
||||
{
|
||||
"amount": "1678.78841952348827",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x8484839ab3c62c0f75b415b468f8297f24655622fb9fb94db7652f00093c9cec"
|
||||
},
|
||||
{
|
||||
"amount": "856.08784586478614",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
@@ -41867,8 +41911,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "150551.801",
|
||||
"withdrawn_tokens": "70289.2123238900799",
|
||||
"remaining_tokens": "80262.5886761099201"
|
||||
"withdrawn_tokens": "71968.00074341356817",
|
||||
"remaining_tokens": "78583.80025658643183"
|
||||
},
|
||||
{
|
||||
"address": "0x4d982Ab0823fD2f48e934a7be2bb0a5374a26148",
|
||||
@@ -43629,8 +43673,8 @@
|
||||
"tranche_start": "2021-11-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-05T00:00:00.000Z",
|
||||
"total_added": "14597706.0446472999",
|
||||
"total_removed": "4396450.802668984389787156",
|
||||
"locked_amount": "1938216.002398478950673792862823089",
|
||||
"total_removed": "4397321.114354183114240906",
|
||||
"locked_amount": "1918126.765987955843750323100941893",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129284.449",
|
||||
@@ -43889,6 +43933,11 @@
|
||||
"user": "0xfc3b2D0b548d3edBb512CeE0Bb79Fb7FaD50AaF3",
|
||||
"tx": "0x43c34302d1895983b4f5b28ad186ea31662a045f3c7c63075a1c9db4b059e42d"
|
||||
},
|
||||
{
|
||||
"amount": "870.31168519872445375",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xec81b6b72e023ad393f023ea8ca6ff754d39370a61f77b3e519e3d2285835d39"
|
||||
},
|
||||
{
|
||||
"amount": "1333.9237119810715295",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -46809,6 +46858,12 @@
|
||||
"tranche_id": 3,
|
||||
"tx": "0x5fcd1783bbd5f8b519d94782580777ff37d84df742f617baf632fc693615f39a"
|
||||
},
|
||||
{
|
||||
"amount": "870.31168519872445375",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xec81b6b72e023ad393f023ea8ca6ff754d39370a61f77b3e519e3d2285835d39"
|
||||
},
|
||||
{
|
||||
"amount": "1333.9237119810715295",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -49253,8 +49308,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "359123.469575",
|
||||
"withdrawn_tokens": "310868.25548215842910375",
|
||||
"remaining_tokens": "48255.21409284157089625"
|
||||
"withdrawn_tokens": "311738.5671673571535575",
|
||||
"remaining_tokens": "47384.9024076428464425"
|
||||
},
|
||||
{
|
||||
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
|
||||
@@ -50598,7 +50653,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "5778205.3912159303",
|
||||
"total_removed": "2749067.463242913023296295",
|
||||
"locked_amount": "448897.461240245945578932424695999",
|
||||
"locked_amount": "440960.082435758127733940976567038",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "552496.6455",
|
||||
@@ -52624,8 +52679,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "472355.6199999996",
|
||||
"total_removed": "34093.0379154332685",
|
||||
"locked_amount": "133935.9297845122141846487163876",
|
||||
"total_removed": "34139.7667776862685",
|
||||
"locked_amount": "132963.5236706734184837412785388",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "3000",
|
||||
@@ -59344,6 +59399,11 @@
|
||||
"user": "0x28FC83947F02f59Cb36b40f97f2D32BBC5D00585",
|
||||
"tx": "0x9d1ced8d04e8d58af92b1434bcb269487a3f6ddf4305c0739ab0f158f5401ec6"
|
||||
},
|
||||
{
|
||||
"amount": "46.728862253",
|
||||
"user": "0xB18ba44b42d48206aA9f6cBEb7a9e463F558847a",
|
||||
"tx": "0x0a0e9842787da5fb2a91c18e4f83f2d32a4b2fd05a5c3f05d0541eb56cd5f0db"
|
||||
},
|
||||
{
|
||||
"amount": "13.1116203702",
|
||||
"user": "0x4cBC0C88d8FE503f62823B42f30a4900292C13bE",
|
||||
@@ -69435,6 +69495,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "46.728862253",
|
||||
"user": "0xB18ba44b42d48206aA9f6cBEb7a9e463F558847a",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x0a0e9842787da5fb2a91c18e4f83f2d32a4b2fd05a5c3f05d0541eb56cd5f0db"
|
||||
},
|
||||
{
|
||||
"amount": "25.047884956",
|
||||
"user": "0xB18ba44b42d48206aA9f6cBEb7a9e463F558847a",
|
||||
@@ -69443,8 +69509,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "100",
|
||||
"withdrawn_tokens": "25.047884956",
|
||||
"remaining_tokens": "74.952115044"
|
||||
"withdrawn_tokens": "71.776747209",
|
||||
"remaining_tokens": "28.223252791"
|
||||
},
|
||||
{
|
||||
"address": "0x4c587d5981bC62F2821f138Ed5e33caF79b9B84d",
|
||||
|
||||
@@ -201,12 +201,21 @@ context('Staking Page - verify elements on page', function () {
|
||||
cy.get(stakeShare)
|
||||
.invoke('text')
|
||||
.then(($stakePercentage) => {
|
||||
if ($stakePercentage != '-') {
|
||||
cy.wrap($stakePercentage).should(
|
||||
'match',
|
||||
/\b(?<!\.)(?!0+(?:\.0+)?%)(?:\d|[1-9]\d|100)(?:(?<!100)\.\d+)?%/
|
||||
);
|
||||
}
|
||||
// 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+)?)%/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -744,13 +744,22 @@
|
||||
"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",
|
||||
|
||||
@@ -363,6 +363,7 @@ 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,8 +59,6 @@ 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 {
|
||||
|
||||
+1
@@ -236,6 +236,7 @@ export const StandbyPendingValidatorsTable = ({
|
||||
customThemeParams={NODE_LIST_GRID_STYLES}
|
||||
rowHeight={52}
|
||||
defaultColDef={defaultColDef}
|
||||
tooltipShowDelay={0}
|
||||
animateRows={true}
|
||||
suppressCellFocus={true}
|
||||
overlayNoRowsTemplate={t('noValidators')}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
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,19 +1,25 @@
|
||||
import { useMemo } from 'react';
|
||||
import React, { 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 * as Schema from '@vegaprotocol/types';
|
||||
import countryData from '../../../components/country-selector/country-data';
|
||||
import { SubHeading } from '../../../components/heading';
|
||||
import {
|
||||
getLastEpochScoreAndPerformance,
|
||||
@@ -23,6 +29,7 @@ import {
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
getUnnormalisedVotingPower,
|
||||
getStakePercentage,
|
||||
} from '../shared';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { StakingNodeFieldsFragment } from './__generated__/Staking';
|
||||
@@ -62,7 +69,7 @@ export const ValidatorTable = ({
|
||||
stakedTotal,
|
||||
previousEpochData,
|
||||
}: ValidatorTableProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { ETHERSCAN_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
appState: { decimals },
|
||||
@@ -81,10 +88,7 @@ export const ValidatorTable = ({
|
||||
node.stakedTotal
|
||||
);
|
||||
|
||||
const stakePercentage =
|
||||
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
|
||||
? '-'
|
||||
: stakedOnNode.dividedBy(total).times(100).dp(2).toString() + '%';
|
||||
const stakePercentage = getStakePercentage(total, stakedOnNode);
|
||||
|
||||
const totalPenaltiesAmount = getTotalPenalties(
|
||||
rawValidatorScore,
|
||||
@@ -94,158 +98,207 @@ export const ValidatorTable = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<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>
|
||||
<>
|
||||
<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="mb-10">
|
||||
{t('validatorTableIntro')}{' '}
|
||||
<UTLink
|
||||
href={ExternalLinks.VALIDATOR_FORUM}
|
||||
target="_blank"
|
||||
data-testid="validator-forum-link"
|
||||
>
|
||||
{t('onTheForum')}
|
||||
</UTLink>
|
||||
<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>
|
||||
|
||||
<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,6 +8,7 @@ import {
|
||||
getFormattedPerformanceScore,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
getStakePercentage,
|
||||
} from './shared';
|
||||
|
||||
describe('getLastEpochScoreAndPerformance', () => {
|
||||
@@ -139,3 +140,23 @@ 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%'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,3 +95,8 @@ 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() + '%';
|
||||
|
||||
@@ -66,3 +66,25 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
.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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ 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(() => {
|
||||
@@ -173,8 +174,13 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
function openMarketDropDown() {
|
||||
cy.getByTestId(dialogCloseBtn).should('be.visible');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
cy.getByTestId('popover-trigger').click();
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -42,10 +42,18 @@ export const update: Update<
|
||||
},
|
||||
};
|
||||
if (delta.buy) {
|
||||
updatedData.depth.buy = updateLevels(data.depth.buy ?? [], delta.buy);
|
||||
updatedData.depth.buy = updateLevels(
|
||||
data.depth.buy ?? [],
|
||||
delta.buy,
|
||||
false
|
||||
);
|
||||
}
|
||||
if (delta.sell) {
|
||||
updatedData.depth.sell = updateLevels(data.depth.sell ?? [], delta.sell);
|
||||
updatedData.depth.sell = updateLevels(
|
||||
data.depth.sell ?? [],
|
||||
delta.sell,
|
||||
true
|
||||
);
|
||||
}
|
||||
updatedData.depth.sequenceNumber = delta.sequenceNumber;
|
||||
return updatedData;
|
||||
|
||||
@@ -26,9 +26,19 @@ export interface OrderbookRowData {
|
||||
|
||||
type PartialOrderbookRowData = Pick<OrderbookRowData, 'price' | 'ask' | 'bid'>;
|
||||
|
||||
export type OrderbookData = Partial<
|
||||
Omit<MarketData, '__typename' | 'market'>
|
||||
> & { rows: OrderbookRowData[] | null };
|
||||
type OrderbookMarketData = Pick<
|
||||
MarketData,
|
||||
| 'bestStaticBidPrice'
|
||||
| 'bestStaticOfferPrice'
|
||||
| 'indicativePrice'
|
||||
| 'indicativeVolume'
|
||||
| 'marketTradingMode'
|
||||
>;
|
||||
|
||||
export type OrderbookData = Partial<OrderbookMarketData> & {
|
||||
rows: OrderbookRowData[] | null;
|
||||
midPrice?: string;
|
||||
};
|
||||
|
||||
export const getPriceLevel = (price: string | bigint, resolution: number) => {
|
||||
const p = BigInt(price);
|
||||
@@ -40,6 +50,18 @@ export const getPriceLevel = (price: string | bigint, resolution: number) => {
|
||||
return priceLevel.toString();
|
||||
};
|
||||
|
||||
export const getMidPrice = (
|
||||
sell: PriceLevelFieldsFragment[] | null | undefined,
|
||||
buy: PriceLevelFieldsFragment[] | null | undefined,
|
||||
resolution: number
|
||||
) =>
|
||||
buy?.length && sell?.length
|
||||
? getPriceLevel(
|
||||
(BigInt(buy[0].price) + BigInt(sell[0].price)) / BigInt(2),
|
||||
resolution
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const getMaxVolumes = (orderbookData: OrderbookRowData[]) => ({
|
||||
bid: Math.max(...orderbookData.map((data) => data.bid)),
|
||||
ask: Math.max(...orderbookData.map((data) => data.ask)),
|
||||
@@ -157,8 +179,15 @@ export const compactRows = (
|
||||
}
|
||||
orderbookData.push(row);
|
||||
});
|
||||
// order by price, it's safe to cast to number price diff should not exceed Number.MAX_SAFE_INTEGER
|
||||
orderbookData.sort((a, b) => Number(BigInt(b.price) - BigInt(a.price)));
|
||||
orderbookData.sort((a, b) => {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
if (BigInt(a.price) > BigInt(b.price)) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
// count cumulative volumes
|
||||
if (orderbookData.length > 1) {
|
||||
const maxIndex = orderbookData.length - 1;
|
||||
@@ -253,28 +282,6 @@ export const updateCompactedRows = (
|
||||
return data;
|
||||
};
|
||||
|
||||
export const mapMarketData = (
|
||||
data: Pick<
|
||||
MarketData,
|
||||
| 'staticMidPrice'
|
||||
| 'bestStaticBidPrice'
|
||||
| 'bestStaticOfferPrice'
|
||||
| 'indicativePrice'
|
||||
> | null,
|
||||
resolution: number
|
||||
) => ({
|
||||
staticMidPrice:
|
||||
data?.staticMidPrice && getPriceLevel(data?.staticMidPrice, resolution),
|
||||
bestStaticBidPrice:
|
||||
data?.bestStaticBidPrice &&
|
||||
getPriceLevel(data?.bestStaticBidPrice, resolution),
|
||||
bestStaticOfferPrice:
|
||||
data?.bestStaticOfferPrice &&
|
||||
getPriceLevel(data?.bestStaticOfferPrice, resolution),
|
||||
indicativePrice:
|
||||
data?.indicativePrice && getPriceLevel(data?.indicativePrice, resolution),
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates raw data with new data received from subscription - mutates input
|
||||
* @param levels
|
||||
@@ -283,7 +290,8 @@ export const mapMarketData = (
|
||||
*/
|
||||
export const updateLevels = (
|
||||
draft: PriceLevelFieldsFragment[],
|
||||
updates: (PriceLevelFieldsFragment | PriceLevelFieldsFragment)[]
|
||||
updates: (PriceLevelFieldsFragment | PriceLevelFieldsFragment)[],
|
||||
ascending = true
|
||||
) => {
|
||||
const levels = [...draft];
|
||||
updates.forEach((update) => {
|
||||
@@ -295,8 +303,10 @@ export const updateLevels = (
|
||||
levels[index] = update;
|
||||
}
|
||||
} else if (update.volume !== '0') {
|
||||
index = levels.findIndex(
|
||||
(level) => BigInt(level.price) > BigInt(update.price)
|
||||
index = levels.findIndex((level) =>
|
||||
ascending
|
||||
? BigInt(level.price) > BigInt(update.price)
|
||||
: BigInt(level.price) < BigInt(update.price)
|
||||
);
|
||||
if (index !== -1) {
|
||||
levels.splice(index, 0, update);
|
||||
@@ -346,22 +356,20 @@ export const generateMockData = ({
|
||||
numberOfOrders: '',
|
||||
}));
|
||||
const rows = compactRows(sell, buy, resolution);
|
||||
const marketTradingMode =
|
||||
overlap > 0
|
||||
? Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION
|
||||
: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS;
|
||||
return {
|
||||
rows,
|
||||
resolution,
|
||||
indicativeVolume: indicativeVolume?.toString(),
|
||||
marketTradingMode:
|
||||
overlap > 0
|
||||
? Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION
|
||||
: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
...mapMarketData(
|
||||
{
|
||||
staticMidPrice: '',
|
||||
bestStaticBidPrice: bestStaticBidPrice.toString(),
|
||||
bestStaticOfferPrice: bestStaticOfferPrice.toString(),
|
||||
indicativePrice: indicativePrice?.toString() ?? '',
|
||||
},
|
||||
resolution
|
||||
),
|
||||
marketTradingMode,
|
||||
midPrice: ((bestStaticBidPrice + bestStaticOfferPrice) / 2).toString(),
|
||||
bestStaticBidPrice: bestStaticBidPrice.toString(),
|
||||
bestStaticOfferPrice: bestStaticOfferPrice.toString(),
|
||||
indicativePrice: indicativePrice
|
||||
? getPriceLevel(indicativePrice.toString(), resolution)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { Orderbook } from './orderbook';
|
||||
@@ -8,12 +9,14 @@ import type { MarketData } from '@vegaprotocol/market-list';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type {
|
||||
MarketDepthUpdateSubscription,
|
||||
MarketDepthQuery,
|
||||
PriceLevelFieldsFragment,
|
||||
} from './__generated__/MarketDepth';
|
||||
import {
|
||||
compactRows,
|
||||
updateCompactedRows,
|
||||
mapMarketData,
|
||||
getMidPrice,
|
||||
getPriceLevel,
|
||||
} from './orderbook-data';
|
||||
import type { OrderbookData } from './orderbook-data';
|
||||
import { usePersistedOrderStore } from '@vegaprotocol/orders';
|
||||
@@ -31,6 +34,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
});
|
||||
const dataRef = useRef<OrderbookData>({ rows: null });
|
||||
const marketDataRef = useRef<MarketData | null>(null);
|
||||
const rawDataRef = useRef<MarketDepthQuery['market'] | null>(null);
|
||||
const deltaRef = useRef<{
|
||||
sell: PriceLevelFieldsFragment[];
|
||||
buy: PriceLevelFieldsFragment[];
|
||||
@@ -42,7 +46,17 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
throttle(() => {
|
||||
dataRef.current = {
|
||||
...marketDataRef.current,
|
||||
...mapMarketData(marketDataRef.current, resolutionRef.current),
|
||||
indicativePrice: marketDataRef.current?.indicativePrice
|
||||
? getPriceLevel(
|
||||
marketDataRef.current.indicativePrice,
|
||||
resolutionRef.current
|
||||
)
|
||||
: undefined,
|
||||
midPrice: getMidPrice(
|
||||
rawDataRef.current?.depth.sell,
|
||||
rawDataRef.current?.depth.buy,
|
||||
resolution
|
||||
),
|
||||
rows:
|
||||
deltaRef.current.buy.length || deltaRef.current.sell.length
|
||||
? updateCompactedRows(
|
||||
@@ -56,14 +70,16 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
deltaRef.current.buy = [];
|
||||
deltaRef.current.sell = [];
|
||||
setOrderbookData(dataRef.current);
|
||||
}, 1000)
|
||||
}, 250)
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
({
|
||||
delta: deltas,
|
||||
data: rawData,
|
||||
}: {
|
||||
delta?: MarketDepthUpdateSubscription['marketsDepthUpdate'];
|
||||
data?: MarketDepthQuery['market'];
|
||||
}) => {
|
||||
if (!dataRef.current.rows) {
|
||||
return false;
|
||||
@@ -78,6 +94,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
if (delta.buy) {
|
||||
deltaRef.current.buy.push(...delta.buy);
|
||||
}
|
||||
rawDataRef.current = rawData;
|
||||
updateOrderbookData.current();
|
||||
}
|
||||
return true;
|
||||
@@ -134,9 +151,14 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
}
|
||||
dataRef.current = {
|
||||
...marketDataRef.current,
|
||||
...mapMarketData(marketDataRef.current, resolution),
|
||||
indicativePrice: getPriceLevel(
|
||||
marketDataRef.current.indicativePrice,
|
||||
resolution
|
||||
),
|
||||
midPrice: getMidPrice(data.depth.sell, data.depth.buy, resolution),
|
||||
rows: compactRows(data.depth.sell, data.depth.buy, resolution),
|
||||
};
|
||||
rawDataRef.current = data;
|
||||
setOrderbookData(dataRef.current);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('Orderbook', () => {
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(90 * rowHeight);
|
||||
});
|
||||
|
||||
it('should should keep price it the middle', async () => {
|
||||
it('should keep price it the middle', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
@@ -106,7 +106,7 @@ describe('Orderbook', () => {
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = result.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 92 * rowHeight;
|
||||
scrollElement.scrollTop = 92 * rowHeight + 0.01;
|
||||
fireEvent.scroll(scrollElement);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
@@ -121,10 +121,10 @@ describe('Orderbook', () => {
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 0.01);
|
||||
});
|
||||
|
||||
it('should should get back to mid price on click', async () => {
|
||||
it('should get back to mid price on click', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
@@ -138,15 +138,15 @@ describe('Orderbook', () => {
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = result.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 0;
|
||||
scrollElement.scrollTop = 1;
|
||||
fireEvent.scroll(scrollElement);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(0);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(1);
|
||||
const scrollToMidPriceButton = result.getByTestId('scroll-to-midprice');
|
||||
fireEvent.click(scrollToMidPriceButton);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1);
|
||||
});
|
||||
|
||||
it('should should get back to mid price on resolution change', async () => {
|
||||
it('should get back to mid price on resolution change', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
@@ -160,9 +160,9 @@ describe('Orderbook', () => {
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = result.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 0;
|
||||
scrollElement.scrollTop = 1;
|
||||
fireEvent.scroll(scrollElement);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(0);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(1);
|
||||
const resolutionSelect = result.getByTestId(
|
||||
'resolution'
|
||||
) as HTMLSelectElement;
|
||||
@@ -181,6 +181,6 @@ describe('Orderbook', () => {
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(5 * rowHeight);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(6 * rowHeight);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import styles from './orderbook.module.scss';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
Fragment,
|
||||
} from 'react';
|
||||
import { useEffect, useRef, useState, useCallback, Fragment } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
@@ -21,7 +13,7 @@ import {
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { OrderbookRow } from './orderbook-row';
|
||||
import { createRow, getPriceLevel } from './orderbook-data';
|
||||
import { createRow } from './orderbook-data';
|
||||
import { Checkbox, Icon, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import type { OrderbookData, OrderbookRowData } from './orderbook-data';
|
||||
|
||||
@@ -36,7 +28,7 @@ interface OrderbookProps extends OrderbookData {
|
||||
|
||||
const HorizontalLine = ({ top, testId }: { top: string; testId: string }) => (
|
||||
<div
|
||||
className="absolute border-b border-default inset-x-0"
|
||||
className="absolute border-b border-default inset-x-0 hidden"
|
||||
style={{ top }}
|
||||
data-testid={testId}
|
||||
/>
|
||||
@@ -97,7 +89,12 @@ const getRowsToRender = (
|
||||
};
|
||||
|
||||
// 17px of row height plus 5px gap
|
||||
export const gridGap = 5;
|
||||
export const rowHeight = 22;
|
||||
// top padding to make space for header
|
||||
const headerPadding = 30;
|
||||
// bottom padding to make space for footer
|
||||
const footerPadding = 25;
|
||||
// buffer size in rows
|
||||
const bufferSize = 30;
|
||||
// margin size in px, when reached scrollOffset will be updated
|
||||
@@ -112,30 +109,30 @@ const getBestStaticBidPriceLinePosition = (
|
||||
rows: OrderbookRowData[] | null
|
||||
) => {
|
||||
let bestStaticBidPriceLinePosition = '';
|
||||
if (maxPriceLevel !== '0' && minPriceLevel !== '0') {
|
||||
if (
|
||||
bestStaticBidPrice &&
|
||||
BigInt(bestStaticBidPrice) < BigInt(maxPriceLevel) &&
|
||||
BigInt(bestStaticBidPrice) > BigInt(minPriceLevel)
|
||||
) {
|
||||
if (fillGaps) {
|
||||
if (
|
||||
rows?.length &&
|
||||
bestStaticBidPrice &&
|
||||
BigInt(bestStaticBidPrice) < BigInt(maxPriceLevel) &&
|
||||
BigInt(bestStaticBidPrice) > BigInt(minPriceLevel)
|
||||
) {
|
||||
if (fillGaps) {
|
||||
bestStaticBidPriceLinePosition = (
|
||||
((BigInt(maxPriceLevel) - BigInt(bestStaticBidPrice)) /
|
||||
BigInt(resolution)) *
|
||||
BigInt(rowHeight) +
|
||||
BigInt(headerPadding) -
|
||||
BigInt(3)
|
||||
).toString();
|
||||
} else {
|
||||
const index = rows?.findIndex(
|
||||
(row) => BigInt(row.price) <= BigInt(bestStaticBidPrice)
|
||||
);
|
||||
if (index !== undefined && index !== -1) {
|
||||
bestStaticBidPriceLinePosition = (
|
||||
((BigInt(maxPriceLevel) - BigInt(bestStaticBidPrice)) /
|
||||
BigInt(resolution) +
|
||||
BigInt(1)) *
|
||||
BigInt(rowHeight) +
|
||||
BigInt(1)
|
||||
index * rowHeight +
|
||||
headerPadding -
|
||||
3
|
||||
).toString();
|
||||
} else {
|
||||
const index = rows?.findIndex(
|
||||
(row) => BigInt(row.price) <= BigInt(bestStaticBidPrice)
|
||||
);
|
||||
if (index !== undefined && index !== -1) {
|
||||
bestStaticBidPriceLinePosition = (
|
||||
(index + 1) * rowHeight +
|
||||
1
|
||||
).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,6 +148,7 @@ const getBestStaticOfferPriceLinePosition = (
|
||||
) => {
|
||||
let bestStaticOfferPriceLinePosition = '';
|
||||
if (
|
||||
rows?.length &&
|
||||
bestStaticOfferPrice &&
|
||||
BigInt(bestStaticOfferPrice) <= BigInt(maxPriceLevel) &&
|
||||
BigInt(bestStaticOfferPrice) > BigInt(minPriceLevel)
|
||||
@@ -159,9 +157,10 @@ const getBestStaticOfferPriceLinePosition = (
|
||||
bestStaticOfferPriceLinePosition = (
|
||||
((BigInt(maxPriceLevel) - BigInt(bestStaticOfferPrice)) /
|
||||
BigInt(resolution) +
|
||||
BigInt(2)) *
|
||||
BigInt(1)) *
|
||||
BigInt(rowHeight) +
|
||||
BigInt(1)
|
||||
BigInt(headerPadding) -
|
||||
BigInt(3)
|
||||
).toString();
|
||||
} else {
|
||||
const index = rows?.findIndex(
|
||||
@@ -169,8 +168,9 @@ const getBestStaticOfferPriceLinePosition = (
|
||||
);
|
||||
if (index !== undefined && index !== -1) {
|
||||
bestStaticOfferPriceLinePosition = (
|
||||
(index + 2) * rowHeight +
|
||||
1
|
||||
(index + 1) * rowHeight +
|
||||
headerPadding -
|
||||
3
|
||||
).toString();
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ const OrderbookDebugInfo = ({
|
||||
bestStaticOfferPrice,
|
||||
maxPriceLevel,
|
||||
minPriceLevel,
|
||||
resolution,
|
||||
midPrice,
|
||||
}: {
|
||||
decimalPlaces: number;
|
||||
numberOfRows: number;
|
||||
@@ -198,7 +198,7 @@ const OrderbookDebugInfo = ({
|
||||
bestStaticOfferPrice?: string;
|
||||
maxPriceLevel: string;
|
||||
minPriceLevel: string;
|
||||
resolution: number;
|
||||
midPrice?: string;
|
||||
}) => (
|
||||
<Fragment>
|
||||
<div
|
||||
@@ -247,16 +247,7 @@ const OrderbookDebugInfo = ({
|
||||
decimalPlaces
|
||||
),
|
||||
midPrice: addDecimalsFixedFormatNumber(
|
||||
(bestStaticOfferPrice &&
|
||||
bestStaticBidPrice &&
|
||||
getPriceLevel(
|
||||
BigInt(bestStaticOfferPrice) +
|
||||
(BigInt(bestStaticBidPrice) -
|
||||
BigInt(bestStaticOfferPrice)) /
|
||||
BigInt(2),
|
||||
resolution
|
||||
)) ??
|
||||
'0',
|
||||
midPrice ?? '0',
|
||||
decimalPlaces
|
||||
),
|
||||
},
|
||||
@@ -270,6 +261,7 @@ const OrderbookDebugInfo = ({
|
||||
|
||||
export const Orderbook = ({
|
||||
rows,
|
||||
midPrice,
|
||||
bestStaticBidPrice,
|
||||
bestStaticOfferPrice,
|
||||
marketTradingMode,
|
||||
@@ -295,21 +287,36 @@ export const Orderbook = ({
|
||||
// price level which is rendered in center of viewport, need to preserve price level when rows will be added or removed
|
||||
// if undefined then we render mid price in center
|
||||
const priceInCenter = useRef<string>();
|
||||
// by default mid price is rendered in center - view locked on mid price
|
||||
const [lockOnMidPrice, setLockOnMidPrice] = useState(true);
|
||||
const resolutionRef = useRef(resolution);
|
||||
const [viewportHeight, setViewportHeight] = useState(window.innerHeight);
|
||||
// show price levels with no orders, can lead to enormous number of rows
|
||||
const [fillGaps, setFillGaps] = useState(!!initialFillGaps);
|
||||
const numberOfRows = useMemo(
|
||||
() => (fillGaps ? getNumberOfRows(rows, resolution) : rows?.length ?? 0),
|
||||
[rows, resolution, fillGaps]
|
||||
);
|
||||
const maxPriceLevel = rows?.[0]?.price ?? '0';
|
||||
const minPriceLevel = (
|
||||
fillGaps
|
||||
? BigInt(maxPriceLevel) - BigInt(Math.floor(numberOfRows * resolution))
|
||||
: BigInt(rows?.[rows.length - 1]?.price ?? '0')
|
||||
).toString();
|
||||
const [debug, setDebug] = useState(false);
|
||||
|
||||
const numberOfRows = fillGaps
|
||||
? getNumberOfRows(rows, resolution)
|
||||
: rows?.length ?? 0;
|
||||
const maxPriceLevel = rows?.[0]?.price ?? '0';
|
||||
const minPriceLevel = rows?.[rows.length - 1]?.price ?? '0';
|
||||
|
||||
let offset = Math.max(0, Math.round(scrollOffset / rowHeight));
|
||||
const prependingBufferSize = Math.min(bufferSize, offset);
|
||||
offset -= prependingBufferSize;
|
||||
const viewportSize = Math.round(viewportHeight / rowHeight);
|
||||
const limit = Math.min(
|
||||
prependingBufferSize + viewportSize + bufferSize,
|
||||
numberOfRows - offset
|
||||
);
|
||||
const data = fillGaps
|
||||
? getRowsToRender(rows, resolution, offset, limit)
|
||||
: rows?.slice(offset, offset + limit) ?? [];
|
||||
|
||||
const paddingTop = offset * rowHeight + headerPadding;
|
||||
const paddingBottom =
|
||||
(numberOfRows - offset - limit) * rowHeight + footerPadding;
|
||||
|
||||
const updateScrollOffset = useCallback(
|
||||
(scrollTop: number) => {
|
||||
if (Math.abs(scrollOffset - scrollTop) > marginSize) {
|
||||
@@ -318,23 +325,35 @@ export const Orderbook = ({
|
||||
},
|
||||
[scrollOffset]
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
(event: React.UIEvent<HTMLDivElement>) => {
|
||||
const { scrollTop } = event.currentTarget;
|
||||
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
|
||||
updateScrollOffset(scrollTop);
|
||||
if (scrollTop === scrollTopRef.current) {
|
||||
return;
|
||||
} else if ((scrollTop - scrollTopRef.current) % rowHeight === 0) {
|
||||
if (scrollElement.current) {
|
||||
scrollElement.current.scrollTop = scrollTopRef.current;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (scrollTop === 0 || scrollHeight === clientHeight + scrollTop) {
|
||||
priceInCenter.current = undefined;
|
||||
} else {
|
||||
// top offset in rows to row in the middle
|
||||
const offsetTop = Math.floor(
|
||||
(scrollTop +
|
||||
Math.floor((viewportHeight - footerPadding - headerPadding) / 2)) /
|
||||
rowHeight
|
||||
);
|
||||
priceInCenter.current = fillGaps
|
||||
? (
|
||||
BigInt(maxPriceLevel) -
|
||||
BigInt(offsetTop) * BigInt(resolution)
|
||||
).toString()
|
||||
: rows?.[Math.min(offsetTop, rows.length - 1)].price.toString();
|
||||
}
|
||||
const offsetTop = Math.floor(
|
||||
(scrollTop + Math.floor(viewportHeight / 2)) / rowHeight
|
||||
);
|
||||
priceInCenter.current = fillGaps
|
||||
? (
|
||||
BigInt(resolution) + // extra row on very top - sticky header
|
||||
BigInt(maxPriceLevel) -
|
||||
BigInt(offsetTop) * BigInt(resolution)
|
||||
).toString()
|
||||
: rows?.[Math.min(offsetTop, rows.length - 1)]?.price?.toString();
|
||||
if (lockOnMidPrice) {
|
||||
setLockOnMidPrice(false);
|
||||
}
|
||||
@@ -361,24 +380,22 @@ export const Orderbook = ({
|
||||
(Number(
|
||||
(BigInt(maxPriceLevel) - BigInt(price)) / BigInt(resolution)
|
||||
) +
|
||||
1) * // add one row for sticky header
|
||||
rowHeight +
|
||||
rowHeight / 2 -
|
||||
(viewportHeight % rowHeight);
|
||||
1) *
|
||||
rowHeight;
|
||||
} else if (rows) {
|
||||
const index = rows.findIndex(
|
||||
(row) => BigInt(row.price) <= BigInt(price)
|
||||
);
|
||||
if (index !== -1) {
|
||||
scrollTop =
|
||||
index * rowHeight + rowHeight / 2 - (viewportHeight % rowHeight);
|
||||
if (
|
||||
price === rows[index].price ||
|
||||
index === 0 ||
|
||||
BigInt(rows[index].price) - BigInt(price) <
|
||||
BigInt(price) - BigInt(rows[index - 1].price)
|
||||
) {
|
||||
scrollTop += rowHeight;
|
||||
scrollTop = rowHeight * (index + 1);
|
||||
if (index !== 0) {
|
||||
const diffToCurrentRow =
|
||||
BigInt(price) - BigInt(rows[index].price);
|
||||
const diffToPreviousRow =
|
||||
BigInt(rows[index - 1].price) - BigInt(price);
|
||||
if (diffToPreviousRow < diffToCurrentRow) {
|
||||
scrollTop -= rowHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,7 +406,13 @@ export const Orderbook = ({
|
||||
(scrollTopRef.current % rowHeight) - (scrollTop % rowHeight);
|
||||
const priceCenterScrollOffset = Math.max(
|
||||
0,
|
||||
Math.min(scrollTop, numberOfRows * rowHeight - viewportHeight)
|
||||
Math.min(
|
||||
scrollTop,
|
||||
numberOfRows * rowHeight +
|
||||
headerPadding +
|
||||
footerPadding +
|
||||
-viewportHeight
|
||||
)
|
||||
);
|
||||
if (scrollTopRef.current !== priceCenterScrollOffset) {
|
||||
updateScrollOffset(priceCenterScrollOffset);
|
||||
@@ -410,72 +433,31 @@ export const Orderbook = ({
|
||||
);
|
||||
|
||||
const scrollToMidPrice = useCallback(() => {
|
||||
if (!bestStaticOfferPrice || !bestStaticBidPrice) {
|
||||
if (!midPrice) {
|
||||
return;
|
||||
}
|
||||
priceInCenter.current = undefined;
|
||||
let midPrice = getPriceLevel(
|
||||
BigInt(bestStaticOfferPrice) +
|
||||
(BigInt(bestStaticBidPrice) - BigInt(bestStaticOfferPrice)) / BigInt(2),
|
||||
resolution
|
||||
);
|
||||
if (BigInt(midPrice) > BigInt(maxPriceLevel)) {
|
||||
midPrice = maxPriceLevel;
|
||||
} else {
|
||||
if (BigInt(midPrice) < BigInt(minPriceLevel)) {
|
||||
midPrice = minPriceLevel.toString();
|
||||
}
|
||||
}
|
||||
scrollToPrice(midPrice);
|
||||
setLockOnMidPrice(true);
|
||||
}, [
|
||||
bestStaticOfferPrice,
|
||||
bestStaticBidPrice,
|
||||
scrollToPrice,
|
||||
resolution,
|
||||
maxPriceLevel,
|
||||
minPriceLevel,
|
||||
]);
|
||||
}, [midPrice, scrollToPrice]);
|
||||
|
||||
// adjust scroll position to keep selected price in center
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
if (priceInCenter.current) {
|
||||
scrollToPrice(priceInCenter.current);
|
||||
} else if (lockOnMidPrice && midPrice) {
|
||||
scrollToPrice(midPrice);
|
||||
}
|
||||
}, [midPrice, scrollToPrice, lockOnMidPrice]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resolutionRef.current !== resolution) {
|
||||
priceInCenter.current = undefined;
|
||||
resolutionRef.current = resolution;
|
||||
setLockOnMidPrice(true);
|
||||
}
|
||||
if (priceInCenter.current) {
|
||||
scrollToPrice(priceInCenter.current);
|
||||
} else {
|
||||
scrollToMidPrice();
|
||||
}
|
||||
}, [scrollToMidPrice, scrollToPrice, resolution]);
|
||||
}, [resolution]);
|
||||
|
||||
// handles window resize
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
if (rootElement.current) {
|
||||
setViewportHeight(
|
||||
rootElement.current.clientHeight || window.innerHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
// sets the correct width of header and footer
|
||||
useLayoutEffect(() => {
|
||||
if (
|
||||
!gridElement.current ||
|
||||
!headerElement.current ||
|
||||
!footerElement.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const gridWidth = gridElement.current.clientWidth;
|
||||
headerElement.current.style.width = `${gridWidth}px`;
|
||||
footerElement.current.style.width = `${gridWidth}px`;
|
||||
}, [headerElement, footerElement, gridElement]);
|
||||
// handles resizing of the Allotment.Pane (x-axis)
|
||||
// adjusts the header and footer width
|
||||
const gridResizeHandler: ResizeObserverCallback = useCallback(
|
||||
@@ -509,20 +491,6 @@ export const Orderbook = ({
|
||||
useResizeObserver(gridElement.current, gridResizeHandler);
|
||||
useResizeObserver(rootElement.current, rootElementResizeHandler);
|
||||
|
||||
let offset = Math.max(0, Math.round(scrollOffset / rowHeight));
|
||||
const prependingBufferSize = Math.min(bufferSize, offset);
|
||||
offset -= prependingBufferSize;
|
||||
const viewportSize = Math.round(viewportHeight / rowHeight);
|
||||
const limit = Math.min(
|
||||
prependingBufferSize + viewportSize + bufferSize,
|
||||
numberOfRows - offset
|
||||
);
|
||||
const data = fillGaps
|
||||
? getRowsToRender(rows, resolution, offset, limit)
|
||||
: rows?.slice(offset, offset + limit) ?? [];
|
||||
|
||||
const paddingTop = offset * rowHeight;
|
||||
const paddingBottom = (numberOfRows - offset - limit) * rowHeight;
|
||||
const tableBody =
|
||||
data && data.length !== 0 ? (
|
||||
<div
|
||||
@@ -603,16 +571,16 @@ export const Orderbook = ({
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`h-full overflow-auto relative ${styles['scroll']} pt-[26px] pb-[17px]`}
|
||||
className={`h-full overflow-auto relative ${styles['scroll']}`}
|
||||
onScroll={onScroll}
|
||||
ref={scrollElement}
|
||||
data-testid="scroll"
|
||||
>
|
||||
<div
|
||||
className="relative text-right min-h-full"
|
||||
className="relative text-right min-h-full overflow-hidden"
|
||||
style={{
|
||||
paddingTop: paddingTop,
|
||||
paddingBottom: paddingBottom,
|
||||
paddingTop,
|
||||
paddingBottom,
|
||||
background: tableBody ? gradientStyles : 'none',
|
||||
}}
|
||||
ref={gridElement}
|
||||
@@ -685,7 +653,7 @@ export const Orderbook = ({
|
||||
{debug && (
|
||||
<OrderbookDebugInfo
|
||||
decimalPlaces={decimalPlaces}
|
||||
resolution={resolution}
|
||||
midPrice={midPrice}
|
||||
numberOfRows={numberOfRows}
|
||||
viewportHeight={viewportHeight}
|
||||
lockOnMidPrice={lockOnMidPrice}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"executor": "@nrwl/web:rollup",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"buildableProjectDepsInPackageJsonType": "dependencies",
|
||||
"outputPath": "dist/libs/react-helpers",
|
||||
"tsConfig": "libs/react-helpers/tsconfig.lib.json",
|
||||
"project": "libs/react-helpers/package.json",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { getSecondsFromInterval } from './time';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -8,3 +8,41 @@ export const fromNanoSeconds = (ts: string) => {
|
||||
const val = parseISO(ts);
|
||||
return new Date(isValid(val) ? val : 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the interval string we get for the epoch length from the
|
||||
* network parameter API. These are in the format '1D2H3m' for 1 day,
|
||||
* 2 hours and 3 minutes.
|
||||
*
|
||||
* @param str Interval string
|
||||
* @returns integer the number of seconds the interval represents
|
||||
*/
|
||||
export function getSecondsFromInterval(str: string) {
|
||||
let seconds = 0;
|
||||
|
||||
if (!str || !str.match) {
|
||||
return seconds;
|
||||
}
|
||||
|
||||
const months = str.match(/(\d+)\s*M/);
|
||||
const days = str.match(/(\d+)\s*D/);
|
||||
const hours = str.match(/(\d+)\s*h/);
|
||||
const minutes = str.match(/(\d+)\s*m/);
|
||||
const secs = str.match(/(\d+)\s*s/);
|
||||
if (months) {
|
||||
seconds += parseInt(months[1]) * 86400 * 30;
|
||||
}
|
||||
if (days) {
|
||||
seconds += parseInt(days[1]) * 86400;
|
||||
}
|
||||
if (hours) {
|
||||
seconds += parseInt(hours[1]) * 3600;
|
||||
}
|
||||
if (minutes) {
|
||||
seconds += parseInt(minutes[1]) * 60;
|
||||
}
|
||||
if (secs) {
|
||||
seconds += parseInt(secs[1]);
|
||||
}
|
||||
return seconds;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"outputPath": "dist/libs/tailwindcss-config",
|
||||
"main": "libs/tailwindcss-config/src/index.js",
|
||||
"tsConfig": "libs/tailwindcss-config/tsconfig.lib.json",
|
||||
"assets": ["libs/tailwindcss-config/*.md"]
|
||||
"assets": ["libs/tailwindcss-config/*.md"],
|
||||
"buildableProjectDepsInPackageJsonType": "dependencies"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"input": ".",
|
||||
"output": "."
|
||||
}
|
||||
]
|
||||
],
|
||||
"buildableProjectDepsInPackageJsonType": "dependencies"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
ValidatorStatus,
|
||||
VoteValue,
|
||||
WithdrawalStatus,
|
||||
DispatchMetric,
|
||||
} from './__generated__/types';
|
||||
|
||||
export const AccountTypeMapping: {
|
||||
@@ -434,3 +435,13 @@ export const DescriptionTransferTypeMapping: TransferTypeMap = {
|
||||
TRANSFER_TYPE_UNSPECIFIED: 'Default value, always invalid',
|
||||
TRANSFER_TYPE_CHECKPOINT_BALANCE_RESTORE: `Balances are being restored to the user's account following a checkpoint restart of the network`,
|
||||
};
|
||||
|
||||
type DispatchMetricLabel = {
|
||||
[T in DispatchMetric]: string;
|
||||
};
|
||||
export const DispatchMetricLabels: DispatchMetricLabel = {
|
||||
DISPATCH_METRIC_LP_FEES_RECEIVED: 'Liquidity Provision fees received',
|
||||
DISPATCH_METRIC_MAKER_FEES_PAID: 'Price maker fees paid',
|
||||
DISPATCH_METRIC_MAKER_FEES_RECEIVED: 'Price maker fees earned',
|
||||
DISPATCH_METRIC_MARKET_VALUE: 'Total market Value',
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"executor": "@nrwl/web:rollup",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"buildableProjectDepsInPackageJsonType": "dependencies",
|
||||
"outputPath": "dist/libs/ui-toolkit",
|
||||
"tsConfig": "libs/ui-toolkit/tsconfig.lib.json",
|
||||
"project": "libs/ui-toolkit/package.json",
|
||||
|
||||
Reference in New Issue
Block a user