Compare commits

..
94 changed files with 1971 additions and 4295 deletions
+2 -4
View File
@@ -18,11 +18,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
- uses: actions/setup-node@v3
with:
node-version: 16.15.1
node-version: 16
- name: Run Cypress tests
uses: cypress-io/github-action@v4
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
node-version: 16.14.0
- name: Install root dependencies
run: yarn install
- name: Generate queries
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
node-version: 16.14.0
- name: Install root dependencies
run: yarn install
- name: Check PR title
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
node-version: 16.14.0
- name: Install root dependencies
run: yarn install
- name: Generate queries
-47
View File
@@ -1,47 +0,0 @@
name: Publish libs to npm
on:
workflow_dispatch:
inputs:
project:
description: 'Monorepo project to publish'
required: true
type: choice
options:
- ui-toolkit
- react-helpers
- tailwindcss-config
- types
jobs:
publish:
name: Build & Publish - Tag
runs-on: ubuntu-latest
permissions:
contents: 'read'
actions: 'read'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: User Node.js 16
id: Node
uses: actions/setup-node@v3
with:
node-version: 16.15.1
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: node_modules-${{ hashFiles('**/yarn.lock') }}
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Build project
run: yarn nx build ${{inputs.project}}
- name: Publish project to @vegaprotocol
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
package: dist/libs/${{inputs.project}}/package.json
access: public
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
id: Node
uses: actions/setup-node@v3
with:
node-version: 16.15.1
node-version: 16.14.0
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
+1 -1
View File
@@ -1 +1 @@
16.15.1
16.14.0
@@ -1,50 +0,0 @@
query ExplorerEpoch($id: ID!) {
epoch(id: $id) {
id
timestamps {
start
end
firstBlock
lastBlock
}
}
}
query ExplorerFutureEpoch {
networkParameter(key: "validators.epoch.length") {
value
}
epoch {
id
timestamps {
start
}
}
}
# query ExplorerEpoch($id: ID!) {
#
##### This could be useful for calculating roughly when a future epoch will
##### occur, but epoch not exist results in a total error
# networkParameter(key: "validators.epoch.length") {
# value
# }
#
##### This could be useful for relating where we are in time, but as above
##### the total failure caused by epoch(id) not existing
##### means this is useful
# currentEpoch: epoch {
# id
# }
#
# epoch(id: $id) {
# id
# timestamps {
# start
# end
# firstBlock
# lastBlock
# }
# }
#}
@@ -1,99 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerEpochQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, firstBlock: string, lastBlock?: string | null } } };
export type ExplorerFutureEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerFutureEpochQuery = { __typename?: 'Query', networkParameter?: { __typename?: 'NetworkParameter', value: string } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null } } };
export const ExplorerEpochDocument = gql`
query ExplorerEpoch($id: ID!) {
epoch(id: $id) {
id
timestamps {
start
end
firstBlock
lastBlock
}
}
}
`;
/**
* __useExplorerEpochQuery__
*
* To run a query within a React component, call `useExplorerEpochQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerEpochQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerEpochQuery(baseOptions: Apollo.QueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
}
export function useExplorerEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
}
export type ExplorerEpochQueryHookResult = ReturnType<typeof useExplorerEpochQuery>;
export type ExplorerEpochLazyQueryHookResult = ReturnType<typeof useExplorerEpochLazyQuery>;
export type ExplorerEpochQueryResult = Apollo.QueryResult<ExplorerEpochQuery, ExplorerEpochQueryVariables>;
export const ExplorerFutureEpochDocument = gql`
query ExplorerFutureEpoch {
networkParameter(key: "validators.epoch.length") {
value
}
epoch {
id
timestamps {
start
}
}
}
`;
/**
* __useExplorerFutureEpochQuery__
*
* To run a query within a React component, call `useExplorerFutureEpochQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerFutureEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerFutureEpochQuery({
* variables: {
* },
* });
*/
export function useExplorerFutureEpochQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
}
export function useExplorerFutureEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
}
export type ExplorerFutureEpochQueryHookResult = ReturnType<typeof useExplorerFutureEpochQuery>;
export type ExplorerFutureEpochLazyQueryHookResult = ReturnType<typeof useExplorerFutureEpochLazyQuery>;
export type ExplorerFutureEpochQueryResult = Apollo.QueryResult<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>;
@@ -1,56 +0,0 @@
import { render } from '@testing-library/react';
import { IconForEpoch } from './epoch';
const THE_PAST = 'Monday, 17 February 2022 11:44:09';
const THE_FUTURE = 'Monday, 17 February 3023 11:44:09';
describe('IconForEpoch', () => {
it('Handles malformed dates', () => {
const start = 'This is n0t a d4te';
const end = '📅';
const screen = render(<IconForEpoch start={start} end={end} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('defaults to a calendar icon', () => {
const start = null as unknown as string;
const end = null as unknown as string;
const screen = render(<IconForEpoch start={start} end={end} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('if start and end are both in the future, stick with calendar', () => {
const screen = render(<IconForEpoch start={THE_FUTURE} end={THE_FUTURE} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('if start is in the past and end is in the future, this is currently active', () => {
const screen = render(<IconForEpoch start={THE_PAST} end={THE_FUTURE} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'circle icon'
);
});
it('if start and end are in the paste, this is done', () => {
const screen = render(<IconForEpoch start={THE_PAST} end={THE_PAST} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'tick-circle icon'
);
});
});
@@ -1,125 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import EpochMissingOverview, { calculateEpochData } from './epoch-missing';
import { getSecondsFromInterval } from '@vegaprotocol/react-helpers';
const START_DATE_PAST = 'Monday, 17 February 2022 11:44:09';
describe('getSecondsFromInterval', () => {
it('returns 0 for bad data', () => {
expect(getSecondsFromInterval(null as unknown as string)).toEqual(0);
expect(getSecondsFromInterval('')).toEqual(0);
expect(getSecondsFromInterval('🧙')).toEqual(0);
expect(getSecondsFromInterval(2 as unknown as string)).toEqual(0);
});
it('parses out months from a capital M', () => {
expect(getSecondsFromInterval('2M')).toEqual(5184000);
});
it('parses out days from a capital D', () => {
expect(getSecondsFromInterval('1D')).toEqual(86400);
});
it('parses out hours from a lower case h', () => {
expect(getSecondsFromInterval('11h')).toEqual(39600);
});
it('parses out minutes from a lower case m', () => {
expect(getSecondsFromInterval('10m')).toEqual(600);
});
it('parses out seconds from a lower case s', () => {
expect(getSecondsFromInterval('99s')).toEqual(99);
});
it('parses complex examples', () => {
expect(getSecondsFromInterval('24h')).toEqual(86400);
expect(getSecondsFromInterval('1h30m')).toEqual(5400);
expect(getSecondsFromInterval('1D1h30m1s')).toEqual(91801);
});
});
describe('calculateEpochData', () => {
it('Handles bad data', () => {
const currentEpochId = null as unknown as string;
const missingEpochId = null as unknown as string;
const epochStart = null as unknown as string;
const epochLength = null as unknown as string;
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
expect(res).toHaveProperty('label', 'Missing data');
expect(res).toHaveProperty('isInFuture', false);
});
it('Calculates that a bigger epoch number is in the future from basic data', () => {
const currentEpochId = '10';
const missingEpochId = '20';
const epochStart = '';
const epochLength = '';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
expect(res).toHaveProperty('isInFuture', true);
});
it('If it has an epoch length and a start time, it provides an estimate', () => {
const currentEpochId = '10';
const missingEpochId = '20';
const epochStart = START_DATE_PAST;
const epochLength = '1s';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
expect(res).toHaveProperty('label');
expect(res.label).toMatch(/^Estimate/);
expect(res.label).toMatch(/in less than a minute$/);
});
it('Provide decent string for past', () => {
const currentEpochId = '20';
const missingEpochId = '10';
const epochStart = START_DATE_PAST;
const epochLength = '1s';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
expect(res).toHaveProperty('label');
expect(res.label).toMatch(/^Estimate/);
expect(res.label).toMatch(/less than a minute ago$/);
});
});
describe('EpochMissingOverview', () => {
function renderComponent(missingEpochId: string) {
return render(
<MockedProvider>
<EpochMissingOverview missingEpochId={missingEpochId} />
</MockedProvider>
);
}
it('renders a - if no id is provided', () => {
const n = null as unknown as string;
const screen = renderComponent(n);
expect(screen.getByTestId('empty')).toBeInTheDocument();
});
});
@@ -1,111 +0,0 @@
import { useExplorerFutureEpochQuery } from './__generated__/Epoch';
import addSeconds from 'date-fns/addSeconds';
import formatDistance from 'date-fns/formatDistance';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import isFuture from 'date-fns/isFuture';
import { isValidDate } from '@vegaprotocol/react-helpers';
import { getSecondsFromInterval } from '@vegaprotocol/react-helpers';
export type EpochMissingOverviewProps = {
missingEpochId?: string;
};
/**
* Renders a set of details for an epoch that has no representation in the
* data node. This is primarily for one of two reasons:
*
* 1. The epoch hasn't happened yet
* 2. The epoch happened before a snapshot, and thus the details don't exist
*
* This component is used when the API has responded with no data for an epoch
* by ID, so we already know that we can't display start time/block etc.
*
* We can detect 1 if the epoch is a higher number than the current epoch
* We can detect 2 if the epoch is in the past, but we still get no response.
*/
const EpochMissingOverview = ({
missingEpochId,
}: EpochMissingOverviewProps) => {
const { data, error, loading } = useExplorerFutureEpochQuery();
// This should not happen, but it's easily handled
if (!missingEpochId) {
return <span data-testid="empty">-</span>;
}
// No data should also not happen - we've requested the current epoch. This
// could happen at chain restart, but shouldn't. If it does, fallback.
if (!data || loading || error) {
return <span data-testid="empty">{missingEpochId}</span>;
}
// If we have enough information to predict a future or past block time, let's do it
if (
!missingEpochId ||
!data.epoch.id ||
!data.epoch.timestamps.start ||
!data?.networkParameter?.value
) {
return <span data-testid="empty">{missingEpochId}</span>;
}
const { label, isInFuture } = calculateEpochData(
data.epoch.id,
missingEpochId,
data.epoch.timestamps.start,
data.networkParameter.value
);
return (
<Tooltip description={<p className="text-xs m-2">{label}</p>}>
<p>
{isInFuture ? (
<Icon name="calendar" className="mr-1" />
) : (
<Icon name="outdated" className="mr-1" />
)}
{missingEpochId}
</p>
</Tooltip>
);
};
export function calculateEpochData(
currentEpochId: string,
missingEpochId: string,
epochStart: string,
epochLength: string
) {
// Blank string will be return 0 seconds from getSecondsFromInterval
const epochLengthInSeconds = getSecondsFromInterval(epochLength);
if (!epochStart || !epochLength) {
// Let's just take a guess
return {
label: 'Missing data',
isInFuture: parseInt(missingEpochId) > parseInt(currentEpochId),
};
}
const startFrom = new Date(epochStart);
const diff = parseInt(missingEpochId) - parseInt(currentEpochId);
const futureDate = addSeconds(startFrom, diff * epochLengthInSeconds);
const label =
isValidDate(futureDate) && isValidDate(startFrom)
? `Estimate: ${futureDate.toLocaleString()} - ${formatDistance(
futureDate,
startFrom,
{ addSuffix: true }
)}`
: 'Missing data';
return {
label,
isInFuture: isFuture(futureDate),
};
}
export default EpochMissingOverview;
@@ -1,127 +0,0 @@
import { useExplorerEpochQuery } from './__generated__/Epoch';
import { t } from '@vegaprotocol/react-helpers';
import { BlockLink } from '../links';
import { Time } from '../time';
import { TimeAgo } from '../time-ago';
import EpochMissingOverview from './epoch-missing';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import isPast from 'date-fns/isPast';
const borderClass =
'border-solid border-2 border-vega-dark-200 border-collapse';
export type EpochOverviewProps = {
id?: string;
};
/**
* Displays detailed information about an epoch, given an ID. This
* works for past epochs and current epochs - future epochs, and a
* few other situations (see epoch-missing) will not return us
* enough information to render this.
*
* The details are hidden in a tooltip, behind the epoch number
*/
const EpochOverview = ({ id }: EpochOverviewProps) => {
const { data, error, loading } = useExplorerEpochQuery({
variables: { id: id || '' },
});
const ti = data?.epoch.timestamps;
if (
error?.message &&
error.message.includes('no resource corresponding to this id')
) {
return <EpochMissingOverview missingEpochId={id} />;
}
if (!ti || loading || error) {
return <span>{id}</span>;
}
const description = (
<table className="text-xs m-2">
<thead>
<tr>
<th></th>
<th className={`text-center ${borderClass}`}>{t('Block')}</th>
<th className={`text-center ${borderClass}`}>{t('Time')}</th>
</tr>
</thead>
<tbody>
<tr>
<th className={`px-2 ${borderClass}`}>{t('Start')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.firstBlock ? <BlockLink height={ti.firstBlock} /> : '-'}
</td>
<td className={`px-2 ${borderClass}`}>
<Time date={ti.start} />
<br />
<TimeAgo date={ti.start} />
</td>
</tr>
<tr>
<th className={`px-2 ${borderClass}`}>{t('End')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.lastBlock ? (
<BlockLink height={ti.lastBlock} />
) : (
t('In progress')
)}
</td>
<td className={`px-2 ${borderClass}`}>
{ti.end ? (
<>
<Time date={ti.end} />
<br />
<TimeAgo date={ti.end} />
</>
) : (
<span>{t('-')}</span>
)}
</td>
</tr>
</tbody>
</table>
);
return (
<Tooltip description={description}>
<p>
<IconForEpoch start={ti.start} end={ti.end} />
{id}
</p>
</Tooltip>
);
};
export type IconForEpochProps = {
start: string;
end: string;
};
/**
* Chooses an icon to display next to the epoch number, representing
* when the epoch is relative to now (i.e. not yet started, started,
* finished)
*/
export function IconForEpoch({ start, end }: IconForEpochProps) {
const startHasPassed = start ? isPast(new Date(start)) : false;
const endHasPassed = end ? isPast(new Date(end)) : false;
let i: IconProps['name'] = 'calendar';
if (!startHasPassed && !endHasPassed) {
i = 'calendar';
} else if (startHasPassed && !endHasPassed) {
i = 'circle';
} else if (startHasPassed && endHasPassed) {
i = 'tick-circle';
}
return <Icon name={i} className="mr-2" />;
}
export default EpochOverview;
@@ -11,14 +11,14 @@ export const InfoBlock = ({ title, subtitle, tooltipInfo }: InfoBlockProps) => {
return (
<div className="flex flex-col text-center ">
<h3 className="text-4xl">{title}</h3>
<p className="text-vega-dark-100 dark:text-vega-light-200">
<p className="text-zinc-800 dark:text-zinc-300">
{subtitle}
{tooltipInfo ? (
<Tooltip description={tooltipInfo} align="center">
<span>
<Icon
name="info-sign"
className="ml-2 text-vega-light-300 dark:text-vega-dark-300"
className="ml-2 text-zinc-400 dark:text-zinc-600"
/>
</span>
</Tooltip>
@@ -44,12 +44,12 @@ export const InfoPanel = ({
text={id}
startChars={visibleChars}
endChars={visibleChars}
className="text-vega-dark-100 dark:text-vega-light-200"
className="text-black dark:text-zinc-200"
/>
) : (
<p
title={id}
className="text-vega-dark-100 dark:text-vega-light-200 truncate ..."
className="text-black dark:text-zinc-200 truncate ..."
>
{id}
</p>
@@ -70,7 +70,7 @@ export const InfoPanel = ({
</div>
{copy && (
<CopyWithTooltip text={id}>
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" />
</button>
</CopyWithTooltip>
@@ -11,7 +11,6 @@ import { Routes } from '../../../routes/route-names';
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
assetId: string;
asDialog?: boolean;
showAssetSymbol?: boolean;
};
/**
@@ -19,22 +18,12 @@ export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
* with a link to the assets modal. If the name does not come back
* it will use the ID instead.
*/
export const AssetLink = ({
assetId,
asDialog,
showAssetSymbol = false,
...props
}: AssetLinkProps) => {
export const AssetLink = ({ assetId, asDialog, ...props }: AssetLinkProps) => {
const { data: asset } = useAssetDataProvider(assetId);
const open = useAssetDetailsDialogStore((state) => state.open);
const navigate = useNavigate();
const label = asset
? showAssetSymbol
? asset?.symbol
: asset?.name
: assetId;
const label = asset?.name ? asset.name : assetId;
return (
<ButtonLink
data-testid="asset-link"
@@ -48,7 +48,7 @@ const MarketLink = ({
<Link
className="underline"
{...props}
to={`/${Routes.MARKETS}/${id}`}
to={`/${Routes.MARKETS}#${id}`}
title={id}
>
{label}
@@ -56,7 +56,7 @@ const MarketLink = ({
);
} else {
return (
<Link className="underline" {...props} to={`/${Routes.MARKETS}/${id}`}>
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
<Hash text={id} />
</Link>
);
@@ -1,39 +0,0 @@
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import PartyLink from './party-link';
describe('PartyLink', () => {
it('renders Network for 000.000 party', () => {
const zeroes =
'0000000000000000000000000000000000000000000000000000000000000000';
const screen = render(<PartyLink id={zeroes} />);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders Network for network party', () => {
const screen = render(<PartyLink id="network" />);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders ID with no link for invalid party', () => {
const screen = render(<PartyLink id="this-party-is-not-valid" />);
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
});
it('links a valid party to the party page', () => {
const aValidParty =
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
const screen = render(
<MemoryRouter>
<PartyLink id={aValidParty} />
</MemoryRouter>
);
const el = screen.getByText(aValidParty);
expect(el).toBeInTheDocument();
// The text should be a link that points to the party's page
expect(el.parentElement?.tagName).toEqual('A');
expect(el.parentElement?.getAttribute('href')).toContain(aValidParty);
});
});
@@ -3,47 +3,19 @@ import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import Hash from '../hash';
import { t } from '@vegaprotocol/react-helpers';
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
export const SPECIAL_CASE_NETWORK_ID =
'0000000000000000000000000000000000000000000000000000000000000000';
export const SPECIAL_CASE_NETWORK = 'network';
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
truncate?: boolean;
};
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
// Some transactions will involve the 'network' party, which is alias for '000...000'
// The party page does not handle this nicely, so in this case we render the word 'Network'
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
return (
<span className="font-mono" data-testid="network">
{t('Network')}
</span>
);
}
// If the party doesn't look correct, there's no point in linking to id. Just render
// the ID as it was given to us
if (!isValidPartyId(id)) {
return (
<span className="font-mono" data-testid="invalid-party">
{id}
</span>
);
}
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
return (
<Link
className="underline font-mono"
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
<Hash text={truncate ? truncateMiddle(id) : id} />
<Hash text={id} />
</Link>
);
};
@@ -22,7 +22,8 @@ export const MarketDetails = ({
}: {
market: MarketInfoNoCandlesQuery['market'];
}) => {
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
const assetSymbol =
market?.tradableInstrument.instrument.product?.settlementAsset.symbol;
const assetId = useMemo(
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
[market]
@@ -38,10 +39,6 @@ export const MarketDetails = ({
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
const liquidityPriceRange = formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
);
const panels = [
{
title: t('Key details'),
@@ -182,41 +179,32 @@ export const MarketDetails = ({
{
title: t('Liquidity price range'),
content: (
<>
<p className="text-xs mb-4">
{`For liquidity orders count towards a commitment they have to be
within either the liquidity or price monitoring bounds (whichever is
tighter).`}
</p>
<p className="text-xs mb-4">
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
price.`}
</p>
<MarketInfoTable
noBorder={false}
data={{
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
lowestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
highestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
}}
></MarketInfoTable>
</>
<MarketInfoTable
noBorder={false}
data={{
liquidityPriceRange: formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
),
LPVolumeMin:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${assetSymbol}`,
LPVolumeMax:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${assetSymbol}`,
}}
></MarketInfoTable>
),
},
{
@@ -13,7 +13,7 @@ export interface DeterministicOrderDetailsProps {
}
export const wrapperClasses =
'grid lg:grid-cols-1 flex items-center max-w-xl border border-vega-light-200 dark:border-vega-dark-150 rounded-md pv-2 ph-5 mb-5';
'grid lg:grid-cols-1 flex items-center max-w-xl border border-zinc-200 dark:border-zinc-800 rounded-md pv-2 ph-5 mb-5';
/**
* This component renders the *current* details for an order
@@ -42,7 +42,7 @@ const DeterministicOrderDetails = ({
<h2 className="text-3xl font-bold mb-4 display-5">
{t('Order not found')}
</h2>
<p className="text-vega-light-400 mb-12">
<p className="text-gray-500 mb-12">
{t('No order created from this transaction')}
</p>
</div>
@@ -38,7 +38,7 @@ export const PageHeader = ({
</h2>
{copy && (
<CopyWithTooltip data-testid="copy-to-clipboard" text={title}>
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" className="" />
</button>
</CopyWithTooltip>
@@ -8,7 +8,7 @@ interface PanelProps {
export const Panel = ({ children, className }: PanelProps) => (
<div
className={classNames(
'border border-vega-light-150 dark:border-vega-dark-150 rounded-md p-5 mb-5',
'border border-zinc-200 dark:border-zinc-800 rounded-md p-5 mb-5',
className
)}
>
@@ -1,43 +0,0 @@
import { useAssetDataProvider } from '@vegaprotocol/assets';
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { AssetLink } from '../links';
export type DecimalSource = 'ASSET';
export type SizeInAssetProps = {
assetId: string;
size?: string | number;
decimalSource?: DecimalSource;
};
/**
* Given a market ID and an order size it will fetch the market
* order size, and format the size accordingly
*/
const SizeInAsset = ({
assetId,
size,
decimalSource = 'ASSET',
}: SizeInAssetProps) => {
const { data } = useAssetDataProvider(assetId);
if (!size) {
return <span>-</span>;
}
let label = size;
if (data) {
if (decimalSource === 'ASSET' && data.decimals) {
label = addDecimalsFormatNumber(size, data.decimals);
}
}
return (
<p>
<span>{label}</span>&nbsp;
<AssetLink assetId={assetId} showAssetSymbol={true} asDialog={true} />
</p>
);
};
export default SizeInAsset;
@@ -3,7 +3,7 @@ import { useExplorerMarketQuery } from '../links/market-link/__generated__/Marke
export type DecimalSource = 'MARKET';
export type SizeInMarketProps = {
export type PriceInMarketProps = {
marketId: string;
size?: string | number;
decimalSource?: DecimalSource;
@@ -17,7 +17,7 @@ const SizeInMarket = ({
marketId,
size,
decimalSource = 'MARKET',
}: SizeInMarketProps) => {
}: PriceInMarketProps) => {
const { data } = useExplorerMarketQuery({
variables: { id: marketId },
fetchPolicy: 'cache-first',
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
type Deposit = components['schemas']['vegaBuiltinAssetDeposit'];
const fullMock: Deposit = {
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
partyId: 'party123',
vegaAssetId: 'asset123',
amount: 'amount123',
};
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
type Withdrawal = components['schemas']['vegaBuiltinAssetWithdrawal'];
const fullMock: Withdrawal = {
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
partyId: 'party123',
vegaAssetId: 'asset123',
amount: 'amount123',
};
@@ -67,12 +67,11 @@ describe('Chain Event: Builtin asset withdrawal', () => {
expect(screen.getByText(`${fullMock.amount}`)).toBeInTheDocument();
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
const partyLink = screen.getByText(`${fullMock.partyId}`);
expect(partyLink).toBeInTheDocument();
if (!partyLink.parentElement) {
throw new Error('Party link does not exist');
}
expect(partyLink.parentElement.tagName).toEqual('A');
expect(partyLink.parentElement.getAttribute('href')).toEqual(
`/parties/${fullMock.partyId}`
@@ -10,12 +10,10 @@ import { TxDetailsChainEventDeposit } from './tx-erc20-deposit';
type Deposit = components['schemas']['vegaERC20Deposit'];
const fullMock: Deposit = {
vegaAssetId:
'0000000000000000000000000000000000000000000000000000000000000002',
vegaAssetId: 'asset123',
amount: 'amount123',
sourceEthereumAddress: 'eth123',
targetPartyId:
'0000000000000000000000000000000000000000000000000000000000000001',
targetPartyId: 'vega123',
};
describe('Chain Event: ERC20 asset deposit', () => {
@@ -13,8 +13,7 @@ const fullMock: Deposit = {
amount: 'amount123',
blockTime: 'block123',
ethereumAddress: 'eth123',
vegaPublicKey:
'0000000000000000000000000000000000000000000000000000000000000001',
vegaPublicKey: 'vega123',
};
describe('Chain Event: Stake deposit', () => {
@@ -13,8 +13,7 @@ const fullMock: Remove = {
amount: 'amount123',
blockTime: 'block123',
ethereumAddress: 'eth123',
vegaPublicKey:
'0000000000000000000000000000000000000000000000000000000000000001',
vegaPublicKey: 'vega123',
};
describe('Chain Event: Stake remove', () => {
@@ -36,7 +36,7 @@ export const ChainResponseCode = ({
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
return (
<div title={`Response code: ${code} - ${label}`} className="inline-block">
<div title={`Response code: ${code} - ${label}`}>
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
@@ -14,14 +14,10 @@ interface TxDetailsSharedProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
// A transitional property used in some complex TX types to display more detailed type information
// than the shared component can derive
hideTypeRow?: boolean;
}
// Applied to all header cells
export const sharedHeaderProps = {
const sharedHeaderProps = {
// Ensures that multi line contents still have the header aligned to the first line
className: 'align-top',
};
@@ -35,7 +31,6 @@ export const TxDetailsShared = ({
txData,
pubKey,
blockData,
hideTypeRow = false,
}: TxDetailsSharedProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
@@ -46,12 +41,10 @@ export const TxDetailsShared = ({
return (
<>
{hideTypeRow === false ? (
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{txData.type}</TableCell>
</TableRow>
) : null}
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{txData.type}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Hash')}</TableCell>
<TableCell>
@@ -1,138 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { PartyLink } from '../../../../links';
import {
SPECIAL_CASE_NETWORK,
SPECIAL_CASE_NETWORK_ID,
} from '../../../../links/party-link/party-link';
import SizeInAsset from '../../../../size-in-asset/size-in-asset';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { AccountType } from '@vegaprotocol/types';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { Transfer } from '../transfer-details';
interface TransferParticipantsProps {
transfer: Transfer;
from: string;
}
/**
* Renders a box containing the To, From and amount of a
* transfer. This is shown for all transfers, including
* recurring and reward transfers.
*
* @param transfer A recurring transfer object
* @param from The sender is not in the transaction, but comes from the Transaction submitter
*/
export function TransferParticipants({
transfer,
from,
}: TransferParticipantsProps) {
// This mapping is required as the global account types require a type to be set, while
// the underlying protobufs allow for every field to be undefined.
const fromAcct =
transfer.fromAccountType &&
transfer.fromAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? AccountType[transfer.fromAccountType]
: AccountType.ACCOUNT_TYPE_GENERAL;
const fromAccountTypeLabel = transfer.fromAccountType
? AccountTypeMapping[fromAcct]
: 'Unknown';
const toAcct =
transfer.toAccountType &&
transfer.toAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? AccountType[transfer.toAccountType]
: AccountType.ACCOUNT_TYPE_GENERAL;
const toAccountTypeLabel = transfer.fromAccountType
? AccountTypeMapping[toAcct]
: 'Unknown';
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Transfer')}</h2>
<div className="relative block rounded-lg py-6 text-center">
<PartyLink id={from} truncate={true} />
<Tooltip
description={
<p>{`${t('From account')}: ${fromAccountTypeLabel}`}</p>
}
>
<span>
<Icon className="ml-3" name={'bank-account'} />
</span>
</Tooltip>
<br />
{/* This block of divs is used to render the inset arrow containing the transfer amount */}
<div className="bg-vega-light-200 dark:vega-dark-200 flex items-center justify-center my-4 relative">
<div className="bg-vega-light-200 dark:bg-vega-dark-200 border w-full pt-5 pb-3 px-3 border-vega-light-200 dark:border-vega-dark-150 relative">
<div className="text-xs z-20 relative leading-none">
{transfer.asset ? (
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
) : null}
</div>
{/* Empty divs for the top arrow and the bottom arrow of the transfer inset */}
<div className="z-10 absolute top-[-1px] left-1/2 w-4 h-4">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-black"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
</div>
<div className="z-10 absolute bottom-[-16px] left-1/2 w-4 h-4">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-vega-dark-200"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
</div>
{/*
<div className="z-10 absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rotate-45 w-4 h-4 dark:border-vega-dark-200 border-vega-light-200 bg-white dark:bg-black border-r border-b"></div>
<div className="z-10 absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-1/2 rotate-45 w-4 h-4 border-vega-light-200 dark:border-vega-dark-200 bg-vega-light-200 dark:bg-vega-dark-200 border-r border-b"></div>
*/}
</div>
</div>
<TransferRecurringRecipient to={transfer.to} />
<Tooltip
description={<p>{`${t('To account')}: ${toAccountTypeLabel}`}</p>}
>
<span>
<Icon className="ml-3" name={'bank-account'} />
</span>
</Tooltip>
<br />
</div>
</div>
);
}
interface TransferRecurringRecipientProps {
to?: string;
}
/**
* If the transfer is to 000...000, then this is a transfer to the
* Rewards Pool rather than the network. This component saves this
* logic from complicating the To section of the participants block
*
* @param markets String[] IDs of markets for this dispatch strategy
*/
export function TransferRecurringRecipient({
to,
}: TransferRecurringRecipientProps) {
if (to === SPECIAL_CASE_NETWORK || to === SPECIAL_CASE_NETWORK_ID) {
return <span>{t('Rewards pool')}</span>;
} else if (to) {
return <PartyLink id={to} truncate={true} />;
}
// Fallback should not happen
return null;
}
@@ -1,86 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { Icon } from '@vegaprotocol/ui-toolkit';
import EpochOverview from '../../../../epoch-overview/epoch';
import { useExplorerFutureEpochQuery } from '../../../../epoch-overview/__generated__/Epoch';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import type { Recurring } from '../transfer-details';
interface TransferRepeatProps {
recurring: Recurring;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferRepeat({ recurring }: TransferRepeatProps) {
const { data } = useExplorerFutureEpochQuery();
if (!recurring) {
return null;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Active epochs')}</h2>
<div className="relative block rounded-lg py-6 text-center p-6">
<p>
<EpochOverview id={recurring.startEpoch} />
</p>
<p className="leading-10 my-2">
<IconForEpoch
start={recurring.startEpoch}
end={recurring.endEpoch}
current={data?.epoch.id}
/>
</p>
<p>
{recurring.endEpoch ? (
<EpochOverview id={recurring.endEpoch} />
) : (
<span>{t('Forever')}</span>
)}
</p>
</div>
</div>
);
}
export type IconForTransferProps = {
current?: string;
start?: string;
end?: string;
};
/**
* Pick an icon rto represent the state of the repetition for this recurring
* transfer. It can be unstarted, in progress, or complete.
*
* @param start The epoch in which the transfer first occurs
* @param end The last epoch in which the transfer occurs
* @param current The current epoch
*/
function IconForEpoch({ start, end, current }: IconForTransferProps) {
let i: IconProps['name'] = 'repeat';
if (current && start && end) {
const startEpoch = parseInt(start);
const endEpoch = parseInt(end);
const currentEpoch = parseInt(current);
if (currentEpoch > endEpoch) {
// If we've finished
i = 'updated';
} else if (startEpoch > currentEpoch) {
// If we haven't yet started
i = 'time';
} else if (startEpoch < currentEpoch && endEpoch > currentEpoch) {
i = 'repeat';
}
}
return <Icon name={i} className="mr-2" />;
}
@@ -1,97 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { AssetLink, MarketLink } from '../../../../links';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { components } from '../../../../../../types/explorer';
import type { Recurring } from '../transfer-details';
import { DispatchMetricLabels } from '@vegaprotocol/types';
export type Metric = components['schemas']['vegaDispatchMetric'];
export type Strategy = components['schemas']['vegaDispatchStrategy'];
const metricLabels = {
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
...DispatchMetricLabels,
};
interface TransferRewardsProps {
recurring: Recurring;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferRewards({ recurring }: TransferRewardsProps) {
const metric =
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
if (!recurring || !recurring.dispatchStrategy) {
return null;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Reward metrics')}</h2>
<ul className="relative block rounded-lg py-6 text-center p-6">
{recurring.dispatchStrategy.assetForMetric ? (
<li>
<strong>{t('Asset')}</strong>:{' '}
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {metricLabels[metric]}
</li>
{recurring.dispatchStrategy.markets &&
recurring.dispatchStrategy.markets.length > 0 ? (
<li>
<strong>{t('Markets in scope')}</strong>:
<ul>
{recurring.dispatchStrategy.markets.map((m) => (
<li key={m}>
<MarketLink id={m} />
</li>
))}
</ul>
</li>
) : null}
<li>
<strong>{t('Factor')}</strong>: {recurring.factor}
</li>
</ul>
</div>
);
}
interface TransferRecurringStrategyProps {
strategy: Strategy;
}
/**
* Simple renderer for a dispatch strategy in a recurring transfer
*
* @param strategy Dispatch strategy object
*/
export function TransferRecurringStrategy({
strategy,
}: TransferRecurringStrategyProps) {
if (!strategy) {
return null;
}
return (
<>
{strategy.assetForMetric ? (
<li>
<strong>{t('Asset for metric')}</strong>:{' '}
<AssetLink assetId={strategy.assetForMetric} />
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {strategy.metric}
</li>
</>
);
}
@@ -1,39 +0,0 @@
import type { components } from '../../../../../types/explorer';
import { TransferRepeat } from './blocks/transfer-repeat';
import { TransferRewards } from './blocks/transfer-rewards';
import { TransferParticipants } from './blocks/transfer-participants';
export type Recurring = components['schemas']['v1RecurringTransfer'];
export type Metric = components['schemas']['vegaDispatchMetric'];
export const wrapperClasses =
'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha';
export type Transfer = components['schemas']['commandsv1Transfer'];
interface TransferDetailsProps {
transfer: Transfer;
from: string;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferDetails({ transfer, from }: TransferDetailsProps) {
const recurring = transfer.recurring;
return (
<div className="flex gap-5 flex-wrap">
<TransferParticipants from={from} transfer={transfer} />
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
{recurring && recurring.dispatchStrategy ? (
<TransferRewards recurring={transfer.recurring} />
) : null}
</div>
);
}
@@ -23,7 +23,6 @@ import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
import { TxDetailsNodeAnnounce } from './tx-node-announce';
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
import { TxDetailsTransfer } from './tx-transfer';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -109,8 +108,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsUndelegate;
case 'State Variable Proposal':
return TxDetailsStateVariable;
case 'Transfer Funds':
return TxDetailsTransfer;
default:
return TxDetailsGeneric;
}
@@ -1,125 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
import { TableRow, TableCell, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import { PartyLink } from '../../links';
import SizeInAsset from '../../size-in-asset/size-in-asset';
import { TransferDetails } from './transfer/transfer-details';
import {
SPECIAL_CASE_NETWORK,
SPECIAL_CASE_NETWORK_ID,
} from '../../links/party-link/party-link';
type Transfer = components['schemas']['commandsv1Transfer'];
interface TxDetailsNodeAnnounceProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Displays the details of a transfer. Broadly there are three distinct
* types of transfer, listed below in order of complexity:
*
* - A one off transfer
* - A recurring transfer
* - A recurring rewards pool transfer
*
* One off transfers are simple, really the important data is the amount
* and who sent it to whom. This is rendered as one distinct box.
*
* A recurring transfer has two components - the same as above, and an
* additional box that shows details about how it repeats. This is defined
* as a start epoch and and end epoch. The Epoch/MissingEpoch components
* render slightly differently depending on if the epoch is in the past,
* current or in the future.
*
* Finally rewards pool transfers get the two boxes above, and an additional
* one that describes how the reward is distributed.
*
* The information is split up in to three boxes to allow for the reuse across
* all the types of transfer above.
*/
export const TxDetailsTransfer = ({
txData,
pubKey,
blockData,
}: TxDetailsNodeAnnounceProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const transfer: Transfer = txData.command.transfer;
if (!transfer) {
return <>{t('Transfer data missing')}</>;
}
const from = txData.submitter;
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{getTypeLabelForTransfer(transfer)}</TableCell>
</TableRow>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
hideTypeRow={true}
/>
{from ? (
<TableRow modifier="bordered">
<TableCell>{t('From')}</TableCell>
<TableCell>
<PartyLink id={from} />
</TableCell>
</TableRow>
) : null}
{transfer.to ? (
<TableRow modifier="bordered">
<TableCell>{t('To')}</TableCell>
<TableCell>
<PartyLink id={transfer.to} />
</TableCell>
</TableRow>
) : null}
{transfer.asset && transfer.amount ? (
<TableRow modifier="bordered">
<TableCell>{t('Amount')}</TableCell>
<TableCell>
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
</TableCell>
</TableRow>
) : null}
</TableWithTbody>
<TransferDetails from={from} transfer={transfer} />
</>
);
};
/**
* Gets a string description of this transfer
* @param txData A full transfer
* @returns string Transfer label
*/
export function getTypeLabelForTransfer(tx: Transfer) {
if (tx.to === SPECIAL_CASE_NETWORK || tx.to === SPECIAL_CASE_NETWORK_ID) {
if (tx.recurring && tx.recurring.dispatchStrategy) {
return 'Reward top up transfer';
}
// Else: we don't know that it's a reward transfer, so let's not guess
} else if (tx.recurring) {
return 'Recurring transfer';
} else if (tx.oneOff) {
// Currently redundant, but could be used to indicate something more specific
return 'Transfer';
}
return 'Transfer';
}
@@ -14,17 +14,14 @@ interface StringMap {
// Using https://github.com/vegaprotocol/protos/blob/e0f646ce39aab1fc66a9200ceec0262306d3beb3/commands/transaction.go#L93 as a reference
const displayString: StringMap = {
OrderSubmission: 'Order Submission',
'Submit Order': 'Order',
OrderCancellation: 'Order Cancellation',
OrderAmendment: 'Order Amendment',
VoteSubmission: 'Vote Submission',
WithdrawSubmission: 'Withdraw Submission',
Withdraw: 'Withdraw Request',
LiquidityProvisionSubmission: 'LP order',
'Liquidity Provision Order': 'LP order',
LiquidityProvisionCancellation: 'LP cancel',
LiquidityProvisionAmendment: 'LP update',
'Amend LiquidityProvision Order': 'Amend LP',
LiquidityProvisionSubmission: 'Liquidity Provision',
LiquidityProvisionCancellation: 'Liquidity Cancellation',
LiquidityProvisionAmendment: 'Liquidity Amendment',
ProposalSubmission: 'Governance Proposal',
AnnounceNode: 'Node Announcement',
NodeVote: 'Node Vote',
@@ -34,11 +31,10 @@ const displayString: StringMap = {
DelegateSubmission: 'Delegation',
UndelegateSubmission: 'Undelegation',
KeyRotateSubmission: 'Key Rotation',
StateVariableProposal: 'State Variable',
StateVariableProposal: 'State Variable Proposal',
Transfer: 'Transfer',
CancelTransfer: 'Cancel Transfer',
ValidatorHeartbeat: 'Heartbeat',
'Batch Market Instructions': 'Batch',
ValidatorHeartbeat: 'Validator Heartbeat',
};
/**
@@ -136,8 +132,7 @@ export function getLabelForChainEvent(
export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
let type = displayString[orderType] || orderType;
let colours =
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
let colours = 'text-white dark:text-white bg-zinc-800 dark:bg-zinc-800';
// This will get unwieldy and should probably produce a different colour of tag
if (type === 'Chain Event' && !!command?.chainEvent) {
@@ -1,58 +0,0 @@
import { getTypeLabelForTransfer } from './details/tx-transfer';
import type { components } from '../../../types/explorer';
type Transfer = components['schemas']['commandsv1Transfer'];
describe('TX: Transfer: getLabelForTransfer', () => {
it('renders reward top up label if the TO party is 000', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000000',
recurring: {
dispatchStrategy: {},
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Reward top up transfer');
});
it('renders reward top up label if the TO party is network', () => {
const mock = {
to: 'network',
recurring: {
dispatchStrategy: {},
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Reward top up transfer');
});
it('renders recurring label if the tx has a recurring property', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000001',
recurring: {
startEpoch: '0',
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Recurring transfer');
});
it('renders one off label if the tx has a oneOff property', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000001',
oneOff: {
deliverOn: '0',
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
});
it('renders one off label otherwise', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000001',
};
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
});
});
@@ -37,9 +37,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="tx-hash"
>
<span className="xl:hidden uppercase text-vega-dark-300">
ID:&nbsp;
</span>
<span className="xl:hidden uppercase text-zinc-500">ID:&nbsp;</span>
<TruncatedLink
to={`/${Routes.TX}/${toHex(hash)}`}
text={hash}
@@ -51,9 +49,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="pub-key"
>
<span className="xl:hidden uppercase text-vega-dark-300">
By:&nbsp;
</span>
<span className="xl:hidden uppercase text-zinc-500">By:&nbsp;</span>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
@@ -68,9 +64,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-3 xl:col-span-1 leading-none flex items-center"
data-testid="tx-block"
>
<span className="xl:hidden uppercase text-vega-dark-300">
Block:&nbsp;
</span>
<span className="xl:hidden uppercase text-zinc-500">Block:&nbsp;</span>
<TruncatedLink
to={`/${Routes.BLOCKS}/${block}`}
text={block}
@@ -82,7 +76,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-2 xl:col-span-1 leading-none flex items-center"
data-testid="tx-success"
>
<span className="xl:hidden uppercase text-vega-dark-300">
<span className="xl:hidden uppercase text-zinc-500">
Success:&nbsp;
</span>
{isNumber(code) ? (
@@ -94,7 +94,7 @@ export const TxsInfiniteList = ({
return (
<div className={className} data-testid="transactions-list">
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-zinc-500 uppercase">
<div className="col-span-3">
<span className="hidden xl:inline">Transaction &nbsp;</span>
<span>ID</span>
@@ -68,7 +68,7 @@ const Party = () => {
return (
<section>
<h1
className="font-alpha uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
className="font-alpha uppercase font-xl mb-4 text-zinc-800 dark:text-zinc-200"
data-testid="parties-header"
>
{t('Public key')}
+21 -62
View File
@@ -15,7 +15,6 @@ type OneOf<T extends any[]> = T extends [infer Only]
? OneOf<[XOR<A, B>, ...Rest]>
: never;
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface paths {
'/info': {
/**
@@ -41,6 +40,8 @@ export interface paths {
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/**
@@ -65,7 +66,7 @@ export interface components {
| 'OPERATOR_LESS_THAN'
| 'OPERATOR_LESS_THAN_OR_EQUAL';
/**
* The supported oracle sources
* The supported Oracle sources
* @description - ORACLE_SOURCE_UNSPECIFIED: The default value
* - ORACLE_SOURCE_OPEN_ORACLE: Specifies that the payload will be base64 encoded JSON conforming to the Open Oracle standard
* - ORACLE_SOURCE_JSON: Specifies that the payload will be base64 encoded JSON, but does not specify the shape of the data
@@ -173,7 +174,7 @@ export interface components {
readonly '@type'?: string;
[key: string]: unknown | undefined;
};
/** Used to announce a node as a new pending validator */
/** Used announce a node as a new pending validator */
readonly v1AnnounceNode: {
/** AvatarURL of the validator */
readonly avatarUrl?: string;
@@ -268,13 +269,13 @@ export interface components {
readonly v1ETHAddress: {
readonly address?: string;
};
/** A transaction to allow a validator to rotate their ethereum keys */
/** A transaction to allow validator to rotate their ethereum keys */
readonly v1EthereumKeyRotateSubmission: {
/** Currently used public address */
readonly currentAddress?: string;
/** Signature that can be verified using the new ethereum address */
readonly ethereumSignature?: components['schemas']['v1Signature'];
/** The new address to rotate to */
/** The new adress to rotate to */
readonly newAddress?: string;
/** Ethereum public key to use as a submitter to allow automatic signature generation */
readonly submitterAddress?: string;
@@ -308,9 +309,7 @@ export interface components {
readonly version?: string;
};
readonly v1InputData: {
/** A command used by a node operator to announce its node as a pending validator */
readonly announceNode?: components['schemas']['v1AnnounceNode'];
/** A command to submit a batch of order instructions to a market */
readonly batchMarketInstructions?: components['schemas']['v1BatchMarketInstructions'];
/**
* Format: uint64
@@ -324,35 +323,17 @@ export interface components {
* `block_height` prevents replay attacks in conjunction with `nonce` (see above).
*/
readonly blockHeight?: string;
/** A command to request cancelling a recurring transfer */
readonly cancelTransfer?: components['schemas']['v1CancelTransfer'];
/**
* Command used by a validator to submit an event forwarded to the Vega network to provide information
* on events happening on other networks, to be used by a foreign chain
* to recognise a decision taken by the Vega network
*/
readonly chainEvent?: components['schemas']['v1ChainEvent'];
/** Command to delegate tokens to a validator */
readonly delegateSubmission?: components['schemas']['v1DelegateSubmission'];
/** Command used by a validator to allow given validator to rotate their Ethereum keys */
readonly ethereumKeyRotateSubmission?: components['schemas']['v1EthereumKeyRotateSubmission'];
/** Command used by a validator to submit signatures to a smart contract */
readonly issueSignatures?: components['schemas']['v1IssueSignatures'];
/** Command used by a validator to allow given validator to rotate their Vega keys */
readonly keyRotateSubmission?: components['schemas']['v1KeyRotateSubmission'];
/** Command to request amending a liquidity commitment */
readonly liquidityProvisionAmendment?: components['schemas']['v1LiquidityProvisionAmendment'];
/** Command to request cancelling a liquidity commitment */
readonly liquidityProvisionCancellation?: components['schemas']['v1LiquidityProvisionCancellation'];
/** Command to submit a liquidity commitment */
readonly liquidityProvisionSubmission?: components['schemas']['v1LiquidityProvisionSubmission'];
/** Command used by a validator to submit a signature, to be used by a foreign chain to recognise a decision taken by the Vega network */
readonly nodeSignature?: components['schemas']['v1NodeSignature'];
/**
* Validator commands
* Command used by a validator when a node votes for validating that a given resource exists or is valid,
* for example, an ERC20 deposit is valid and exists on ethereum
*/
/** Validator commands */
readonly nodeVote?: components['schemas']['v1NodeVote'];
/**
* Format: uint64
@@ -368,50 +349,30 @@ export interface components {
* slightly differently, causing a different hash.
*/
readonly nonce?: string;
/**
* Oracles
* Command to submit new oracle data from third party providers
*/
/** Oracles */
readonly oracleDataSubmission?: components['schemas']['v1OracleDataSubmission'];
/** Command to amend an order */
readonly orderAmendment?: components['schemas']['v1OrderAmendment'];
/**
* User commands
* Command to cancel an order
*/
readonly orderCancellation?: components['schemas']['v1OrderCancellation'];
/** A command for submitting an order */
/** User commands */
readonly orderSubmission?: components['schemas']['v1OrderSubmission'];
/** Command to submit a governance proposal */
readonly proposalSubmission?: components['schemas']['v1ProposalSubmission'];
/** Command used by a validator to propose a protocol upgrade */
readonly protocolUpgradeProposal?: components['schemas']['v1ProtocolUpgradeProposal'];
/** Command used by a validator to submit a floating point value */
readonly stateVariableProposal?: components['schemas']['v1StateVariableProposal'];
/** Command to submit a transfer */
readonly transfer?: components['schemas']['commandsv1Transfer'];
/** Command to remove tokens delegated to a validator */
readonly undelegateSubmission?: components['schemas']['v1UndelegateSubmission'];
/**
* Command used by a validator to signal they are still online and validating blocks
* or ready to validate blocks when they are still a pending validator
*/
readonly validatorHeartbeat?: components['schemas']['v1ValidatorHeartbeat'];
/** Command to submit a vote on a governance proposal */
readonly voteSubmission?: components['schemas']['v1VoteSubmission'];
/** Command to submit a withdrawal */
readonly withdrawSubmission?: components['schemas']['v1WithdrawSubmission'];
};
/** A transaction for a validator to submit signatures to a smart contract */
readonly v1IssueSignatures: {
/** The kind of signatures to generate, namely for whether a signer is being added or removed */
readonly kind?: components['schemas']['v1NodeSignatureKind'];
/** The ethereum address which will submit the signatures to the smart contract */
/** The ethereum address which will submit the signatures to the smart-contract */
readonly submitter?: string;
/** The ID of the node that will be signed in or out of the smart contract */
/** The ID of the node that will be signed in or out of the smartcontract */
readonly validatorNodeId?: string;
};
/** A transaction to allow a validator to rotate their Vega keys */
/** A transaction to allow validator to rotate their Vega keys */
readonly v1KeyRotateSubmission: {
/** Hash of currently used public key */
readonly currentPubKeyHash?: string;
@@ -535,8 +496,8 @@ export interface components {
/** Specific details for a one off transfer */
readonly v1OneOffTransfer: {
/**
* A unix timestamp in seconds. Time at which the
* transfer should be delivered into the To account
* A unix timestamp in second. Time at which the
* transfer should be delivered in the to account
* Format: int64
*/
readonly deliverOn?: string;
@@ -551,7 +512,7 @@ export interface components {
readonly payload?: string;
/**
* @description The source from which the data is coming from. Must be base64 encoded.
* Oracle data is a type of external data source data.
* Oracle data a type of external data source data.
*/
readonly source?: components['schemas']['OracleDataSubmissionOracleSource'];
};
@@ -642,7 +603,7 @@ export interface components {
/** Type for the order, required field - See `Order.Type` */
readonly type?: components['schemas']['vegaOrderType'];
};
/** @description PropertyKey describes the property key contained in data source data. */
/** @description PropertyKey describes the property key contained in an data source data. */
readonly v1PropertyKey: {
/** @description name is the name of the property. */
readonly name?: string;
@@ -689,7 +650,6 @@ export interface components {
/** Proposal configuration and the actual change that is meant to be executed when proposal is enacted */
readonly terms?: components['schemas']['vegaProposalTerms'];
};
/** A transaction for a validator to suggest a protocol upgrade */
readonly v1ProtocolUpgradeProposal: {
/**
* The block height at which to perform the upgrade
@@ -748,7 +708,6 @@ export interface components {
*/
readonly pubKey?: components['schemas']['v1PubKey'];
};
/** A transaction for a validator to submit a floating point value */
readonly v1StateVariableProposal: {
/** The state value proposal details */
readonly proposal?: components['schemas']['vegaStateValueProposal'];
@@ -1010,7 +969,7 @@ export interface components {
readonly sourceEthereumAddress?: string;
/** The Vega network internal identifier of the asset */
readonly vegaAssetId?: string;
/** The updated withdrawal threshold */
/** The updated withdraw threshold */
readonly withdrawThreshold?: string;
};
/** An asset allow-listing for an ERC20 token */
@@ -1105,7 +1064,7 @@ export interface components {
/** The ethereum address of the old signer */
readonly oldSigner?: string;
};
/** The threshold has been updated on the multisig control */
/** The threshold have been updated on the multisigcontrol */
readonly vegaERC20ThresholdSet: {
/**
* Format: int64
@@ -1119,13 +1078,13 @@ export interface components {
* Format: int64
*/
readonly newThreshold?: number;
/** The nonce created by the Vega network */
/** The nonce create by the vega network */
readonly nonce?: string;
};
readonly vegaERC20Update: {
/**
* The lifetime limits deposit per address.
* This will be interpreted against the asset decimals.
* This is will be interpreted against the asset decimals.
* note: this is a temporary measure that can be changed by governance
*/
readonly lifetimeLimit?: string;
@@ -1278,7 +1237,7 @@ export interface components {
* price levels over which automated liquidity provision orders will be deployed
*/
readonly lpPriceRange?: string;
/** Optional new market metadata, tags */
/** Optional new market meta data, tags */
readonly metadata?: readonly string[];
/**
* Decimal places for order sizes, sets what size the smallest order / position on the market can be
File diff suppressed because it is too large Load Diff
@@ -201,21 +201,12 @@ context('Staking Page - verify elements on page', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
if ($stakePercentage != '-') {
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?!0+(?:\.0+)?%)(?:\d|[1-9]\d|100)(?:(?<!100)\.\d+)?%/
);
}
});
});
+1 -10
View File
@@ -744,22 +744,13 @@
"Proposals": "Proposals",
"Validators": "Validators",
"Redeem": "Redeem",
"validatorFormIntro": "To learn more about validators and how scores are calculated,",
"readMoreValidatorForm": "read about staking on Vega",
"StakeDescription": "The total amount $VEGA staked to this validator including self-stake and all delegation.",
"NormalisedVotingPowerDescription": "Voting power is the relative weighting given to the validator in tendermint consensus. It is calculated based on the stake share of the validator minus any penalties due to overstaking or performance.",
"StakeShareDescription": "The stake a validator represents as a share of total stake across the network.",
"TotalPenaltiesDescription": "Total of penalties taking into account performance (considering proportion of blocks proposed against the number of blocks the validator was expected to propose) and any overstaking.",
"PendingStakeDescription": "The amount of stake that will be added or removed from the validator from the next epoch.",
"StakeNeededForPromotionStandbyDescription": "{{prefix}} additional stake needed for promotion to consensus, assuming constant performance in line with previous epoch.",
"StakeNeededForPromotionCandidateDescription": "{{prefix}} additional stake needed for promotion to standby, assuming constant performance in line with previous epoch.",
"StakedByOperatorDescription": "The stake provided as self-stake by the node operator, must be at least the minimum stake as defined by network parameter",
"AboutThisValidatorDescription": "External URL provided by the validator linking to information about themselves",
"ValidatorStatusDescription": "Consensus, Standby or Pending (Candidate), depending on how much stake the validator has attracted",
"StakedByDelegatesDescription": "The stake delegated to the node by other users",
"OverstakedPenaltyDescription": "A penalty applied for having more stake than the optimal stake for the network. Designed to avoid concentration of voting power with a small number of validators",
"PerformancePenaltyDescription": "Performance score is a measure of how often a validator proposed blocks in the last epoch relative to how many they should be expected to propose based on their voting power. Performance penalty is applied for having a performance score of less than 1",
"UnnormalisedVotingPowerDescription": "The voting power of the validator based on their final validator score after all penalties have been applied",
"NormalisedVotingPowerDescription": "The voting power of the validator, adjusted to ensure all validator scores sum to 1, used for distribution of rewards",
"Score": "Score",
"performancePenalty": "Performance penalty",
"overstaked": "Overstaked",
@@ -363,7 +363,6 @@ export const ConsensusValidatorsTable = ({
customThemeParams={NODE_LIST_GRID_STYLES}
getRowHeight={(params: RowHeightParams) => getRowHeight(params)}
defaultColDef={defaultColDef}
tooltipShowDelay={0}
animateRows={true}
suppressCellFocus={true}
overlayNoRowsTemplate={t('noValidators')}
@@ -59,6 +59,8 @@ export const defaultColDef = {
comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b),
cellStyle: { margin: '10px 0', padding: '0 12px' },
tooltipComponent: TooltipCellComponent,
tooltipShowDelay: 0,
tooltipHideDelay: 0,
};
interface ValidatorRendererProps {
@@ -236,7 +236,6 @@ export const StandbyPendingValidatorsTable = ({
customThemeParams={NODE_LIST_GRID_STYLES}
rowHeight={52}
defaultColDef={defaultColDef}
tooltipShowDelay={0}
animateRows={true}
suppressCellFocus={true}
overlayNoRowsTemplate={t('noValidators')}
@@ -1,145 +0,0 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { ValidatorTable } from './validator-table';
import { ValidatorStatus } from '@vegaprotocol/types';
import countryData from '../../../components/country-selector/country-data';
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
const mockNode = {
id: 'bb1822715aa86ce0e205aa4c78e9b71cdeaec94596ce72d366f0d50589eb1bf5',
name: 'Marvin',
pubkey: 'f45085a3bbce4e8197910448a0f22e7d5e79f8234336053bc11b177b0d70e785',
infoUrl: 'https://en.wikipedia.org/wiki/Marvin_the_Paranoid_Android',
location: 'AQ',
ethereumAddress: '0x6ae2ff81b4a00f2edbed1fe3551ee0e3d81aa4f4',
stakedByOperator: '3000000000000000000000',
stakedByDelegates: '1280000000000000000',
stakedTotal: '3001280000000000000000',
pendingStake: '0',
epochData: null,
rankingScore: {
rankingScore: '0.3999466965166174',
stakeScore: '0.1999733482583087',
performanceScore: '1',
votingPower: '2000',
status: ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
},
};
const mockStakedTotal = '15008.4';
const decimals = 18;
const renderComponent = () =>
render(
<AppStateProvider>
<ValidatorTable node={mockNode} stakedTotal={mockStakedTotal} />
</AppStateProvider>
);
describe('ValidatorTable', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should render a link to the staking guide', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-table-staking-guide-link')).toBeTruthy();
});
it('should render the correct node id', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-id')).toHaveTextContent(mockNode.id);
});
it('should render the validator description url', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-description-url')).toHaveAttribute(
'href',
mockNode.infoUrl
);
});
it('should render the correct validator status', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-status')).toHaveTextContent('Consensus');
});
it('should render a link to the validator forum', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-forum-link')).toBeTruthy();
});
it('should render the pubkey', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-public-key')).toHaveTextContent(
mockNode.pubkey
);
});
it('should render the server location', () => {
const location = countryData.find((c) => c.code === mockNode.location);
const { getByTestId } = renderComponent();
expect(getByTestId('validator-server-location')).toHaveTextContent(
// @ts-ignore - location is not null
location.name
);
});
it('should render the ethereum address', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('validator-eth-address')).toHaveTextContent(
mockNode.ethereumAddress
);
});
it('should render the staked by operator', () => {
const stakedByOperator = formatNumber(
toBigNum(mockNode.stakedByOperator, decimals)
);
const { getByTestId } = renderComponent();
expect(getByTestId('staked-by-operator')).toHaveTextContent(
stakedByOperator
);
});
it('should render the staked by delegates', () => {
const stakedByDelegates = formatNumber(
toBigNum(mockNode.stakedByDelegates, decimals)
);
const { getByTestId } = renderComponent();
expect(getByTestId('staked-by-delegates')).toHaveTextContent(
stakedByDelegates
);
});
it('should render the total stake', () => {
const stakedTotal = formatNumber(toBigNum(mockNode.stakedTotal, decimals));
const { getByTestId } = renderComponent();
expect(getByTestId('total-stake')).toHaveTextContent(stakedTotal);
});
it('should render the pending stake', () => {
const pendingStake = formatNumber(
toBigNum(mockNode.pendingStake, decimals)
);
const { getByTestId } = renderComponent();
expect(getByTestId('pending-stake')).toHaveTextContent(pendingStake);
});
it('should render the values calculated by the helper functions', () => {
// these functions are all tested in shared.spec.ts
const { getByTestId } = renderComponent();
expect(getByTestId('stake-percentage')).toBeInTheDocument();
expect(getByTestId('overstaking-penalty')).toBeInTheDocument();
expect(getByTestId('performance-penalty')).toBeInTheDocument();
expect(getByTestId('total-penalties')).toBeInTheDocument();
expect(getByTestId('unnormalised-voting-power')).toBeInTheDocument();
expect(getByTestId('normalised-voting-power')).toBeInTheDocument();
});
});
@@ -1,25 +1,19 @@
import React, { useMemo } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import countryData from '../../../components/country-selector/country-data';
import { Link as UTLink, Link } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import {
createDocsLinks,
ExternalLinks,
toBigNum,
} from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
import {
Link as UTLink,
Link,
Tooltip,
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { BigNumber } from '../../../lib/bignumber';
import { formatNumber } from '../../../lib/format-number';
import { ExternalLinks, toBigNum } from '@vegaprotocol/react-helpers';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import countryData from '../../../components/country-selector/country-data';
import * as Schema from '@vegaprotocol/types';
import { SubHeading } from '../../../components/heading';
import {
getLastEpochScoreAndPerformance,
@@ -29,7 +23,6 @@ import {
getPerformancePenalty,
getTotalPenalties,
getUnnormalisedVotingPower,
getStakePercentage,
} from '../shared';
import type { ReactNode } from 'react';
import type { StakingNodeFieldsFragment } from './__generated__/Staking';
@@ -69,7 +62,7 @@ export const ValidatorTable = ({
stakedTotal,
previousEpochData,
}: ValidatorTableProps) => {
const { ETHERSCAN_URL, VEGA_DOCS_URL } = useEnvironment();
const { ETHERSCAN_URL } = useEnvironment();
const { t } = useTranslation();
const {
appState: { decimals },
@@ -88,7 +81,10 @@ export const ValidatorTable = ({
node.stakedTotal
);
const stakePercentage = getStakePercentage(total, stakedOnNode);
const stakePercentage =
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
? '-'
: stakedOnNode.dividedBy(total).times(100).dp(2).toString() + '%';
const totalPenaltiesAmount = getTotalPenalties(
rawValidatorScore,
@@ -98,207 +94,158 @@ export const ValidatorTable = ({
);
return (
<>
<p className="mb-12">
{t('validatorFormIntro')}{' '}
{VEGA_DOCS_URL && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
target="_blank"
data-testid="validator-table-staking-guide-link"
className="text-white"
>
{t('readMoreValidatorForm')}
</ExternalLink>
)}
</p>
<div className="my-12" data-testid="validator-table">
<SubHeading title={t('profile')} />
<RoundedWrapper>
<KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow>
<span>{t('id')}</span>
<ValidatorTableCell dataTestId="validator-id">
{node.id}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('ABOUT THIS VALIDATOR')}</span>
<span>
<a href={node.infoUrl}>{node.infoUrl}</a>
</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('STATUS')}</strong>
</span>
<span data-testid="validator-status">
<strong>
{t(statusTranslationKey(node.rankingScore.status))}
</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<div className="my-12" data-testid="validator-table">
<SubHeading title={t('profile')} />
<RoundedWrapper>
<KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow>
<span>{t('id')}</span>
<ValidatorTableCell dataTestId="validator-id">
{node.id}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('ABOUT THIS VALIDATOR')}</span>
<Tooltip description={t('AboutThisValidatorDescription')}>
<a data-testid="validator-description-url" href={node.infoUrl}>
{node.infoUrl}
</a>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('STATUS')}</strong>
</span>
<Tooltip description={t('ValidatorStatusDescription')}>
<span data-testid="validator-status">
<strong>
{t(statusTranslationKey(node.rankingScore.status))}
</strong>
</span>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<div className="mb-10">
{t('validatorTableIntro')}{' '}
<UTLink
href={ExternalLinks.VALIDATOR_FORUM}
target="_blank"
data-testid="validator-forum-link"
>
{t('onTheForum')}
</UTLink>
</div>
<SubHeading title={t('ADDRESS')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
<ValidatorTableCell dataTestId="validator-public-key">
{node.pubkey}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('SERVER LOCATION')}</span>
<ValidatorTableCell dataTestId="validator-server-location">
{countryData.find((c) => c.code === node.location)?.name ||
t('not available')}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('ETHEREUM ADDRESS')}</span>
<span data-testid="validator-eth-address">
<Link
title={t('View on Etherscan (opens in a new tab)')}
href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
target="_blank"
>
{node.ethereumAddress}
</Link>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('STAKE')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span>
<Tooltip description={t('StakedByOperatorDescription')}>
<span data-testid="staked-by-operator">
{formatNumber(toBigNum(node.stakedByOperator, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('STAKED BY DELEGATES')}</span>
<Tooltip description={t('StakedByDelegatesDescription')}>
<span data-testid="staked-by-delegates">
{formatNumber(toBigNum(node.stakedByDelegates, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
<strong>{t('TOTAL STAKE')}</strong>
</span>
<span data-testid="total-stake">
<strong>
{formatNumber(toBigNum(node.stakedTotal, decimals))}
</strong>
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PENDING STAKE')}</span>
<Tooltip description={t('PendingStakeDescription')}>
<span data-testid="pending-stake">
{formatNumber(toBigNum(node.pendingStake, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('STAKE SHARE')}</span>
<Tooltip description={t('StakeShareDescription')}>
<span data-testid="stake-percentage">{stakePercentage}</span>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('PENALTIES')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-penalties">
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
<Tooltip description={t('OverstakedPenaltyDescription')}>
<span data-testid="overstaking-penalty">
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE PENALTY')}</span>
<Tooltip description={t('PerformancePenaltyDescription')}>
<span data-testid="performance-penalty">
{getPerformancePenalty(performanceScore)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('TOTAL PENALTIES')}</strong>
</span>
<span data-testid="total-penalties">
<strong>{totalPenaltiesAmount}</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('VOTING POWER')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-voting-power">
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
<Tooltip description={t('UnnormalisedVotingPowerDescription')}>
<span data-testid="unnormalised-voting-power">
{getUnnormalisedVotingPower(rawValidatorScore)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('NORMALISED VOTING POWER')}</strong>
</span>
<Tooltip description={t('NormalisedVotingPowerDescription')}>
<strong data-testid="normalised-voting-power">
{getNormalisedVotingPower(node.rankingScore.votingPower)}
</strong>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<div className="mb-10">
{t('validatorTableIntro')}{' '}
<UTLink
href={ExternalLinks.VALIDATOR_FORUM}
target="_blank"
data-testid="validator-forum-link"
>
{t('onTheForum')}
</UTLink>
</div>
</>
<SubHeading title={t('ADDRESS')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
<ValidatorTableCell dataTestId="validator-public-key">
{node.pubkey}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('SERVER LOCATION')}</span>
<ValidatorTableCell>
{countryData.find((c) => c.code === node.location)?.name ||
t('not available')}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('ETHEREUM ADDRESS')}</span>
<span>
<Link
title={t('View on Etherscan (opens in a new tab)')}
href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
target="_blank"
>
{node.ethereumAddress}
</Link>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('STAKE')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span>
<span data-testid="staked-by-operator">
{formatNumber(toBigNum(node.stakedByOperator, decimals))}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('STAKED BY DELEGATES')}</span>
<span data-testid="staked-by-delegates">
{formatNumber(toBigNum(node.stakedByDelegates, decimals))}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
<strong>{t('TOTAL STAKE')}</strong>
</span>
<span data-testid="total-stake">
<strong>
{formatNumber(toBigNum(node.stakedTotal, decimals))}
</strong>
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PENDING STAKE')}</span>
<span data-testid="pending-stake">
{formatNumber(toBigNum(node.pendingStake, decimals))}
</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('STAKE SHARE')}</span>
<span data-testid="stake-percentage">{stakePercentage}</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('PENALTIES')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-penalties">
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
<span>
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE PENALTY')}</span>
<span>{getPerformancePenalty(performanceScore)}</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('TOTAL PENALTIES')}</strong>
</span>
<span>
<strong>{totalPenaltiesAmount}</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<SubHeading title={t('VOTING POWER')} />
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-voting-power">
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
<span>{getUnnormalisedVotingPower(rawValidatorScore)}</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('NORMALISED VOTING POWER')}</strong>
</span>
<span>
<strong>
{getNormalisedVotingPower(node.rankingScore.votingPower)}
</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
);
};
@@ -8,7 +8,6 @@ import {
getFormattedPerformanceScore,
getPerformancePenalty,
getTotalPenalties,
getStakePercentage,
} from './shared';
describe('getLastEpochScoreAndPerformance', () => {
@@ -140,23 +139,3 @@ describe('getTotalPenalties', () => {
expect(getTotalPenalties('0.25', '0.5', '1000', '10000')).toEqual('0.00%');
});
});
describe('getStakePercentage', () => {
it('should return the stake percentage', () => {
expect(
getStakePercentage(new BigNumber('1000'), new BigNumber('100'))
).toEqual('10%');
expect(
getStakePercentage(new BigNumber('1000'), new BigNumber('500'))
).toEqual('50%');
expect(
getStakePercentage(new BigNumber('1000'), new BigNumber('257.5'))
).toEqual('25.75%');
});
it('should return "0%" if the total stake is 0', () => {
expect(getStakePercentage(new BigNumber('0'), new BigNumber('0'))).toEqual(
'0%'
);
});
});
-5
View File
@@ -95,8 +95,3 @@ export const getTotalPenalties = (
2
);
};
export const getStakePercentage = (total: BigNumber, stakedOnNode: BigNumber) =>
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
? '0%'
: stakedOnNode.dividedBy(total).times(100).dp(2).toString() + '%';
-1
View File
@@ -22,7 +22,6 @@ module.exports = defineConfig({
viewportHeight: 900,
responseTimeout: 50000,
requestTimeout: 20000,
retries: 2,
},
env: {
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
@@ -72,11 +72,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
cy.getByTestId('approve-warning').should(
'contain.text',
`Deposits of ${btcSymbol} not approved`
);
cy.getByTestId('deposit-submit').click();
cy.getByTestId('deposit-approve-submit').click();
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
cy.get('[data-testid="Return to deposit"]').click();
cy.get(amountField).clear().type('10');
@@ -411,7 +407,7 @@ describe('capsule', { tags: '@slow' }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
cy.get(assetSelectField, txTimeout).select(vegaName, { force: true });
cy.getByTestId('deposit-submit').click();
cy.getByTestId('deposit-approve-submit').click();
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
cy.get('[data-testid="Return to deposit"]').click();
cy.get(amountField).clear().type('10000');
+6 -132
View File
@@ -1,5 +1,3 @@
import { removeDecimal } from '@vegaprotocol/cypress';
import { ethers } from 'ethers';
import { connectEthereumWallet } from '../support/ethereum-wallet';
import { selectAsset } from '../support/helpers';
@@ -11,7 +9,7 @@ const formFieldError = 'input-error-text';
const ASSET_EURO = 1;
describe('deposit form validation', { tags: '@smoke' }, () => {
function openDepositForm() {
before(() => {
cy.mockWeb3Provider();
cy.mockSubscription();
cy.mockTradingPage();
@@ -22,14 +20,10 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
cy.getByTestId('deposit-button').click();
cy.wait('@Assets');
connectEthereumWallet('MetaMask');
}
before(() => {
openDepositForm();
cy.getByTestId('deposit-submit').click();
});
it('handles empty fields', () => {
cy.getByTestId('deposit-submit').click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 2);
});
@@ -50,13 +44,6 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
});
it('invalid amount', () => {
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
// Deposit amount smaller than minimum viable for selected asset
// Select an amount so that we have a known decimal places value to work with
selectAsset(ASSET_EURO);
@@ -69,126 +56,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
});
it('insufficient funds', () => {
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
// 1001-DEPO-005
// Deposit amount is valid, but less than approved. This will always be the case because our
// CI wallet wont have approved any assets
cy.get(amountField)
.clear()
.type('850')
.type('100')
.next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Insufficient amount in Ethereum wallet');
});
});
describe('deposit actions', { tags: '@smoke' }, () => {
before(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.wait('@MarketsCandles');
cy.getByTestId('dialog-close').click();
});
it('Deposit to trade is visble', () => {
cy.getByTestId('Collateral').click();
cy.contains('[data-testid="deposit"]', 'Deposit to trade').should(
'be.visible'
);
cy.contains('[data-testid="deposit"]', 'Deposit to trade').click();
connectEthereumWallet('MetaMask');
cy.getByTestId('deposit-submit').should('be.visible');
});
});
function mockWeb3DepositCalls({
allowance,
depositLifetimeLimit,
balance,
deposited,
dps,
}: {
allowance: string;
depositLifetimeLimit: string;
balance: string;
deposited: string;
dps: number;
}) {
const assetContractAddress = '0x0158031158bb4df2ad02eaa31e8963e84ea978a4';
const collateralBridgeAddress = '0x7fe27d970bc8afc3b11cc8d9737bfb66b1efd799';
const toResult = (value: string, dps: number) => {
const rawValue = removeDecimal(value, dps);
return ethers.utils.hexZeroPad(
ethers.utils.hexlify(parseInt(rawValue)),
32
);
};
cy.intercept('POST', 'http://localhost:8545', (req) => {
// Mock chainId call
if (req.body.method === 'eth_chainId') {
req.alias = 'eth_chainId';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: '0xaa36a7', // 11155111 for sepolia chain id
});
}
// Mock deposited amount
if (req.body.method === 'eth_getStorageAt') {
req.alias = 'eth_getStorageAt';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(deposited, dps),
});
}
if (req.body.method === 'eth_call') {
// Mock approved amount for asset on collateral bridge
if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0xdd62ed3e000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f0000000000000000000000007fe27d970bc8afc3b11cc8d9737bfb66b1efd799'
) {
req.alias = 'eth_call_allowance';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(allowance, dps),
});
}
// Mock balance of asset in Ethereum wallet
else if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0x70a08231000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f'
) {
req.alias = 'eth_call_balanceOf';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(balance, dps),
});
}
// Mock deposit lifetime limit
else if (
req.body.params[0].to === collateralBridgeAddress &&
req.body.params[0].data ===
'0x354a897a0000000000000000000000000158031158bb4df2ad02eaa31e8963e84ea978a4'
) {
req.alias = 'eth_call_get_deposit_maximum'; // deposit lifetime limit
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(depositLifetimeLimit, dps),
});
}
}
});
}
@@ -29,7 +29,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(0, 'Mark Price', '0.05749');
validateMarketDataRow(1, 'Best Bid Price', '6.81765 ');
validateMarketDataRow(2, 'Best Offer Price', '6.81769 ');
validateMarketDataRow(3, 'Quote Unit', 'BTC');
});
it('market volume displayed', () => {
@@ -169,9 +168,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
it('liquidity price range displayed', () => {
cy.getByTestId(marketTitle).contains('Liquidity price range').click();
validateMarketDataRow(0, 'Liquidity Price Range', '2.00% of mid price');
validateMarketDataRow(1, 'Lowest Price', '0.05634 BTC');
validateMarketDataRow(2, 'Highest Price', '0.05864 BTC');
validateMarketDataRow(0, 'Liquidity Price Range', '2.00%');
validateMarketDataRow(1, 'LP Volume Min', '0.05634 tBTC');
validateMarketDataRow(2, 'LP Volume Max', '0.05864 tBTC');
});
it('oracle displayed', () => {
+4 -10
View File
@@ -4,7 +4,6 @@ import { marketsQuery } from '@vegaprotocol/mock';
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
const dialogCloseBtn = 'dialog-close';
const popoverTrigger = 'popover-trigger';
describe('markets table', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -174,13 +173,8 @@ describe('markets table', { tags: '@smoke' }, () => {
});
function openMarketDropDown() {
cy.getByTestId(dialogCloseBtn).then((button) => {
if (button.is(':visible')) {
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
cy.getByTestId(dialogCloseBtn).click();
}
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
cy.getByTestId(popoverTrigger).click({ force: true });
cy.contains('Loading market data...').should('not.exist');
});
cy.getByTestId(dialogCloseBtn).should('be.visible');
cy.getByTestId(dialogCloseBtn).click();
cy.getByTestId('popover-trigger').click();
cy.contains('Loading market data...').should('not.exist');
}
@@ -6,7 +6,6 @@ beforeEach(() => {
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
cy.wait('@MarketsCandles');
cy.getByTestId('dialog-close').click();
});
+4 -35
View File
@@ -17,50 +17,19 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
});
it('can connect', () => {
// Mock authentication
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
body: {
token: 'test-token',
},
});
// Mock getting keys from wallet
cy.intercept('GET', 'https://wallet.testnet.vega.xyz/api/v1/keys', {
body: {
keys: [
{
algorithm: {
name: 'algo',
version: 1,
},
index: 0,
meta: [],
pub: 'HOSTED_PUBKEY',
tainted: false,
},
],
},
});
cy.getByTestId(connectVegaBtn).click();
mockConnectWallet();
cy.contains('Connect Vega wallet');
cy.contains('Hosted Fairground wallet');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-hosted"]')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.getByTestId(form).find('#wallet').click().type('user');
cy.getByTestId(form).find('#passphrase').click().type('pass');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.wait('@walletReq');
cy.getByTestId(manageVegaBtn).should('exist');
});
it('doesnt connect with invalid credentials', () => {
// Mock incorrect username/password
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
body: {
error: 'No wallet',
},
statusCode: 403, // 403 forbidden invalid crednetials
});
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-hosted"]')
@@ -71,7 +40,7 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
cy.getByTestId('form-error').should('have.text', 'Invalid credentials');
});
it('doesnt connect with empty fields', () => {
it('doesnt connect with invalid fields', () => {
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-hosted"]')
@@ -3,14 +3,12 @@ query AccountHistory(
$assetId: ID!
$accountTypes: [AccountType!]
$dateRange: DateRange
$marketIds: [ID!]
) {
balanceChanges(
filter: {
partyIds: [$partyId]
accountTypes: $accountTypes
assetId: $assetId
marketIds: $marketIds
}
dateRange: $dateRange
) {
@@ -8,7 +8,6 @@ export type AccountHistoryQueryVariables = Types.Exact<{
assetId: Types.Scalars['ID'];
accountTypes?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
dateRange?: Types.InputMaybe<Types.DateRange>;
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
}>;
@@ -24,9 +23,9 @@ export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: {
export const AccountHistoryDocument = gql`
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) {
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange) {
balanceChanges(
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds}
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId}
dateRange: $dateRange
) {
edges {
@@ -59,7 +58,6 @@ export const AccountHistoryDocument = gql`
* assetId: // value for 'assetId'
* accountTypes: // value for 'accountTypes'
* dateRange: // value for 'dateRange'
* marketIds: // value for 'marketIds'
* },
* });
*/
@@ -7,7 +7,7 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact';
import type { ChangeEvent } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
import * as Schema from '@vegaprotocol/types';
@@ -25,10 +25,8 @@ import {
import { AccountTypeMapping } from '@vegaprotocol/types';
import { PriceChart } from 'pennant';
import 'pennant/dist/style.css';
import type { Account } from '@vegaprotocol/accounts';
import { accountsDataProvider } from '@vegaprotocol/accounts';
import { accountsOnlyDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import type { Market } from '@vegaprotocol/market-list';
const DateRange = {
RANGE_1D: '1D',
@@ -99,7 +97,7 @@ const AccountHistoryManager = ({
);
const { data: accounts } = useDataProvider({
dataProvider: accountsDataProvider,
dataProvider: accountsOnlyDataProvider,
variables: variablesForOneTimeQuery,
skip: !pubKey,
});
@@ -120,45 +118,6 @@ const AccountHistoryManager = ({
const [range, setRange] = useState<typeof DateRange[keyof typeof DateRange]>(
DateRange.RANGE_1M
);
const [market, setMarket] = useState<Market | null>(null);
const marketFilterCb = useCallback(
(item: Market) =>
!asset?.id ||
item.tradableInstrument.instrument.product.settlementAsset.id ===
asset?.id,
[asset?.id]
);
const markets = useMemo<Market[] | null>(() => {
const arr =
accounts
?.filter((item: Account) => Boolean(item && item.market))
.map<Market>((item) => item.market as Market) ?? null;
return arr
? Array.from(
new Set(
arr
.filter(marketFilterCb)
.sort((a, b) =>
a.tradableInstrument.instrument.code.localeCompare(
b.tradableInstrument.instrument.code
)
)
)
)
: null;
}, [accounts, marketFilterCb]);
const resolveMarket = useCallback(
(m: Market) => {
setMarket(m);
const newAssetId =
m.tradableInstrument.instrument.product.settlementAsset.id;
const newAsset = assets.find((item) => item.id === newAssetId);
if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) {
setAsset(newAsset);
}
},
[asset, assets]
);
const variables = useMemo(
() => ({
@@ -167,110 +126,62 @@ const AccountHistoryManager = ({
accountTypes: accountType ? [accountType] : undefined,
dateRange:
range === 'All' ? undefined : { start: calculateStartDate(range) },
marketIds: market?.id ? [market.id] : undefined,
}),
[pubKey, asset, accountType, range, market?.id]
[pubKey, asset, accountType, range]
);
const { data } = useAccountHistoryQuery({
variables,
skip: !asset || !pubKey,
});
const accountTypeMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<DropdownMenuItem
key={type}
onClick={() => setAccountType(type as Schema.AccountType)}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [accountType]);
const assetsMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{asset ? asset.symbol : t('Select asset')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{assets.map((a) => (
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
{a.symbol}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [assets, asset]);
const marketsMenu = useMemo(() => {
return accountType === Schema.AccountType.ACCOUNT_TYPE_MARGIN &&
markets?.length ? (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{markets?.map((m) => (
<DropdownMenuItem key={m.id} onClick={() => resolveMarket(m)}>
{m.tradableInstrument.instrument.code}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : null;
}, [markets, market, accountType, resolveMarket]);
useEffect(() => {
if (
accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN ||
(asset &&
market &&
market.tradableInstrument.instrument.product.settlementAsset.id !==
asset.id)
) {
setMarket(null);
}
}, [accountType, asset, market]);
return (
<div className="h-full w-full flex flex-col gap-8">
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
<div className="flex items-center gap-4 shrink-0">
<>
{accountTypeMenu}
{assetsMenu}
{marketsMenu}
</>
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<DropdownMenuItem
key={type}
onClick={() => setAccountType(type as Schema.AccountType)}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{asset ? asset.symbol : t('Select asset')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{assets.map((a) => (
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
{a.symbol}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="pt-1 justify-items-end">
<Toggle
@@ -123,7 +123,7 @@ export const MarketLiquiditySupplied = ({
{showMessage && (
<p className="mt-4">
{t(
'The market has sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
'The market is in an auction because there are no priced limit orders, which are required to deploy liquidity commitment pegged orders. This means the order book is empty on one or both sides.'
)}
</p>
)}
@@ -58,7 +58,6 @@ export const AssetDetailsDialog = ({
const { data } = useAssetsDataProvider();
const asset = data?.find((a) => a.id === assetId);
const assetSymbol = asset?.symbol || '';
const content = asset ? (
<div className="my-2">
@@ -98,12 +97,6 @@ export const AssetDetailsDialog = ({
}}
>
{content}
<p className="text-sm mb-4">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
[assetSymbol]
)}
</p>
<div className="w-1/4">
<Button
data-testid="close-asset-details-dialog"
@@ -48,7 +48,7 @@ export const DealTicketEstimates = ({
<DataTitle quoteName={quoteName}>{t('Est. Position Size')}</DataTitle>
<ValueTooltipRow
value={notionalSize}
description={constants.NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName || '')}
description={constants.NOTIONAL_SIZE_TOOLTIP_TEXT}
/>
</div>
)}
@@ -66,7 +66,7 @@ export const DealTicketEstimates = ({
<DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle>
<ValueTooltipRow
value={estMargin}
description={constants.EST_MARGIN_TOOLTIP_TEXT(quoteName || '')}
description={constants.EST_MARGIN_TOOLTIP_TEXT}
/>
</div>
)}
@@ -75,7 +75,7 @@ export const DealTicketEstimates = ({
<DataTitle quoteName={quoteName}>{t('Est. Close out')}</DataTitle>
<ValueTooltipRow
value={estCloseOut}
description={constants.EST_CLOSEOUT_TOOLTIP_TEXT(quoteName || '')}
description={constants.EST_CLOSEOUT_TOOLTIP_TEXT}
/>
</div>
)}
@@ -318,37 +318,33 @@ const SummaryMessage = memo(
}
if (!pubKey) {
return (
<div className="mb-4">
<Notification
testId={'deal-ticket-connect-wallet'}
intent={Intent.Warning}
message={
<p className="text-sm pb-2">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
with {assetSymbol} to start trading in this market.
</p>
}
buttonProps={{
text: t('Connect wallet'),
action: openVegaWalletDialog,
dataTestId: 'order-connect-wallet',
size: 'md',
}}
/>
</div>
<Notification
testId={'deal-ticket-connect-wallet'}
intent={Intent.Warning}
message={
<p className="text-sm pb-2">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
with {assetSymbol} to start trading in this market.
</p>
}
buttonProps={{
text: t('Connect wallet'),
action: openVegaWalletDialog,
dataTestId: 'order-connect-wallet',
size: 'md',
}}
/>
);
}
if (errorMessage === SummaryValidationType.NoCollateral) {
return (
<div className="mb-4">
<ZeroBalanceError
asset={market.tradableInstrument.instrument.product.settlementAsset}
onClickCollateral={onClickCollateral}
/>
</div>
<ZeroBalanceError
asset={market.tradableInstrument.instrument.product.settlementAsset}
onClickCollateral={onClickCollateral}
/>
);
}
@@ -367,11 +363,7 @@ const SummaryMessage = memo(
// If there is no blocking error but user doesn't have enough
// balance render the margin warning, but still allow submission
if (balanceError) {
return (
<div className="mb-4">
<MarginWarning balance={balance} margin={margin} asset={asset} />;
</div>
);
return <MarginWarning balance={balance} margin={margin} asset={asset} />;
}
// Show auction mode warning
@@ -383,15 +375,13 @@ const SummaryMessage = memo(
].includes(marketData.marketTradingMode)
) {
return (
<div className="mb-4">
<Notification
intent={Intent.Warning}
testId={'dealticket-warning-auction'}
message={t(
'Any orders placed now will not trade until the auction ends'
)}
/>
</div>
<Notification
intent={Intent.Warning}
testId={'dealticket-warning-auction'}
message={t(
'Any orders placed now will not trade until the auction ends'
)}
/>
);
}
@@ -9,7 +9,6 @@ import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { createDocsLinks } from '@vegaprotocol/react-helpers';
import { compileGridData } from './compile-grid-data';
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
import BigNumber from 'bignumber.js';
type TradingModeTooltipProps = {
marketId?: string;
@@ -116,21 +115,13 @@ export const TradingModeTooltip = ({
case Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION: {
switch (trigger) {
case Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY: {
const notEnoughLiquidity = new BigNumber(
marketData.suppliedStake || 0
).isLessThan(marketData.targetStake || 0);
return (
<section data-testid="trading-mode-tooltip">
<p className={classNames({ 'mb-4': Boolean(compiledGrid) })}>
<span className="mb-2">
{notEnoughLiquidity &&
t(
'This market is in auction until it reaches sufficient liquidity.'
)}
{!notEnoughLiquidity &&
t(
'This market may have sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
)}
<span>
{t(
'This market is in auction until it reaches sufficient liquidity.'
)}
</span>{' '}
{VEGA_DOCS_URL && (
<ExternalLink
+9 -19
View File
@@ -1,27 +1,17 @@
import { t } from '@vegaprotocol/react-helpers';
export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
t(
`A fraction of the notional position size, in the market's settlement asset %s, to cover any potential losses that you may incur.
For example, for a notional size of $500, if the margin requirement is 10%, then the estimated margin would be approximately $50.`,
[settlementAsset]
);
export const EST_MARGIN_TOOLTIP_TEXT = t(
'When opening a position on a futures market, you must post margin to cover any potential losses that you may incur. The margin is typically a fraction of the notional position size. For example, for a notional position size of $500, if the margin requirement is 10%, then the estimated margin would be approximately $50.'
);
export const CONTRACTS_MARGIN_TOOLTIP_TEXT = t(
'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.'
);
export const EST_CLOSEOUT_TOOLTIP_TEXT = (quote: string) =>
t(
`If the price drops below this number, measured in the market price quote unit %s, you will be closed out, based on your current position and account balance.`,
[quote]
);
export const NOTIONAL_SIZE_TOOLTIP_TEXT = (settlementAsset: string) =>
t(
`The notional size represents the position size in the settlement asset %s of the futures contract. This is calculated by multiplying the number of contracts by the prices of the contract.
For example 10 contracts traded at a price of $50 has a notional size of $500.`,
[settlementAsset]
);
export const EST_CLOSEOUT_TOOLTIP_TEXT = t(
'Because you only need to post a fraction of your position size as margin when trading futures, it is possible to obtain leverage meaning your notional position size exceeds your account balance. In this scenario, if the market moves against your position, it will sometimes be necessary to force close your position due to insufficient funds. The estimated close out tells you the price at which that would happen based on current position and account balance.'
);
export const NOTIONAL_SIZE_TOOLTIP_TEXT = t(
'The notional size represents the position size in the settlement asset of the futures contract. The notional size is calculated by multiplying the number of contracts by the price of the contract. For example, ten contracts traded at a price of $50 has a notional size of $500.'
);
export const EST_FEES_TOOLTIP_TEXT = t(
'When you execute a new buy or sell order, you must pay a small amount of commission to the network for doing so. This fee is used to provide income to the node operates of the network and market makers who make prices on the futures market you are trading.'
);
@@ -72,13 +72,13 @@ export const useFeeDealTicketDetails = (
return null;
}, [derivedPrice, order.size, market.decimalPlaces]);
const assetSymbol =
const symbol =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
return useMemo(() => {
return {
market,
assetSymbol,
symbol,
notionalSize,
estMargin,
estCloseOut,
@@ -87,7 +87,7 @@ export const useFeeDealTicketDetails = (
};
}, [
market,
assetSymbol,
symbol,
notionalSize,
estMargin,
estCloseOut,
@@ -98,7 +98,7 @@ export const useFeeDealTicketDetails = (
export interface FeeDetails {
market: Market;
assetSymbol: string;
symbol: string;
notionalSize: string | null;
estMargin: OrderMargin | null;
estCloseOut: string | null;
@@ -106,7 +106,7 @@ export interface FeeDetails {
}
export const getFeeDetailsValues = ({
assetSymbol,
symbol,
notionalSize,
estMargin,
estCloseOut,
@@ -114,7 +114,6 @@ export const getFeeDetailsValues = ({
}: FeeDetails) => {
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const formatValueWithMarketDp = (
value: string | number | null | undefined
): string => {
@@ -133,8 +132,8 @@ export const getFeeDetailsValues = ({
{
label: t('Notional'),
value: formatValueWithMarketDp(notionalSize),
symbol: assetSymbol,
labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol),
symbol,
labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT,
},
{
label: t('Fees'),
@@ -145,31 +144,31 @@ export const getFeeDetailsValues = ({
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
'The most you would be expected to pay in fees, the actual amount may vary.'
)}
</span>
<FeesBreakdown
fees={estMargin?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
symbol={symbol}
decimals={assetDecimals}
/>
</>
),
symbol: assetSymbol,
symbol,
},
{
label: t('Margin'),
value:
estMargin?.margin && `~${formatValueWithAssetDp(estMargin?.margin)}`,
symbol: assetSymbol,
labelDescription: EST_MARGIN_TOOLTIP_TEXT(assetSymbol),
symbol,
labelDescription: EST_MARGIN_TOOLTIP_TEXT,
},
{
label: t('Liquidation'),
value: estCloseOut && `~${formatValueWithMarketDp(estCloseOut)}`,
symbol: market.tradableInstrument.instrument.product.quoteName,
labelDescription: EST_CLOSEOUT_TOOLTIP_TEXT(quoteName),
labelDescription: EST_CLOSEOUT_TOOLTIP_TEXT,
},
];
};
+11 -74
View File
@@ -4,12 +4,10 @@ import type { DepositFormProps } from './deposit-form';
import { DepositForm } from './deposit-form';
import * as Schema from '@vegaprotocol/types';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
jest.mock('@vegaprotocol/wallet');
jest.mock('@vegaprotocol/web3');
jest.mock('@web3-react/core');
const mockConnector = { deactivate: jest.fn() };
@@ -39,8 +37,6 @@ function generateAsset(): AssetFieldsFragment {
let asset: AssetFieldsFragment;
let props: DepositFormProps;
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
const MOCK_VEGA_KEY =
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
beforeEach(() => {
asset = generateAsset();
@@ -93,17 +89,14 @@ describe('Deposit form', () => {
});
});
it('fails when Ethereum wallet not connected', async () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: false,
account: '',
});
it('fails when submitted with invalid ethereum address', async () => {
(useWeb3React as jest.Mock).mockReturnValue({ account: '123' });
render(<DepositForm {...props} />);
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Connect Ethereum wallet')
await screen.findByText('Invalid Ethereum address')
).toBeInTheDocument();
});
@@ -145,7 +138,7 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Amount is above deposit limit')
await screen.findByText('Insufficient amount in Ethereum wallet')
).toBeInTheDocument();
});
@@ -166,7 +159,7 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Amount is above approved amount.')
await screen.findByText('Amount is above approved amount')
).toBeInTheDocument();
});
@@ -200,9 +193,9 @@ describe('Deposit form', () => {
});
});
it('handles deposit approvals', async () => {
it('handles deposit approvals', () => {
const mockUseVegaWallet = useVegaWallet as jest.Mock;
mockUseVegaWallet.mockReturnValue({ pubKey: MOCK_VEGA_KEY });
mockUseVegaWallet.mockReturnValue({ pubKey: null });
const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({
@@ -219,18 +212,13 @@ describe('Deposit form', () => {
/>
);
expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
expect(screen.getByTestId('approve-warning')).toHaveTextContent(
`Deposits of ${asset.symbol} not approved`
);
fireEvent.click(
screen.getByRole('button', { name: `Approve ${asset.symbol}` })
screen.getByText(`Approve ${asset.symbol}`, {
selector: '[type="button"]',
})
);
await waitFor(() => {
expect(props.submitApprove).toHaveBeenCalled();
});
expect(props.submitApprove).toHaveBeenCalled();
});
it('handles submitting a deposit', async () => {
@@ -296,55 +284,4 @@ describe('Deposit form', () => {
render(<DepositForm {...props} />);
expect(await screen.queryAllByTestId('view-asset-details')).toHaveLength(0);
});
it('renders a connect button if Ethereum wallet is not connected', () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: false,
account: '',
});
render(<DepositForm {...props} />);
expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument();
expect(
screen.queryByLabelText('From (Ethereum address)')
).not.toBeInTheDocument();
});
it('renders a disabled input if Ethereum wallet is connected', () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: true,
account: MOCK_ETH_ADDRESS,
});
render(<DepositForm {...props} />);
expect(
screen.queryByRole('button', { name: 'Connect' })
).not.toBeInTheDocument();
const fromInput = screen.getByLabelText('From (Ethereum address)');
expect(fromInput).toHaveValue(MOCK_ETH_ADDRESS);
expect(fromInput).toBeDisabled();
expect(fromInput).toHaveAttribute('readonly');
});
it('prevents submission if you are on the wrong chain', () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: true,
account: MOCK_ETH_ADDRESS,
chainId: 1,
});
(useWeb3ConnectStore as unknown as jest.Mock).mockImplementation(
// eslint-disable-next-line
(selector: (result: ReturnType<typeof useWeb3ConnectStore>) => any) => {
return selector({
desiredChainId: 11155111,
open: jest.fn(),
foo: 'asdf',
});
}
);
render(<DepositForm {...props} />);
expect(screen.getByTestId('chain-error')).toHaveTextContent(
/this app only works on/i
);
});
});
+156 -196
View File
@@ -1,8 +1,8 @@
import type { Asset } from '@vegaprotocol/assets';
import { AssetOption } from '@vegaprotocol/assets';
import {
t,
ethereumAddress,
t,
required,
vegaPublicKey,
minSafe,
@@ -14,19 +14,17 @@ import {
import {
Button,
FormGroup,
Icon,
Input,
InputError,
RichSelect,
Notification,
Intent,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes } from 'react';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useMemo } from 'react';
import type { FieldError } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { DepositLimits } from './deposit-limits';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
@@ -74,8 +72,7 @@ export const DepositForm = ({
isFaucetable,
}: DepositFormProps) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const openDialog = useWeb3ConnectStore((store) => store.open);
const { isActive, account } = useWeb3React();
const { account } = useWeb3React();
const { pubKey } = useVegaWallet();
const {
register,
@@ -86,27 +83,26 @@ export const DepositForm = ({
formState: { errors },
} = useForm<FormFields>({
defaultValues: {
from: account,
to: pubKey ? pubKey : undefined,
asset: selectedAsset?.id || '',
},
});
const onSubmit = async (fields: FormFields) => {
const onDeposit = async (fields: FormFields) => {
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
throw new Error('Invalid asset');
}
if (approved) {
submitDeposit({
assetSource: selectedAsset.source.contractAddress,
amount: fields.amount,
vegaPublicKey: fields.to,
});
} else {
submitApprove();
}
submitDeposit({
assetSource: selectedAsset.source.contractAddress,
amount: fields.amount,
vegaPublicKey: fields.to,
});
};
const amount = useWatch({ name: 'amount', control });
const maxAmount = useMemo(() => {
const maxApproved = allowance ? allowance : new BigNumber(0);
const maxAvailable = balance ? balance : new BigNumber(0);
@@ -137,12 +133,9 @@ export const DepositForm = ({
return minViableAmount;
}, [selectedAsset]);
const approved = allowance && allowance.isGreaterThan(0) ? true : false;
const formState = getFormState(selectedAsset, isActive, approved);
return (
<form
onSubmit={handleSubmit(onSubmit)}
onSubmit={handleSubmit(onDeposit)}
noValidate={true}
data-testid="deposit-form"
>
@@ -150,54 +143,19 @@ export const DepositForm = ({
label={t('From (Ethereum address)')}
labelFor="ethereum-address"
>
<Controller
name="from"
control={control}
rules={{
<Input
id="ethereum-address"
{...register('from', {
validate: {
required: (value) => {
if (!value) return t('Connect Ethereum wallet');
return true;
},
required,
ethereumAddress,
},
}}
defaultValue={account}
render={() => {
if (isActive && account) {
return (
<>
<Input
id="ethereum-address"
value={account}
readOnly={true}
disabled={true}
{...register('from', {
validate: {
required,
ethereumAddress,
},
})}
/>
<DisconnectEthereumButton
onDisconnect={() => {
setValue('from', ''); // clear from value so required ethereum connection validation works
}}
/>
</>
);
}
return (
<Button
onClick={openDialog}
variant="primary"
fill={true}
type="button"
data-testid="connect-eth-wallet-btn"
>
{t('Connect')}
</Button>
);
})}
/>
<EthereumButton
clearAddress={() => {
setValue('from', '');
clearErrors('from');
}}
/>
{errors.from?.message && (
@@ -283,136 +241,148 @@ export const DepositForm = ({
deposited={deposited}
balance={balance}
asset={selectedAsset}
allowance={allowance}
/>
</div>
)}
{formState === 'deposit' && (
<FormGroup label={t('Amount')} labelFor="amount">
<Input
type="number"
autoComplete="off"
id="amount"
{...register('amount', {
validate: {
required,
minSafe: (value) => minSafe(new BigNumber(min))(value),
approved: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.approved)) {
return t('Amount is above approved amount');
}
return true;
},
limit: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.limit)) {
return t('Amount is above deposit limit');
}
return true;
},
balance: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.available)) {
return t('Insufficient amount in Ethereum wallet');
}
return true;
},
maxSafe: (v) => {
return maxSafe(maxAmount.amount)(v);
},
<FormGroup label={t('Amount')} labelFor="amount">
<Input
type="number"
autoComplete="off"
id="amount"
{...register('amount', {
validate: {
required,
minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.available)) {
return t('Insufficient amount in Ethereum wallet');
} else if (value.isGreaterThan(maxAmount.limit)) {
return t('Amount is above temporary deposit limit');
} else if (value.isGreaterThan(maxAmount.approved)) {
return t('Amount is above approved amount');
}
return maxSafe(maxAmount.amount)(v);
},
})}
/>
{errors.amount?.message && (
<AmountError error={errors.amount} submitApprove={submitApprove} />
)}
{selectedAsset && balance && (
<UseButton
onClick={() => {
setValue('amount', balance.toFixed(selectedAsset.decimals));
clearErrors('amount');
}}
>
{t('Use maximum')}
</UseButton>
)}
</FormGroup>
)}
<FormButton selectedAsset={selectedAsset} formState={formState} />
},
})}
/>
{errors.amount?.message && (
<InputError intent="danger" forInput="amount">
{errors.amount.message}
</InputError>
)}
{selectedAsset && balance && (
<UseButton
onClick={() => {
setValue('amount', balance.toFixed(selectedAsset.decimals));
clearErrors('amount');
}}
>
{t('Use maximum')}
</UseButton>
)}
</FormGroup>
<FormButton
selectedAsset={selectedAsset}
amount={new BigNumber(amount || 0)}
allowance={allowance}
onApproveClick={submitApprove}
/>
</form>
);
};
const AmountError = ({
error,
submitApprove,
}: {
error: FieldError;
submitApprove: () => void;
}) => {
if (error.type === 'approved') {
return (
<InputError intent="danger" forInput="amount">
{error.message}.
<button onClick={submitApprove} className="underline ml-2">
{t('Update approve amount')}
</button>
</InputError>
);
}
return (
<InputError intent="danger" forInput="amount">
{error.message}
</InputError>
);
};
interface FormButtonProps {
selectedAsset?: Asset;
formState: ReturnType<typeof getFormState>;
amount: BigNumber;
allowance: BigNumber | undefined;
onApproveClick: () => void;
}
const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
const FormButton = ({
selectedAsset,
amount,
allowance,
onApproveClick,
}: FormButtonProps) => {
const { open, desiredChainId } = useWeb3ConnectStore((store) => ({
open: store.open,
desiredChainId: store.desiredChainId,
}));
const { isActive, chainId } = useWeb3React();
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
const submitText =
formState === 'approve'
? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`)
: t('Deposit');
const invalidChain = isActive && chainId !== desiredChainId;
return (
<>
{formState === 'approve' && (
<div className="mb-2">
<Notification
intent={Intent.Warning}
testId="approve-warning"
message={t(`Deposits of ${selectedAsset?.symbol} not approved`)}
/>
</div>
)}
{invalidChain && (
<div className="mb-2">
<Notification
intent={Intent.Danger}
testId="chain-error"
message={t(
`This app only works on ${getChainName(desiredChainId)}.`
)}
/>
</div>
)}
const approved =
allowance && allowance.isGreaterThan(0) && amount.isLessThan(allowance);
let button = null;
let message: ReactNode = '';
if (!isActive) {
button = (
<Button onClick={open} data-testid="connect-eth-wallet-btn">
{t('Connect Ethereum wallet')}
</Button>
);
} else if (chainId !== desiredChainId) {
const chainName = getChainName(desiredChainId);
message = t(`This app only works on ${chainName}.`);
button = (
<Button
type="submit"
data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'}
variant="primary"
fill={true}
disabled={invalidChain}
disabled={true}
>
{submitText}
{t('Deposit')}
</Button>
</>
);
} else if (!selectedAsset) {
button = (
<Button
type="submit"
data-testid="deposit-submit"
variant="primary"
fill={true}
>
{t('Deposit')}
</Button>
);
} else if (approved) {
message = (
<>
<Icon name="tick" className="mr-2" />
<span>{t('Approved')}</span>
</>
);
button = (
<Button
type="submit"
data-testid="deposit-submit"
variant="primary"
fill={true}
>
{t('Deposit')}
</Button>
);
} else {
message = t(`Deposits of ${selectedAsset.symbol} not approved`);
button = (
<Button
onClick={onApproveClick}
data-testid="deposit-approve-submit"
variant="primary"
fill={true}
>
{t(`Approve ${selectedAsset.symbol}`)}
</Button>
);
}
return (
<div className="flex flex-col gap-4">
{message && <p className="text-center">{message}</p>}
{button}
</div>
);
};
@@ -428,20 +398,21 @@ const UseButton = (props: UseButtonProps) => {
);
};
const DisconnectEthereumButton = ({
onDisconnect,
}: {
onDisconnect: () => void;
}) => {
const { connector } = useWeb3React();
const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => {
const openDialog = useWeb3ConnectStore((state) => state.open);
const { isActive, connector } = useWeb3React();
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
if (!isActive) {
return <UseButton onClick={openDialog}>{t('Connect')}</UseButton>;
}
return (
<UseButton
onClick={() => {
connector.deactivate();
clearAddress();
removeEagerConnector();
onDisconnect();
}}
data-testid="disconnect-ethereum-wallet"
>
@@ -449,14 +420,3 @@ const DisconnectEthereumButton = ({
</UseButton>
);
};
const getFormState = (
selectedAsset: Asset | undefined,
isActive: boolean,
approved: boolean
) => {
if (!selectedAsset) return 'deposit';
if (!isActive) return 'deposit';
if (approved) return 'deposit';
return 'approve';
};
-8
View File
@@ -11,7 +11,6 @@ interface DepositLimitsProps {
deposited: BigNumber;
asset: Asset;
balance?: BigNumber;
allowance?: BigNumber;
}
export const DepositLimits = ({
@@ -19,7 +18,6 @@ export const DepositLimits = ({
deposited,
asset,
balance,
allowance,
}: DepositLimitsProps) => {
const limits = [
{
@@ -46,12 +44,6 @@ export const DepositLimits = ({
rawValue: max.minus(deposited),
value: compactNumber(max.minus(deposited), asset.decimals),
},
{
key: 'ALLOWANCE',
label: t('Approved'),
rawValue: allowance,
value: allowance ? compactNumber(allowance, asset.decimals) : '-',
},
];
return (
@@ -27,13 +27,12 @@ export const AssetProposalNotification = ({
</>
);
return (
<div className="mb-2">
<Notification
intent={Intent.Warning}
message={message}
testId="asset-proposal-notification"
/>
</div>
<Notification
intent={Intent.Warning}
message={message}
testId="asset-proposal-notification"
className="mb-2"
/>
);
}
@@ -34,6 +34,7 @@ export const MarketProposalNotification = ({
intent={Intent.Warning}
message={message}
testId="market-proposal-notification"
className="px-2 py-1"
/>
</div>
);
@@ -42,18 +42,10 @@ export const update: Update<
},
};
if (delta.buy) {
updatedData.depth.buy = updateLevels(
data.depth.buy ?? [],
delta.buy,
false
);
updatedData.depth.buy = updateLevels(data.depth.buy ?? [], delta.buy);
}
if (delta.sell) {
updatedData.depth.sell = updateLevels(
data.depth.sell ?? [],
delta.sell,
true
);
updatedData.depth.sell = updateLevels(data.depth.sell ?? [], delta.sell);
}
updatedData.depth.sequenceNumber = delta.sequenceNumber;
return updatedData;
+43 -51
View File
@@ -26,19 +26,9 @@ export interface OrderbookRowData {
type PartialOrderbookRowData = Pick<OrderbookRowData, 'price' | 'ask' | 'bid'>;
type OrderbookMarketData = Pick<
MarketData,
| 'bestStaticBidPrice'
| 'bestStaticOfferPrice'
| 'indicativePrice'
| 'indicativeVolume'
| 'marketTradingMode'
>;
export type OrderbookData = Partial<OrderbookMarketData> & {
rows: OrderbookRowData[] | null;
midPrice?: string;
};
export type OrderbookData = Partial<
Omit<MarketData, '__typename' | 'market'>
> & { rows: OrderbookRowData[] | null };
export const getPriceLevel = (price: string | bigint, resolution: number) => {
const p = BigInt(price);
@@ -50,18 +40,6 @@ 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)),
@@ -179,15 +157,8 @@ export const compactRows = (
}
orderbookData.push(row);
});
orderbookData.sort((a, b) => {
if (a === b) {
return 0;
}
if (BigInt(a.price) > BigInt(b.price)) {
return -1;
}
return 1;
});
// 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)));
// count cumulative volumes
if (orderbookData.length > 1) {
const maxIndex = orderbookData.length - 1;
@@ -282,6 +253,28 @@ 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
@@ -290,8 +283,7 @@ export const updateCompactedRows = (
*/
export const updateLevels = (
draft: PriceLevelFieldsFragment[],
updates: (PriceLevelFieldsFragment | PriceLevelFieldsFragment)[],
ascending = true
updates: (PriceLevelFieldsFragment | PriceLevelFieldsFragment)[]
) => {
const levels = [...draft];
updates.forEach((update) => {
@@ -303,10 +295,8 @@ export const updateLevels = (
levels[index] = update;
}
} else if (update.volume !== '0') {
index = levels.findIndex((level) =>
ascending
? BigInt(level.price) > BigInt(update.price)
: BigInt(level.price) < BigInt(update.price)
index = levels.findIndex(
(level) => BigInt(level.price) > BigInt(update.price)
);
if (index !== -1) {
levels.splice(index, 0, update);
@@ -356,20 +346,22 @@ 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,
midPrice: ((bestStaticBidPrice + bestStaticOfferPrice) / 2).toString(),
bestStaticBidPrice: bestStaticBidPrice.toString(),
bestStaticOfferPrice: bestStaticOfferPrice.toString(),
indicativePrice: indicativePrice
? getPriceLevel(indicativePrice.toString(), resolution)
: undefined,
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
),
};
};
@@ -1,4 +1,3 @@
import React from 'react';
import throttle from 'lodash/throttle';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Orderbook } from './orderbook';
@@ -9,14 +8,12 @@ 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,
getMidPrice,
getPriceLevel,
mapMarketData,
} from './orderbook-data';
import type { OrderbookData } from './orderbook-data';
import { usePersistedOrderStore } from '@vegaprotocol/orders';
@@ -34,7 +31,6 @@ 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[];
@@ -46,17 +42,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
throttle(() => {
dataRef.current = {
...marketDataRef.current,
indicativePrice: marketDataRef.current?.indicativePrice
? getPriceLevel(
marketDataRef.current.indicativePrice,
resolutionRef.current
)
: undefined,
midPrice: getMidPrice(
rawDataRef.current?.depth.sell,
rawDataRef.current?.depth.buy,
resolution
),
...mapMarketData(marketDataRef.current, resolutionRef.current),
rows:
deltaRef.current.buy.length || deltaRef.current.sell.length
? updateCompactedRows(
@@ -70,16 +56,14 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
deltaRef.current.buy = [];
deltaRef.current.sell = [];
setOrderbookData(dataRef.current);
}, 250)
}, 1000)
);
const update = useCallback(
({
delta: deltas,
data: rawData,
}: {
delta?: MarketDepthUpdateSubscription['marketsDepthUpdate'];
data?: MarketDepthQuery['market'];
}) => {
if (!dataRef.current.rows) {
return false;
@@ -94,7 +78,6 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
if (delta.buy) {
deltaRef.current.buy.push(...delta.buy);
}
rawDataRef.current = rawData;
updateOrderbookData.current();
}
return true;
@@ -151,14 +134,9 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
}
dataRef.current = {
...marketDataRef.current,
indicativePrice: getPriceLevel(
marketDataRef.current.indicativePrice,
resolution
),
midPrice: getMidPrice(data.depth.sell, data.depth.buy, resolution),
...mapMarketData(marketDataRef.current, resolution),
rows: compactRows(data.depth.sell, data.depth.buy, resolution),
};
rawDataRef.current = data;
setOrderbookData(dataRef.current);
return () => {
+11 -11
View File
@@ -92,7 +92,7 @@ describe('Orderbook', () => {
expect(result.getByTestId('scroll').scrollTop).toBe(90 * rowHeight);
});
it('should keep price it the middle', async () => {
it('should 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 + 0.01;
scrollElement.scrollTop = 92 * rowHeight;
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 + 0.01);
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
});
it('should get back to mid price on click', async () => {
it('should 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 = 1;
scrollElement.scrollTop = 0;
fireEvent.scroll(scrollElement);
expect(result.getByTestId('scroll').scrollTop).toBe(1);
expect(result.getByTestId('scroll').scrollTop).toBe(0);
const scrollToMidPriceButton = result.getByTestId('scroll-to-midprice');
fireEvent.click(scrollToMidPriceButton);
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1);
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
});
it('should get back to mid price on resolution change', async () => {
it('should 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 = 1;
scrollElement.scrollTop = 0;
fireEvent.scroll(scrollElement);
expect(result.getByTestId('scroll').scrollTop).toBe(1);
expect(result.getByTestId('scroll').scrollTop).toBe(0);
const resolutionSelect = result.getByTestId(
'resolution'
) as HTMLSelectElement;
@@ -181,6 +181,6 @@ describe('Orderbook', () => {
onResolutionChange={onResolutionChange}
/>
);
expect(result.getByTestId('scroll').scrollTop).toBe(6 * rowHeight);
expect(result.getByTestId('scroll').scrollTop).toBe(5 * rowHeight);
});
});
+157 -125
View File
@@ -1,7 +1,15 @@
import styles from './orderbook.module.scss';
import colors from 'tailwindcss/colors';
import { useEffect, useRef, useState, useCallback, Fragment } from 'react';
import {
useEffect,
useLayoutEffect,
useRef,
useState,
useMemo,
useCallback,
Fragment,
} from 'react';
import classNames from 'classnames';
import {
@@ -13,7 +21,7 @@ import {
} from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
import { OrderbookRow } from './orderbook-row';
import { createRow } from './orderbook-data';
import { createRow, getPriceLevel } from './orderbook-data';
import { Checkbox, Icon, Splash } from '@vegaprotocol/ui-toolkit';
import type { OrderbookData, OrderbookRowData } from './orderbook-data';
@@ -28,7 +36,7 @@ interface OrderbookProps extends OrderbookData {
const HorizontalLine = ({ top, testId }: { top: string; testId: string }) => (
<div
className="absolute border-b border-default inset-x-0 hidden"
className="absolute border-b border-default inset-x-0"
style={{ top }}
data-testid={testId}
/>
@@ -89,12 +97,7 @@ 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
@@ -109,30 +112,30 @@ const getBestStaticBidPriceLinePosition = (
rows: OrderbookRowData[] | null
) => {
let bestStaticBidPriceLinePosition = '';
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) {
if (maxPriceLevel !== '0' && minPriceLevel !== '0') {
if (
bestStaticBidPrice &&
BigInt(bestStaticBidPrice) < BigInt(maxPriceLevel) &&
BigInt(bestStaticBidPrice) > BigInt(minPriceLevel)
) {
if (fillGaps) {
bestStaticBidPriceLinePosition = (
index * rowHeight +
headerPadding -
3
((BigInt(maxPriceLevel) - BigInt(bestStaticBidPrice)) /
BigInt(resolution) +
BigInt(1)) *
BigInt(rowHeight) +
BigInt(1)
).toString();
} else {
const index = rows?.findIndex(
(row) => BigInt(row.price) <= BigInt(bestStaticBidPrice)
);
if (index !== undefined && index !== -1) {
bestStaticBidPriceLinePosition = (
(index + 1) * rowHeight +
1
).toString();
}
}
}
}
@@ -148,7 +151,6 @@ const getBestStaticOfferPriceLinePosition = (
) => {
let bestStaticOfferPriceLinePosition = '';
if (
rows?.length &&
bestStaticOfferPrice &&
BigInt(bestStaticOfferPrice) <= BigInt(maxPriceLevel) &&
BigInt(bestStaticOfferPrice) > BigInt(minPriceLevel)
@@ -157,10 +159,9 @@ const getBestStaticOfferPriceLinePosition = (
bestStaticOfferPriceLinePosition = (
((BigInt(maxPriceLevel) - BigInt(bestStaticOfferPrice)) /
BigInt(resolution) +
BigInt(1)) *
BigInt(2)) *
BigInt(rowHeight) +
BigInt(headerPadding) -
BigInt(3)
BigInt(1)
).toString();
} else {
const index = rows?.findIndex(
@@ -168,9 +169,8 @@ const getBestStaticOfferPriceLinePosition = (
);
if (index !== undefined && index !== -1) {
bestStaticOfferPriceLinePosition = (
(index + 1) * rowHeight +
headerPadding -
3
(index + 2) * rowHeight +
1
).toString();
}
}
@@ -187,7 +187,7 @@ const OrderbookDebugInfo = ({
bestStaticOfferPrice,
maxPriceLevel,
minPriceLevel,
midPrice,
resolution,
}: {
decimalPlaces: number;
numberOfRows: number;
@@ -198,7 +198,7 @@ const OrderbookDebugInfo = ({
bestStaticOfferPrice?: string;
maxPriceLevel: string;
minPriceLevel: string;
midPrice?: string;
resolution: number;
}) => (
<Fragment>
<div
@@ -247,7 +247,16 @@ const OrderbookDebugInfo = ({
decimalPlaces
),
midPrice: addDecimalsFixedFormatNumber(
midPrice ?? '0',
(bestStaticOfferPrice &&
bestStaticBidPrice &&
getPriceLevel(
BigInt(bestStaticOfferPrice) +
(BigInt(bestStaticBidPrice) -
BigInt(bestStaticOfferPrice)) /
BigInt(2),
resolution
)) ??
'0',
decimalPlaces
),
},
@@ -261,7 +270,6 @@ const OrderbookDebugInfo = ({
export const Orderbook = ({
rows,
midPrice,
bestStaticBidPrice,
bestStaticOfferPrice,
marketTradingMode,
@@ -287,36 +295,21 @@ 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 [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 numberOfRows = useMemo(
() => (fillGaps ? getNumberOfRows(rows, resolution) : rows?.length ?? 0),
[rows, resolution, fillGaps]
);
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 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 updateScrollOffset = useCallback(
(scrollTop: number) => {
if (Math.abs(scrollOffset - scrollTop) > marginSize) {
@@ -325,35 +318,23 @@ export const Orderbook = ({
},
[scrollOffset]
);
const onScroll = useCallback(
(event: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
const { scrollTop } = 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);
}
@@ -380,22 +361,24 @@ export const Orderbook = ({
(Number(
(BigInt(maxPriceLevel) - BigInt(price)) / BigInt(resolution)
) +
1) *
rowHeight;
1) * // add one row for sticky header
rowHeight +
rowHeight / 2 -
(viewportHeight % rowHeight);
} else if (rows) {
const index = rows.findIndex(
(row) => BigInt(row.price) <= BigInt(price)
);
if (index !== -1) {
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;
}
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;
}
}
}
@@ -406,13 +389,7 @@ export const Orderbook = ({
(scrollTopRef.current % rowHeight) - (scrollTop % rowHeight);
const priceCenterScrollOffset = Math.max(
0,
Math.min(
scrollTop,
numberOfRows * rowHeight +
headerPadding +
footerPadding +
-viewportHeight
)
Math.min(scrollTop, numberOfRows * rowHeight - viewportHeight)
);
if (scrollTopRef.current !== priceCenterScrollOffset) {
updateScrollOffset(priceCenterScrollOffset);
@@ -433,31 +410,72 @@ export const Orderbook = ({
);
const scrollToMidPrice = useCallback(() => {
if (!midPrice) {
if (!bestStaticOfferPrice || !bestStaticBidPrice) {
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);
}, [midPrice, scrollToPrice]);
}, [
bestStaticOfferPrice,
bestStaticBidPrice,
scrollToPrice,
resolution,
maxPriceLevel,
minPriceLevel,
]);
// adjust scroll position to keep selected price in center
useEffect(() => {
if (priceInCenter.current) {
scrollToPrice(priceInCenter.current);
} else if (lockOnMidPrice && midPrice) {
scrollToPrice(midPrice);
}
}, [midPrice, scrollToPrice, lockOnMidPrice]);
useEffect(() => {
useLayoutEffect(() => {
if (resolutionRef.current !== resolution) {
priceInCenter.current = undefined;
resolutionRef.current = resolution;
setLockOnMidPrice(true);
}
}, [resolution]);
if (priceInCenter.current) {
scrollToPrice(priceInCenter.current);
} else {
scrollToMidPrice();
}
}, [scrollToMidPrice, scrollToPrice, 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(
@@ -491,6 +509,20 @@ 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
@@ -571,16 +603,16 @@ export const Orderbook = ({
</div>
</div>
<div
className={`h-full overflow-auto relative ${styles['scroll']}`}
className={`h-full overflow-auto relative ${styles['scroll']} pt-[26px] pb-[17px]`}
onScroll={onScroll}
ref={scrollElement}
data-testid="scroll"
>
<div
className="relative text-right min-h-full overflow-hidden"
className="relative text-right min-h-full"
style={{
paddingTop,
paddingBottom,
paddingTop: paddingTop,
paddingBottom: paddingBottom,
background: tableBody ? gradientStyles : 'none',
}}
ref={gridElement}
@@ -653,7 +685,7 @@ export const Orderbook = ({
{debug && (
<OrderbookDebugInfo
decimalPlaces={decimalPlaces}
midPrice={midPrice}
resolution={resolution}
numberOfRows={numberOfRows}
viewportHeight={viewportHeight}
lockOnMidPrice={lockOnMidPrice}
@@ -93,9 +93,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
const headerClassName = 'uppercase text-lg';
const dayVolume = calcCandleVolume(market);
const assetSymbol =
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
const quoteUnit =
market?.tradableInstrument.instrument.product?.quoteName || '';
market?.tradableInstrument.instrument.product?.settlementAsset.symbol;
const assetId = useMemo(
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
[market]
@@ -131,27 +129,16 @@ export const Info = ({ market, onSelect }: InfoProps) => {
{
title: t('Market price'),
content: (
<>
<MarketInfoTable
data={{
...pick(
market.data,
'name',
'markPrice',
'bestBidPrice',
'bestOfferPrice'
),
quoteUnit: market.tradableInstrument.instrument.product.quoteName,
}}
decimalPlaces={market.decimalPlaces}
/>
<p className="text-xs">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
[assetSymbol, quoteUnit]
)}
</p>
</>
<MarketInfoTable
data={pick(
market.data,
'name',
'markPrice',
'bestBidPrice',
'bestOfferPrice'
)}
decimalPlaces={market.decimalPlaces}
/>
),
},
{
@@ -202,10 +189,6 @@ export const Info = ({ market, onSelect }: InfoProps) => {
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
const liquidityPriceRange = formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
);
const marketSpecPanels = [
{
title: t('Key details'),
@@ -241,21 +224,13 @@ export const Info = ({ market, onSelect }: InfoProps) => {
{
title: t('Settlement asset'),
content: asset ? (
<>
<AssetDetailsTable
asset={asset}
inline={true}
noBorder={true}
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
/>
<p className="text-xs">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
[assetSymbol, quoteUnit]
)}
</p>
</>
<AssetDetailsTable
asset={asset}
inline={true}
noBorder={true}
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
/>
) : (
<Splash>{t('No data')}</Splash>
),
@@ -367,40 +342,31 @@ export const Info = ({ market, onSelect }: InfoProps) => {
{
title: t('Liquidity price range'),
content: (
<>
<p className="text-xs mb-4">
{`For liquidity orders count towards a commitment they have to be
within either the liquidity or price monitoring bounds (whichever is
tighter).`}
</p>
<p className="text-xs mb-4">
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
price.`}
</p>
<MarketInfoTable
data={{
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
lowestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
highestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
}}
></MarketInfoTable>
</>
<MarketInfoTable
data={{
liquidityPriceRange: formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
),
LPVolumeMin:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${assetSymbol}`,
LPVolumeMax:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${assetSymbol}`,
}}
></MarketInfoTable>
),
},
{
@@ -16,9 +16,6 @@ export const tooltipMapping: Record<string, ReactNode> = {
markPrice: t(
'A concept derived from traditional markets. It is a calculated value for the current market price on a market.'
),
quoteUnit: t(
`The underlying that is being priced by the market, described by the market's oracle.`
),
openInterest: t(
'The volume of all open positions in a given market (the sum of the size of all positions greater than 0).'
),
@@ -101,4 +98,7 @@ export const tooltipMapping: Record<string, ReactNode> = {
`The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.`
),
suppliedStake: t('The current amount of liquidity supplied for this market.'),
liquidityPriceRange: t(
`Percentage move up and down from the mid price, which specifies the range of price levels over which automated liquidity provision orders will be deployed.`
),
};
-1
View File
@@ -8,7 +8,6 @@
"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",
+1 -3
View File
@@ -10,9 +10,7 @@ const MIN_FRACTION_DIGITS = 2;
const MAX_FRACTION_DIGITS = 20;
export function toDecimal(numberOfDecimals: number) {
return new BigNumber(1)
.dividedBy(Math.pow(10, numberOfDecimals))
.toString(10);
return 1 / Math.pow(10, numberOfDecimals);
}
export function toBigNum(
-36
View File
@@ -1,36 +0,0 @@
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);
});
});
-38
View File
@@ -8,41 +8,3 @@ 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;
}
@@ -1,6 +1,6 @@
import { t } from '../i18n';
export const validateAmount = (step: number | string, field: string) => {
export const validateAmount = (step: number, field: string) => {
const [, stepDecimals = ''] = String(step).split('.');
return (value: string) => {
+1 -2
View File
@@ -10,8 +10,7 @@
"outputPath": "dist/libs/tailwindcss-config",
"main": "libs/tailwindcss-config/src/index.js",
"tsConfig": "libs/tailwindcss-config/tsconfig.lib.json",
"assets": ["libs/tailwindcss-config/*.md"],
"buildableProjectDepsInPackageJsonType": "dependencies"
"assets": ["libs/tailwindcss-config/*.md"]
}
},
"lint": {
+1 -2
View File
@@ -21,8 +21,7 @@
"input": ".",
"output": "."
}
],
"buildableProjectDepsInPackageJsonType": "dependencies"
]
}
},
"lint": {
-4
View File
@@ -2761,16 +2761,12 @@ export type PositionUpdate = {
__typename?: 'PositionUpdate';
/** Average entry price for this position */
averageEntryPrice: Scalars['String'];
/** The total amount of profit and loss that was not transferred due to loss socialisation */
lossSocializationAmount: Scalars['String'];
/** Market relating to this position */
marketId: Scalars['ID'];
/** Open volume (int64) */
openVolume: Scalars['String'];
/** The party holding this position */
partyId: Scalars['ID'];
/** Enum set if the position was closed out or orders were removed because party was distressed */
positionStatus: PositionStatus;
/** Realised Profit and Loss (int64) */
realisedPNL: Scalars['String'];
/** Unrealised Profit and Loss (int64) */
-11
View File
@@ -20,7 +20,6 @@ import type {
ValidatorStatus,
VoteValue,
WithdrawalStatus,
DispatchMetric,
} from './__generated__/types';
export const AccountTypeMapping: {
@@ -435,13 +434,3 @@ 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',
};
-1
View File
@@ -8,7 +8,6 @@
"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",
@@ -19,6 +19,7 @@ type NotificationProps = {
size?: ButtonSize;
};
testId?: string;
className?: string;
};
const getIcon = (intent: Intent): IconName => {
@@ -38,6 +39,7 @@ export const Notification = ({
title,
testId,
buttonProps,
className,
}: NotificationProps) => {
return (
<div
@@ -59,7 +61,8 @@ export const Notification = ({
intent === Intent.Warning,
'bg-vega-pink-300 dark:bg-vega-pink-650': intent === Intent.Danger,
},
'border rounded p-2 flex items-start gap-2.5'
'border rounded p-2 flex items-start gap-2.5 my-4',
className
)}
>
<div