Compare commits

..
174 changed files with 139933 additions and 141885 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
@@ -0,0 +1 @@
[]
+15 -1
View File
@@ -1,3 +1,17 @@
{
"hosts": ["https://vega.xprv.io/datanode/query"]
"hosts": [
"https://vega-data-graphql.chorus.one/query",
"https://vega.xprv.io/datanode/query",
"http://nala.mainnet.vega.community:3008/query",
"http://commodum.mainnet.vega.community:3008/query",
"http://lovali.mainnet.vega.community:3008/query",
"http://b-harvest.mainnet.vega.community:3008/query",
"http://staking-facilities.mainnet.vega.community:3008/query",
"http://figment.mainnet.vega.community:3008/query",
"http://nodes-guru.mainnet.vega.community:3008/query",
"http://p2p.mainnet.vega.community:3008/query",
"http://rockaway.mainnet.vega.community:3008/query",
"http://greenfield-one.mainnet.vega.community:3008/query",
"http://ryabina.mainnet.vega.community:3008/query"
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
[]
@@ -0,0 +1 @@
[]
@@ -0,0 +1 @@
[]
@@ -0,0 +1 @@
[]
@@ -0,0 +1 @@
[]
-1
View File
@@ -12,7 +12,6 @@ NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit suppl
NX_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
#Test configuration variables
CYPRESS_FAIRGROUND=false
@@ -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,3 +1,4 @@
const connectPrompt = '[data-testid="eth-connect-prompt"]';
const connectButton = '[data-testid="connect-to-eth-btn"]';
context(
@@ -17,7 +18,7 @@ context(
});
it('should have connect Eth wallet info', function () {
cy.get(connectButton).should('be.visible');
cy.get(connectPrompt).should('be.visible');
});
it('should have connect Eth wallet button', function () {
+1 -1
View File
@@ -13,4 +13,4 @@ last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.
not IE 9-11 # For IE 9-11 support, remove 'not'.
-1
View File
@@ -12,7 +12,6 @@ NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
#Test configuration variables
CYPRESS_FAIRGROUND=false
-1
View File
@@ -15,7 +15,6 @@ NX_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
#Test configuration variables
CYPRESS_FAIRGROUND=false
-1
View File
@@ -9,4 +9,3 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
-1
View File
@@ -10,4 +10,3 @@ NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4a9141bd@o286262.ingest.sentry.io/5882996
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
-1
View File
@@ -6,4 +6,3 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
-1
View File
@@ -7,4 +7,3 @@ NX_VEGA_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
-1
View File
@@ -10,4 +10,3 @@ NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
@@ -1,17 +1,17 @@
import * as Sentry from '@sentry/react';
import { toBigNum } from '@vegaprotocol/react-helpers';
import { useEthereumConfig } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import { useEffect } from 'react';
import { useAppState } from '../../contexts/app-state/app-state-context';
import { useContracts } from '../../contexts/contracts/contracts-context';
import { useGetAssociationBreakdown } from '../../hooks/use-get-association-breakdown';
import { useGetUserBalances } from '../../hooks/use-get-user-balances';
import { useGetUserTrancheBalances } from '../../hooks/use-get-user-tranche-balances';
import { useBalances } from '../../lib/balances/balances-store';
import type { ReactElement } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useListenForStakingEvents as useListenForAssociationEvents } from '../../hooks/use-listen-for-staking-events';
import { useTranches } from '../../lib/tranches/tranches-store';
import { useUserTrancheBalances } from '../../routes/redemption/hooks';
import { useEthereumConfig } from '@vegaprotocol/web3';
interface BalanceManagerProps {
children: ReactElement;
@@ -24,13 +24,7 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => {
const {
appState: { decimals },
} = useAppState();
const updateStoreBalances = useBalances((state) => state.updateBalances);
const setTranchesBalances = useBalances((state) => state.setTranchesBalances);
const getUserBalances = useGetUserBalances(account);
const userTrancheBalances = useUserTrancheBalances(account);
useEffect(() => {
setTranchesBalances(userTrancheBalances);
}, [setTranchesBalances, userTrancheBalances]);
const { updateBalances: updateStoreBalances } = useBalances();
const { config } = useEthereumConfig();
const numberOfConfirmations = config?.confirmations || 0;
@@ -47,10 +41,10 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => {
numberOfConfirmations
);
const getTranches = useTranches((state) => state.getTranches);
useEffect(() => {
getTranches(decimals);
}, [decimals, getTranches]);
const getUserTrancheBalances = useGetUserTrancheBalances(
account || '',
contracts?.vesting
);
const getAssociationBreakdown = useGetAssociationBreakdown(
account || '',
contracts?.staking,
@@ -60,14 +54,50 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => {
// update balances on connect to Ethereum
useEffect(() => {
const updateBalances = async () => {
const balances = await getUserBalances();
if (balances) {
updateStoreBalances(balances);
if (!account || !config) return;
try {
const [b, w, stats, a] = await Promise.all([
contracts.vesting.user_total_all_tranches(account),
contracts.token.balanceOf(account),
contracts.vesting.user_stats(account),
contracts.token.allowance(
account,
config.staking_bridge_contract.address
),
]);
const balance = toBigNum(b, decimals);
const walletBalance = toBigNum(w, decimals);
const lien = toBigNum(stats.lien, decimals);
const allowance = toBigNum(a, decimals);
updateStoreBalances({
balanceFormatted: balance,
walletBalance,
lien,
allowance,
});
} catch (err) {
Sentry.captureException(err);
}
};
updateBalances();
}, [getUserBalances, updateStoreBalances]);
}, [
decimals,
contracts.token,
contracts.vesting,
account,
config,
updateStoreBalances,
]);
// This use effect hook is very expensive and is kept separate to prevent expensive reloading of data.
useEffect(() => {
if (account) {
getUserTrancheBalances();
}
}, [account, getUserTrancheBalances]);
useEffect(() => {
if (account) {
@@ -6,22 +6,27 @@ import {
useAppState,
} from '../../contexts/app-state/app-state-context';
export const EthConnectPrompt = () => {
interface EthConnectPrompProps {
children?: React.ReactNode;
}
export const EthConnectPrompt = ({ children }: EthConnectPrompProps) => {
const { t } = useTranslation();
const { appDispatch } = useAppState();
return (
<Button
variant="primary"
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
fill={true}
data-testid="connect-to-eth-btn"
>
{t('connectEthWallet')}
</Button>
<>
{children}
<Button
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
data-testid="connect-to-eth-btn"
>
{t('connectEthWallet')}
</Button>
</>
);
};
-1
View File
@@ -58,7 +58,6 @@ const envName = windowOrDefault('NX_VEGA_ENV') ?? 'local';
export const ENV = {
// Environment
tranchesServiceUrl: windowOrDefault('NX_TRANCHES_SERVICE_URL'),
dsn: windowOrDefault('NX_SENTRY_DSN'),
urlConnect: TRUTHY.includes(windowOrDefault('NX_ETH_URL_CONNECT')),
explorerUrl: windowOrDefault('NX_VEGA_EXPLORER'),
@@ -1,3 +1,4 @@
import type { Tranche } from '@vegaprotocol/smart-contracts';
import React from 'react';
import type { BigNumber } from '../../lib/bignumber';
@@ -18,7 +19,21 @@ export interface VegaKey {
meta: Array<{ key: string; value: string }> | null;
}
export interface UserTrancheBalance {
/** ID of tranche */
id: number;
/** Users vesting tokens on tranche */
locked: BigNumber;
/** Users vested tokens on tranche */
vested: BigNumber;
}
export interface AppState {
/** Array of tranche objects */
tranches: Tranche[] | null;
/** Number of decimal places of the VEGA token (18 on Mainnet, 5 on Testnet) */
decimals: number;
@@ -37,6 +52,9 @@ export interface AppState {
/** Whether or not the connect to Ethereum wallet overlay is open */
ethConnectOverlay: boolean;
/** The error if one was thrown during retrieval of tranche data */
trancheError: Error | null;
/** Whether or not the mobile drawer is open. Only relevant on screens smaller than 960 */
drawerOpen: boolean;
@@ -53,10 +71,12 @@ export enum AppStateActionType {
SET_TOKEN,
SET_ALLOWANCE,
REFRESH_BALANCES,
SET_TRANCHE_DATA,
SET_VEGA_WALLET_OVERLAY,
SET_VEGA_WALLET_MANAGE_OVERLAY,
SET_ETH_WALLET_OVERLAY,
SET_DRAWER,
SET_TRANCHE_ERROR,
REFRESH_ASSOCIATED_BALANCES,
SET_ASSOCIATION_BREAKDOWN,
SET_TRANSACTION_OVERLAY,
@@ -70,6 +90,14 @@ export type AppStateAction =
totalSupply: BigNumber;
totalAssociated: BigNumber;
}
| {
type: AppStateActionType.SET_TRANCHE_DATA;
tranches: Tranche[];
}
| {
type: AppStateActionType.SET_TRANCHE_ERROR;
error: Error | null;
}
| {
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY;
isOpen: boolean;
@@ -14,9 +14,11 @@ const initialAppState: AppState = {
totalAssociated: new BigNumber(0),
decimals: 0,
totalSupply: new BigNumber(0),
tranches: null,
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
trancheError: null,
drawerOpen: false,
transactionOverlay: false,
bannerMessage: '',
@@ -32,6 +34,17 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
totalAssociated: action.totalAssociated,
};
}
case AppStateActionType.SET_TRANCHE_DATA:
return {
...state,
tranches: action.tranches,
};
case AppStateActionType.SET_TRANCHE_ERROR: {
return {
...state,
trancheError: action.error,
};
}
case AppStateActionType.SET_VEGA_WALLET_OVERLAY: {
return {
...state,
@@ -0,0 +1,666 @@
import parseJSON from 'date-fns/parseJSON';
import { BigNumber } from '../../lib/bignumber';
import type { Tranche } from '@vegaprotocol/smart-contracts';
const json: Tranche[] = [
{
tranche_id: 1,
tranche_start: parseJSON('2022-03-05T00:00:00.000Z'),
tranche_end: parseJSON('2023-06-05T00:00:00.000Z'),
total_added: new BigNumber('3505372.53445'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('3505372.53445'),
deposits: [
{
amount: new BigNumber('187637.95'),
user: '0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0',
tx: '0xc4491908e3347b05f2394ac7e1006f573fe8cbc490a57cd1dadad70785e95024',
},
{
amount: new BigNumber('200000'),
user: '0x93b478148FF792B00076B7EdC89Db1FdE7772079',
tx: '0xcc8fba855a0d965044d4cc587701fe9ee9f5863852cede017382ffe02963d9b8',
},
{
amount: new BigNumber('21666.5743'),
user: '0xB523235B6c7C74DDB26b10E78bFb2d0Cb63Ae289',
tx: '0x8cc5159f3d665dd33a2bdac6e990cf1b65dc18b6b5e37a07b37dd54495a8d33c',
},
{
amount: new BigNumber('200000'),
user: '0x7227e17101E6C70F4dAfC7DDB77BB7D83DdfC1C8',
tx: '0x0cdef6868bd6b977362396fd06525e1e757e827659c5d75cd512db121947e6f7',
},
{
amount: new BigNumber('137998.56'),
user: '0x69eFc5642CfcCB1777bc663433640531F044D1F5',
tx: '0xaa9b3b39836a69f760f5d1d2fdba44ad348b27c8f31eb094ad6f779b39d7949c',
},
{
amount: new BigNumber('16249.93'),
user: '0x006B59DD3bC838A74476c0F4a33C1565831dA0DD',
tx: '0xda9bdc846300600c0d360edcaf4a5ed40fa2586ba7872f2bf68251a669adff0e',
},
],
withdrawals: [],
users: [
{
address: '0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0',
deposits: [
{
amount: new BigNumber('187637.95'),
user: '0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0',
tranche_id: 1,
tx: '0xc4491908e3347b05f2394ac7e1006f573fe8cbc490a57cd1dadad70785e95024',
},
],
withdrawals: [],
total_tokens: new BigNumber('187637.95'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('187637.95'),
},
{
address: '0x1A71e3ED1996CAbB91bB043f880CE963D601707e',
deposits: [
{
amount: new BigNumber('112323.67'),
user: '0x1A71e3ED1996CAbB91bB043f880CE963D601707e',
tranche_id: 1,
tx: '0xaa378d2d0d4d7b964a675bb19f19c4f7401deec6ce66b1bd98ad5b812026e53e',
},
],
withdrawals: [],
total_tokens: new BigNumber('112323.67'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('112323.67'),
},
],
},
{
tranche_id: 2,
tranche_start: parseJSON('2022-06-05T00:00:00.000Z'),
tranche_end: parseJSON('2023-12-05T00:00:00.000Z'),
total_added: new BigNumber('15464357.550320999700000001'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('15464357.550320999700000001'),
deposits: [
{
amount: new BigNumber('1'),
user: '0x7fff551249D223f723557a96a0e1a469C79cC934',
tx: '0x55fa59d71aac37428a5804442c48da677a4426a4e92447919a8199520ce20f53',
},
{
amount: new BigNumber('100000'),
user: '0xe20D4d4fFb165e4b9926467d82d03c0e9ab66D89',
tx: '0xabf2cc96c41c8b4f0cff8d8ff7448e241ff83e296a7b7dea79d9d9868f33b7ad',
},
{
amount: new BigNumber('500000'),
user: '0x5f01A497e4033E4812ba5D494bCBc2220cd510Ed',
tx: '0xe3a1a74378a42eace12c21c24889d8b0da6a3736ca7987720e70f59886caa5a3',
},
{
amount: new BigNumber('59228.95'),
user: '0x1d20f66eF3889aa48Bb4Badbbf993dE965BDb029',
tx: '0xf31eab593b86aac54f5fa9fe24bb26105ff52386be60bc24617dd362df7c6f15',
},
{
amount: new BigNumber('206374.1211'),
user: '0x1b979e8AE3BbbaF96Dd1bbbC0060b360A23f2EBE',
tx: '0x2983daa9acf6da5fba0debc2aaaaf47389b92aea0de0c27b20809fb69a63ce69',
},
{
amount: new BigNumber('200000'),
user: '0xe45993F39183E148bC35BA02ba8C289111181c0f',
tx: '0xbda7b70ab8ac629617e74daaaa5451b04d7830bc0da516e10d533efc2eef1530',
},
],
withdrawals: [
{
amount: new BigNumber('0'),
user: '0x4527F5A12bbbbb7c88c5863F8AB9a708928Fe702',
tx: '0xbaa8632cd162265ca46baaaad746ec3d283a474e344727854d962e057380a51',
},
],
users: [
{
address: '0x7fff551249D223f723557a96a0e1aCCCC79cC934',
deposits: [
{
amount: new BigNumber('1'),
user: '0x7fff551249D223f723557a96a0e1aCCCC79cC934',
tranche_id: 2,
tx: '0x55fa59d71aac37428a0000042c48da677a4426a4e92447919a8199520ce20f53',
},
],
withdrawals: [],
total_tokens: new BigNumber('1'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('1'),
},
{
address: '0xCc5CAFD3daA3bb2c1168521F35d1eBEB6cf7c051',
deposits: [
{
amount: new BigNumber('200000'),
user: '0xCc5CAFD3daA3bb2c1168521F35d1eBEB6cf7c051',
tranche_id: 2,
tx: '0x8168230eb08320a7a874ebeeea20f0def842b8059a2b05a036870e00ca624c88',
},
],
withdrawals: [],
total_tokens: new BigNumber('200000'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('200000'),
},
{
address: '0x9cd59376F896a5F2084232E386A65c17EEA4Fe9f',
deposits: [
{
amount: new BigNumber('200000'),
user: '0x9cd59376F896a5F2084232E386A65c17EEA4Fe9f',
tranche_id: 2,
tx: '0x2eb27449e08a245314faaaaf0d93fafc70733586495f18cf2fb69ef979459efd',
},
],
withdrawals: [],
total_tokens: new BigNumber('200000'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('200000'),
},
{
address: '0xe45993F39183E148bC35BA02ba8aaaa807981caa',
deposits: [
{
amount: new BigNumber('200000'),
user: '0xe45993F39183E148bC35BA02ba8aaaa07981caa',
tranche_id: 2,
tx: '0xbda7b70ab8ac629617e74d33575451b04d7830bc0da516e10d533efc2eef1530',
},
],
withdrawals: [],
total_tokens: new BigNumber('200000'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('200000'),
},
],
},
{
tranche_id: 3,
tranche_start: parseJSON('2021-11-05T00:00:00.000Z'),
tranche_end: parseJSON('2023-05-05T00:00:00.000Z'),
total_added: new BigNumber('14597706.0446472999'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('14445316.74229298796336861823365303'),
deposits: [
{
amount: new BigNumber('129284.449'),
user: '0x26100B2C8168Cb0A6c869a5698265086A3Dfeaaa',
tx: '0xcc67a776a2e3b48864470aaa9e0940c1814663b4fde6df60c1a099a636dcae79',
},
{
amount: new BigNumber('1151405.093'),
user: '0x777Ec2e2beaB6a63c1E763D0dc4120AF60BEe39F',
tx: '0x1237d45a40aacc5dff7f8f69e8a4c0536fd0460b915de576bd3f0fdf5892c0a4',
},
{
amount: new BigNumber('1151073.595'),
user: '0x5CD0Ec63687588817044794bF15d4e37991efAB3',
tx: '0xb4eaca7d8abeaf7e1d9d28b1f1fa09fc1933af0e11bff4b0120dcef7d2dfc64d',
},
{
amount: new BigNumber('54034.5'),
user: '0xc01F2E57554Bb392384feCA6a54c8E3A3Ca94E42',
tx: '0xf33289a2a73a65b132accdddd600a8d2c7556c2d80784d5f8d4eea1ecacf93cc',
},
{
amount: new BigNumber('115049.22'),
user: '0x01a8055A97b461b58ba8e37cd349721FeAe77A8D',
tx: '0xf650c3599b24d01dfa1f3e19b22f870ff8ac8dfac529893f0b22d548c3535eda',
},
],
withdrawals: [
{
amount: new BigNumber('0'),
user: '0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b',
tx: '0x1af894d1f9ce5ea79aa52d180fbff5f30b8b456e43b76ca5d7d73366e422ea37',
},
],
users: [
{
address: '0x26100B2C8168Cb0A6c869a5698265086A3DfeF98',
deposits: [
{
amount: new BigNumber('129284.449'),
user: '0x26100B2C8168Cb0A6c869a5698265086A3DfeF98',
tranche_id: 3,
tx: '0xcc67a776a2e3b48864470eff9e0940c1814663b4fde6df60c1a099a636dcae79',
},
],
withdrawals: [],
total_tokens: new BigNumber('129284.449'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('129284.449'),
},
{
address: '0xd4632B682228Db5f38E2283869AEe8c29ee6Eec8',
deposits: [
{
amount: new BigNumber('44499.2'),
user: '0xd4632B682228Db5f38E2283869AEe8c29ee6Eec8',
tranche_id: 3,
tx: '0x57019840d1ce05e0ef65c45801d6699e5eb1032bd8ad56493594d4866996ea82',
},
],
withdrawals: [],
total_tokens: new BigNumber('44499.2'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('44499.2'),
},
{
address: '0xe2E6F37cb1f1980418012BF69f43910d6Bc73e73',
deposits: [
{
amount: new BigNumber('66748.8'),
user: '0xe2E6F37cb1f1980418012BF69f43910d6Bc73e73',
tranche_id: 3,
tx: '0xd257a3aacd5bdf86cbea0ed3db3697f55dff9092129db6baedacaf00b50af936',
},
],
withdrawals: [],
total_tokens: new BigNumber('66748.8'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('66748.8'),
},
{
address: '0xc01F2E57554Bb392384feCA6a54c8E3A3Ca94E42',
deposits: [
{
amount: new BigNumber('54034.5'),
user: '0xc01F2E57554Bb392384feCA6a54c8E3A3Ca94E42',
tranche_id: 3,
tx: '0xf33289a2a73a65b132accdddd600a8d2c7556c2d80784d5f8d4eea1ecacf93cc',
},
],
withdrawals: [],
total_tokens: new BigNumber('54034.5'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('54034.5'),
},
],
},
{
tranche_id: 4,
tranche_start: parseJSON('2021-10-05T00:00:00.000Z'),
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
total_added: new BigNumber('5198082.8647159303'),
total_removed: new BigNumber('12706.1452878164044708'),
locked_amount: new BigNumber('4849328.20502099651595959714823613'),
deposits: [
{
amount: new BigNumber('110499.5291'),
user: '0xa8679b60612Fb2e19d68964326CA02dCe6a08D08',
tx: '0x9d3432b818054796489848c415af5c523acb16c1540e5865010baf71964c03a7',
},
{
amount: new BigNumber('331498.5873'),
user: '0x39fEc2e2beaB6a63c1E763D0dc4120AF60BEe39F',
tx: '0xdf5c6e44ef0763e785721802704e5fd186fa3964302a566899027447d0dda57b',
},
{
amount: new BigNumber('27624.032275'),
user: '0xF5Fb27b912D987B5b6e02A1B1BE0C1F0740E2c6f',
tx: '0x1860d39ae3e9ad710da9e2a9bf9606eedc1b176fca673473d6854ea91ed8beb5',
},
{
amount: new BigNumber('73666.3527333333'),
user: '0x2895059cB5a492BEd58D1fB22713006EfaD465eA',
tx: '0x260619e2129cc58ae03f6b4ecfc5a6eeab29b5998de69875accb3cb945beed04',
},
],
withdrawals: [
{
amount: new BigNumber('1290.014571009862016'),
user: '0xBc934494675a6ceB639B9EfEe5b9C0f017D35a75',
tx: '0x637c3648ce941a77e08e741f981806a5d56275db6d280f041902386ae8567d06',
},
{
amount: new BigNumber('5197.621879605058623'),
user: '0xafa64cCa337eFEE0AD827F6C2684e69275226e90',
tx: '0xe8b8c1a38d3ae809dcaae54a11adeeba0f46be03924e91d8f251f3f2d48c3553',
},
{
amount: new BigNumber('376.2625308599198808'),
user: '0x1dD2718fd01d05C9F50Fce8Bb723A4C7483A1E15',
tx: '0x78af737235f1123bb1c6a607a0b4b7a3c5ed6859a4e8912f45bb7c812724b1de',
},
{
amount: new BigNumber('4162.301372742020875'),
user: '0xBc934494675a6ceB639B9EfEe5b9C0f017D35a75',
tx: '0x85c7d9979f0e3a3660dc5952732eaf9414f7c28a1c00a9a10f449843f91d85e0',
},
{
amount: new BigNumber('1679.944933599543076'),
user: '0x9058e12e2F32cB1cD4D3123359963D77786477FC',
tx: '0xd92083f2e90e84cb70ef27ac410ed1144200c41b1714fb54d29f5f23ca086ecb',
},
],
users: [
{
address: '0xa8679b60612Fb2e19d68964326CA02dCe6a08D08',
deposits: [
{
amount: new BigNumber('110499.5291'),
user: '0xa8679b60612Fb2e19d68964326CA02dCe6a08D08',
tranche_id: 4,
tx: '0x9d3432b818054796489848c415af5c523acb16c1540e5865010baf71964c03a7',
},
],
withdrawals: [],
total_tokens: new BigNumber('110499.5291'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('110499.5291'),
},
{
address: '0x8767d65677Cabaa2050b764AEf40610f2f9796F5',
deposits: [
{
amount: new BigNumber('1104995.291'),
user: '0x8767d65677Cabaa2050b764AEf40610f2f9796F5',
tranche_id: 4,
tx: '0xc6298f52c173a837abd051ed810b01eb5731be307376b73df6e31b4de39d0122',
},
],
withdrawals: [],
total_tokens: new BigNumber('1104995.291'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('1104995.291'),
},
{
address: '0x91715128a71c9C734CDC20E5EdaaeA02E72e422E',
deposits: [
{
amount: new BigNumber('165749.29365'),
user: '0x91715128a71c9C734CDC20E5EdEEeA02E72e422E',
tranche_id: 4,
tx: '0xf828cea685a0689f27446f02d3376c3afa4398182d3b5bf1c81e949f5965d1c1',
},
],
withdrawals: [],
total_tokens: new BigNumber('165749.29365'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('165749.29365'),
},
{
address: '0x2895059cB5a492BEd58D1fB22713006EfaD465eA',
deposits: [
{
amount: new BigNumber('73666.3527333333'),
user: '0x2895059cB5a492BEd58D1fB22713006EfaD465eA',
tranche_id: 4,
tx: '0x260619e2129cc58ae03f6b4ecfc5a6eeab29b5998de69875accb3cb945beed04',
},
],
withdrawals: [],
total_tokens: new BigNumber('73666.3527333333'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('73666.3527333333'),
},
],
},
{
tranche_id: 23,
tranche_start: parseJSON('2022-04-30T00:00:00.000Z'),
tranche_end: parseJSON('2022-04-30T00:00:00.000Z'),
total_added: new BigNumber('10833.29'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('10833.29'),
deposits: [
{
amount: new BigNumber('10833.29'),
user: '0xF3359E5B89f7804c8c9283781Aaaa33BBd979c9D',
tx: '0x65f904ef34d6992b52f449a709d7a24411a501fd07f90aaa9255eacc994bc229',
},
],
withdrawals: [],
users: [
{
address: '0xF3359E5B89f7804c8c9283781Aaaa33BBd979c9D',
deposits: [
{
amount: new BigNumber('10833.29'),
user: '0xF3359E5B89f7804c8c9283781Aaaa33BBd979c9D',
tranche_id: 23,
tx: '0x65f904ef34d6992b52f449a709d7a24411a501fd07f90aaa9255eacc994bc229',
},
],
withdrawals: [],
total_tokens: new BigNumber('10833.29'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('10833.29'),
},
],
},
{
tranche_id: 24,
tranche_start: parseJSON('2022-09-26T00:00:00.000Z'),
tranche_end: parseJSON('2022-09-26T00:00:00.000Z'),
total_added: new BigNumber('16249.93'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('16249.93'),
deposits: [
{
amount: new BigNumber('16249.93'),
user: '0xcE96670971ec2E1E79D0d96688adbA2FfD6F6C7f',
tx: '0x3dbd991b7914986505d89a7c1562278dffffa2f5f444bdbf5d6bbd838e3d8d5d',
},
],
withdrawals: [],
users: [
{
address: '0xcE96670971ec2E1E79D0d96688adbA2FfD6F6C7f',
deposits: [
{
amount: new BigNumber('16249.93'),
user: '0xcE96670971ec2E1E79D0d96688adbA2FfD6F6C7f',
tranche_id: 24,
tx: '0x3dbd991b7914986505d89a7c1562278dffffa2f5f444bdbf5d6bbd838e3d8d5d',
},
],
withdrawals: [],
total_tokens: new BigNumber('16249.93'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('16249.93'),
},
],
},
{
tranche_id: 25,
tranche_start: parseJSON('2022-02-10T00:00:00.000Z'),
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
total_added: new BigNumber('0'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('0'),
deposits: [],
withdrawals: [],
users: [],
},
{
tranche_id: 26,
tranche_start: parseJSON('2022-02-04T00:00:00.000Z'),
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
total_added: new BigNumber('135173.4239508'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('135173.4239508'),
deposits: [
{
amount: new BigNumber('135173.4239508'),
user: '0xc90eA4d8D214D548221EE3622a8BE1D61f7077A2',
tx: '0x123fb1e293a8246b92d85a47c8d33def9fb6468c7cbb70f1754d78914b12dbf8',
},
],
withdrawals: [],
users: [
{
address: '0x222eA4d8D214D548221EE3622a8BE1D61f7077A2',
deposits: [
{
amount: new BigNumber('135173.4239508'),
user: '0x222eA4d8D214D548221EE3622a8BE1D61f7077A2',
tranche_id: 26,
tx: '0x9fafb1e293a8246b92d85a47c8d33def9fb6468c7cbb70f1754d78914b12dbf8',
},
],
withdrawals: [],
total_tokens: new BigNumber('135173.4239508'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('135173.4239508'),
},
],
},
{
tranche_id: 27,
tranche_start: parseJSON('2022-05-09T00:00:00.000Z'),
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
total_added: new BigNumber('32499.86'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('32499.86'),
deposits: [
{
amount: new BigNumber('32499.86'),
user: '0x1E7c4E57A1dc4dD4bBE81b833e3E437f69619DaB',
tx: '0x25a3dd4852ce8ac1c15fe42123cfab14e4bf8a5c1cb97167cb4d6fe20bd319ae',
},
],
withdrawals: [],
users: [
{
address: '0x3E7c4E57A1dc4dD4bBE81bbEFBe3Eaaaf69619Da9',
deposits: [
{
amount: new BigNumber('32499.86'),
user: '0x3E7c4E57A1dc4dD4bBE81bbEFBe3Eaaaf69619Da9',
tranche_id: 27,
tx: '0x75a3dd4852ce8ac1c15fe42ff6cfab1455bf8a5c1cb97167cb4d6fe20bd319ae',
},
],
withdrawals: [],
total_tokens: new BigNumber('32499.86'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('32499.86'),
},
],
},
{
tranche_id: 28,
tranche_start: parseJSON('2022-04-30T00:00:00.000Z'),
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
total_added: new BigNumber('10833.29'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('10833.29'),
deposits: [
{
amount: new BigNumber('10833.29'),
user: '0xF3359E5B89f7804c8c9283781Aaa133BBd979c9D',
tx: '0x166d235eff44e7bcc63597c0d6e698552e4440fe90ec7cf07a1d510c275cf3e0',
},
],
withdrawals: [],
users: [
{
address: '0x12349E5B89f7804c8c9283781A23133BBd979c22',
deposits: [
{
amount: new BigNumber('10833.29'),
user: '0x12349E5B89f7804c8c9283781A23133BBd979c22',
tranche_id: 28,
tx: '0x166d235eff44e7baa6359712345698552e6150fe90ec7cf07a1d510c275cf3e0',
},
],
withdrawals: [],
total_tokens: new BigNumber('10833.29'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('10833.29'),
},
],
},
{
tranche_id: 29,
tranche_start: parseJSON('2022-09-26T00:00:00.000Z'),
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
total_added: new BigNumber('16249.93'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('16249.93'),
deposits: [
{
amount: new BigNumber('16249.93'),
user: '0xcE96670971ec2E1E79D0d12388adbA2FfD6F6C7f',
tx: '0xa770ab2d05e6c81be46b73ac2326e81a111da4ce55a2d8544bd95b48ad530674',
},
],
withdrawals: [],
users: [
{
address: '0xcE96670971ec2E1E79D0d96688adbA2FfD6F6C7f',
deposits: [
{
amount: new BigNumber('16249.93'),
user: '0xcE96670971ec212379D0d12388adbA2Ffc6F6C7f',
tranche_id: 29,
tx: '0xa220ab2d05e6c81be12373ac2326e81a912da4ce55a2d8544bd95b48ad530674',
},
],
withdrawals: [],
total_tokens: new BigNumber('16249.93'),
withdrawn_tokens: new BigNumber('0'),
remaining_tokens: new BigNumber('16249.93'),
},
],
},
{
tranche_id: 30,
tranche_start: parseJSON('2021-11-01T00:00:00.000Z'),
tranche_end: parseJSON('2022-05-01T00:00:00.000Z'),
total_added: new BigNumber('0'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('0'),
deposits: [],
withdrawals: [],
users: [],
},
{
tranche_id: 31,
tranche_start: parseJSON('2022-02-01T00:00:00.000Z'),
tranche_end: parseJSON('2022-08-01T00:00:00.000Z'),
total_added: new BigNumber('0'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('0'),
deposits: [],
withdrawals: [],
users: [],
},
{
tranche_id: 32,
tranche_start: parseJSON('2022-05-01T00:00:00.000Z'),
tranche_end: parseJSON('2022-11-01T00:00:00.000Z'),
total_added: new BigNumber('0'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('0'),
deposits: [],
withdrawals: [],
users: [],
},
{
tranche_id: 33,
tranche_start: parseJSON('2022-11-01T00:00:00.000Z'),
tranche_end: parseJSON('2023-05-01T00:00:00.000Z'),
total_added: new BigNumber('0'),
total_removed: new BigNumber('0'),
locked_amount: new BigNumber('0'),
deposits: [],
withdrawals: [],
users: [],
},
];
export default json;
@@ -0,0 +1,21 @@
import React from 'react';
import mock from './tranches-mock';
import type { Tranche } from '@vegaprotocol/smart-contracts';
export function useTranches() {
const [tranches, setTranches] = React.useState<Tranche[] | null>(null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
const run = async () => {
try {
setTranches(mock);
} catch (err) {
setError((err as Error).message);
}
};
run();
}, []);
return { tranches, error };
}
@@ -1,41 +0,0 @@
import { useCallback } from 'react';
import * as Sentry from '@sentry/react';
import { toBigNum } from '@vegaprotocol/react-helpers';
import { useContracts } from '../contexts/contracts/contracts-context';
import { useAppState } from '../contexts/app-state/app-state-context';
import { useEthereumConfig } from '@vegaprotocol/web3';
export const useGetUserBalances = (account: string | undefined) => {
const { token, vesting } = useContracts();
const { config } = useEthereumConfig();
const {
appState: { decimals },
} = useAppState();
return useCallback(async () => {
if (!account || !config) return;
try {
const [b, w, stats, a] = await Promise.all([
vesting.user_total_all_tranches(account),
token.balanceOf(account),
vesting.user_stats(account),
token.allowance(account, config.staking_bridge_contract.address),
]);
const balance = toBigNum(b, decimals);
const walletBalance = toBigNum(w, decimals);
const lien = toBigNum(stats.lien, decimals);
const allowance = toBigNum(a, decimals);
return {
balanceFormatted: balance,
walletBalance,
lien,
allowance,
balance,
};
} catch (err) {
Sentry.captureException(err);
return null;
}
}, [account, config, decimals, token, vesting]);
};
@@ -0,0 +1,70 @@
import React from 'react';
import * as Sentry from '@sentry/react';
import type { TokenVesting } from '@vegaprotocol/smart-contracts';
import {
AppStateActionType,
useAppState,
} from '../contexts/app-state/app-state-context';
import { BigNumber } from '../lib/bignumber';
import { useTranches } from './use-tranches';
import { toBigNum } from '@vegaprotocol/react-helpers';
import { useBalances } from '../lib/balances/balances-store';
export const useGetUserTrancheBalances = (
address: string,
vesting: TokenVesting
) => {
const {
appState: { decimals },
appDispatch,
} = useAppState();
const { setTranchesBalances } = useBalances();
const { tranches } = useTranches();
return React.useCallback(async () => {
appDispatch({
type: AppStateActionType.SET_TRANCHE_ERROR,
error: null,
});
try {
if (!tranches) {
return;
}
const userTranches = tranches?.filter((t) =>
t.users.some(
({ address: a }) =>
a && address && a.toLowerCase() === address.toLowerCase()
)
);
const trancheIds = [0, ...userTranches.map((t) => t.tranche_id)];
const promises = trancheIds.map(async (tId) => {
const [t, v] = await Promise.all([
vesting.get_tranche_balance(address, tId),
vesting.get_vested_for_tranche(address, tId),
]);
const total = toBigNum(t, decimals);
const vested = toBigNum(v, decimals);
return {
id: tId,
locked: tId === 0 ? total : total.minus(vested),
vested: tId === 0 ? new BigNumber(0) : vested,
};
});
const trancheBalances = await Promise.all(promises);
setTranchesBalances(trancheBalances);
appDispatch({
type: AppStateActionType.SET_TRANCHE_DATA,
tranches,
});
} catch (e) {
Sentry.captureException(e);
appDispatch({
type: AppStateActionType.SET_TRANCHE_ERROR,
error: e as Error,
});
}
}, [appDispatch, tranches, setTranchesBalances, address, vesting, decimals]);
};
+70
View File
@@ -0,0 +1,70 @@
import { useFetch } from '@vegaprotocol/react-helpers';
import type { Tranche } from '@vegaprotocol/smart-contracts';
import React, { useEffect } from 'react';
import type { Networks } from '@vegaprotocol/environment';
import { useEnvironment } from '@vegaprotocol/environment';
import { BigNumber } from '../lib/bignumber';
const TRANCHES_URLS: { [N in Networks]: string } = {
MAINNET: 'https://static.vega.xyz/assets/mainnet-tranches.json',
MIRROR: 'https://static.vega.xyz/assets/mirror-tranches.json',
TESTNET: 'https://static.vega.xyz/assets/testnet-tranches.json',
SANDBOX: 'https://static.vega.xyz/assets/sandbox-tranches.json',
STAGNET1: 'https://static.vega.xyz/assets/stagnet1-tranches.json',
STAGNET3: 'https://static.vega.xyz/assets/stagnet3-tranches.json',
DEVNET: 'https://static.vega.xyz/assets/devnet-tranches.json',
CUSTOM: 'https://static.vega.xyz/assets/testnet-tranches.json',
};
export function useTranches() {
const { VEGA_ENV } = useEnvironment();
const [tranches, setTranches] = React.useState<Tranche[] | null>(null);
const url = React.useMemo(() => TRANCHES_URLS[VEGA_ENV], [VEGA_ENV]);
const {
state: { data, loading, error },
} = useFetch<Tranche[] | null>(url);
useEffect(() => {
const processedTrances = data
?.map((t) => ({
...t,
tranche_start: new Date(t.tranche_start),
tranche_end: new Date(t.tranche_end),
total_added: new BigNumber(t.total_added),
total_removed: new BigNumber(t.total_removed),
locked_amount: new BigNumber(t.locked_amount),
deposits: t.deposits.map((d) => ({
...d,
amount: new BigNumber(d.amount),
})),
withdrawals: t.withdrawals.map((w) => ({
...w,
amount: new BigNumber(w.amount),
})),
users: t.users.map((u) => ({
...u,
// @ts-ignore - types are incorrect in the SDK lib
deposits: u.deposits.map((d) => ({
...d,
amount: new BigNumber(d.amount),
})),
// @ts-ignore - types are incorrect in the SDK lib
withdrawals: u.withdrawals.map((w) => ({
...w,
amount: new BigNumber(w.amount),
})),
total_tokens: new BigNumber(u.total_tokens),
withdrawn_tokens: new BigNumber(u.withdrawn_tokens),
remaining_tokens: new BigNumber(u.remaining_tokens),
})),
}))
.sort((a: Tranche, b: Tranche) => a.tranche_id - b.tranche_id);
setTranches(processedTrances ? processedTrances : null);
}, [data]);
return {
tranches,
loading,
error,
};
}
+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",
@@ -0,0 +1,21 @@
import type { Tranche } from '@vegaprotocol/smart-contracts';
import { BigNumber } from '../bignumber';
export function generateTranche(id: number): Tranche {
return {
tranche_id: id,
tranche_start: new Date(),
tranche_end: new Date(),
total_added: new BigNumber(0),
total_removed: new BigNumber(0),
locked_amount: new BigNumber(0),
deposits: [],
withdrawals: [],
users: [],
};
}
export function generateTranches(count = 1) {
return new Array(count).fill(null).map((_, i) => generateTranche(i));
}
@@ -1,11 +1,6 @@
import BigNumber from 'bignumber.js';
import { create } from 'zustand';
interface UserTrancheBalance {
id: number;
locked: BigNumber;
vested: BigNumber;
}
import type { UserTrancheBalance } from '../../contexts/app-state/app-state-context';
export interface AssociationBreakdown {
stakingAssociations: { [vegaKey: string]: BigNumber };
@@ -18,8 +13,8 @@ export type BalancesStore = {
balanceFormatted: BigNumber;
totalVestedBalance: BigNumber;
totalLockedBalance: BigNumber;
trancheBalances: UserTrancheBalance[];
walletBalance: BigNumber;
trancheBalances: UserTrancheBalance[];
lien: BigNumber;
walletAssociatedBalance: BigNumber;
vestingAssociatedBalance: BigNumber;
@@ -43,8 +38,8 @@ export const useBalances = create<BalancesStore>((set) => ({
stakingAssociations: {},
vestingAssociations: {},
},
allowance: new BigNumber(0),
trancheBalances: [],
allowance: new BigNumber(0),
totalVestedBalance: new BigNumber(0),
totalLockedBalance: new BigNumber(0),
balanceFormatted: new BigNumber(0),
@@ -1,80 +0,0 @@
import { toBigNum } from '@vegaprotocol/react-helpers';
import type { TrancheServiceResponse } from '@vegaprotocol/smart-contracts';
import type BigNumber from 'bignumber.js';
import create from 'zustand';
import { ENV } from '../../config';
export interface Tranche {
tranche_id: number;
tranche_start: Date;
tranche_end: Date;
total_added: BigNumber;
total_removed: BigNumber;
locked_amount: BigNumber;
users: string[];
}
const URL = `${ENV.tranchesServiceUrl}/tranches/stats`;
export interface UserTrancheBalance {
/** ID of tranche */
id: number;
/** Users vesting tokens on tranche */
locked: BigNumber;
/** Users vested tokens on tranche */
vested: BigNumber;
}
export type TranchesStore = {
tranches: Tranche[] | null;
loading: boolean;
error: Error | null;
getTranches: (decimals: number) => void;
};
const secondsToDate = (seconds: number) => new Date(seconds * 1000);
export const useTranches = create<TranchesStore>((set) => ({
tranches: null,
loading: false,
error: null,
getTranches: async (decimals: number) => {
set({ loading: true, error: null });
try {
const res = await fetch(URL);
const data = (await res.json()) as TrancheServiceResponse;
const now = Math.round(Date.now() / 1000);
const tranches = Object.values(data.tranches)
?.map((t) => {
const tranche_progress =
t.duration !== 0 ? (now - t.cliff_start) / t.duration : 0;
const lockedDecimal = tranche_progress < 0 ? 1 : 1 - tranche_progress;
return {
tranche_id: t.tranche_id,
tranche_start: secondsToDate(t.cliff_start),
tranche_end: secondsToDate(t.cliff_start + t.duration),
total_added: toBigNum(t.initial_balance, decimals),
total_removed: toBigNum(t.initial_balance, decimals).minus(
toBigNum(t.current_balance, decimals)
),
locked_amount: toBigNum(t.initial_balance, decimals).times(
lockedDecimal
),
users: t.users,
};
})
.sort((a, b) => a.tranche_id - b.tranche_id);
set({
tranches,
});
} catch (e) {
set({ error: e as unknown as Error });
} finally {
set({
loading: false,
});
}
},
}));
+1 -1
View File
@@ -4,6 +4,7 @@ import { format } from 'date-fns';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import type { Tranche } from '@vegaprotocol/smart-contracts';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import { useContracts } from '../../contexts/contracts/contracts-context';
@@ -21,7 +22,6 @@ import { TrancheNotFound } from './tranche-not-found';
import { UntargetedClaim } from './untargeted-claim';
import { Verifying } from './verifying';
import type { ClaimAction, ClaimState } from './claim-reducer';
import type { Tranche } from '../../lib/tranches/tranches-store';
interface ClaimFlowProps {
state: ClaimState;
+1 -1
View File
@@ -1,8 +1,8 @@
import { useTranslation } from 'react-i18next';
import { format } from 'date-fns';
import type { Tranche } from '@vegaprotocol/smart-contracts';
import { DATE_FORMAT_LONG } from '../../lib/date-formats';
import type { Tranche } from '../../lib/tranches/tranches-store';
interface ClaimInfoProps {
tranche: Tranche;
+7 -2
View File
@@ -1,5 +1,7 @@
import React from 'react';
import { useAppState } from '../../contexts/app-state/app-state-context';
import { useContracts } from '../../contexts/contracts/contracts-context';
import { useGetUserTrancheBalances } from '../../hooks/use-get-user-tranche-balances';
import { useRefreshBalances } from '../../hooks/use-refresh-balances';
import { useSearchParams } from '../../hooks/use-search-params';
import { ClaimError } from './claim-error';
@@ -11,7 +13,7 @@ import {
initialClaimState,
} from './claim-reducer';
import type { Tranche } from '../../lib/tranches/tranches-store';
import type { Tranche } from '@vegaprotocol/smart-contracts';
const Claim = ({
address,
@@ -21,8 +23,10 @@ const Claim = ({
tranches: Tranche[];
}) => {
const params = useSearchParams();
const { vesting } = useContracts();
const { appState } = useAppState();
const [state, dispatch] = React.useReducer(claimReducer, initialClaimState);
const getUserTrancheBalances = useGetUserTrancheBalances(address, vesting);
const refreshBalances = useRefreshBalances(address);
React.useEffect(() => {
@@ -44,9 +48,10 @@ const Claim = ({
// If the claim has been committed refetch the new VEGA balance
React.useEffect(() => {
if (state.claimStatus === ClaimStatus.Finished && address) {
getUserTrancheBalances();
refreshBalances();
}
}, [address, refreshBalances, state.claimStatus]);
}, [address, getUserTrancheBalances, refreshBalances, state.claimStatus]);
if (state.error) {
return <ClaimError />;
+4 -9
View File
@@ -5,7 +5,7 @@ import { EthConnectPrompt } from '../../components/eth-connect-prompt';
import { Heading } from '../../components/heading';
import { SplashLoader } from '../../components/splash-loader';
import { useDocumentTitle } from '../../hooks/use-document-title';
import { useTranches } from '../../lib/tranches/tranches-store';
import { useTranches } from '../../hooks/use-tranches';
import type { RouteChildProps } from '..';
import Claim from './claim';
import { ClaimRestricted } from './claim-restricted';
@@ -16,11 +16,7 @@ const ClaimIndex = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
const { account } = useWeb3React();
const { tranches, loading, error } = useTranches((state) => ({
loading: state.loading,
error: state.error,
tranches: state.tranches,
}));
const { tranches, loading, error } = useTranches();
if (loading || !tranches) {
return (
@@ -42,14 +38,13 @@ const ClaimIndex = ({ name }: RouteChildProps) => {
if (!account) {
content = (
<>
<EthConnectPrompt>
<p data-testid="eth-connect-prompt">
{t(
"Use the Ethereum wallet you want to send your tokens to. You'll also need enough Ethereum to pay gas."
)}
</p>
<EthConnectPrompt />
</>
</EthConnectPrompt>
);
} else {
content = isRestricted() ? (
@@ -51,9 +51,11 @@ const mockAppState: AppState = {
totalAssociated: new BigNumber('50063005'),
decimals: 18,
totalSupply: new BigNumber(65000000),
tranches: null,
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
trancheError: null,
drawerOpen: false,
transactionOverlay: false,
bannerMessage: '',
@@ -14,9 +14,11 @@ const mockAppState: AppState = {
totalAssociated: new BigNumber('50063005'),
decimals: 18,
totalSupply: mockTotalSupply,
tranches: null,
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
trancheError: null,
drawerOpen: false,
transactionOverlay: false,
bannerMessage: '',
@@ -1,81 +1,56 @@
import { Callout, Intent } from '@vegaprotocol/ui-toolkit';
import { useEffect, useMemo, useState } from 'react';
import { useBalances } from '../../../lib/balances/balances-store';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
import { AddLockedTokenAddress } from '../../../components/add-locked-token';
import { formatNumber } from '../../../lib/format-number';
import { truncateMiddle } from '../../../lib/truncate-middle';
import Routes from '../../routes';
import type { RedemptionState } from '../redemption-reducer';
import { Tranche0Table, TrancheTable } from '../tranche-table';
import { VestingTable } from './vesting-table';
import { useTranches } from '../../../lib/tranches/tranches-store';
import { useGetUserBalances } from '../../../hooks/use-get-user-balances';
import BigNumber from 'bignumber.js';
import { useUserTrancheBalances } from '../hooks';
interface UserBalances {
balanceFormatted: BigNumber;
walletBalance: BigNumber;
lien: BigNumber;
allowance: BigNumber;
balance: BigNumber;
}
export const RedemptionInformation = () => {
const { t } = useTranslation();
const { state, account } = useOutletContext<{
state: RedemptionState;
account: string;
}>();
const navigate = useNavigate();
const tranches = useTranches((state) => state.tranches);
const { address } = useParams<{ address: string }>();
const [userBalances, setUserBalances] = useState<null | UserBalances>();
const getUsersBalances = useGetUserBalances(address);
useEffect(() => {
getUsersBalances().then(setUserBalances);
}, [getUsersBalances]);
const userTrancheBalances = useUserTrancheBalances(address);
const filteredTranches = useMemo(
const { t } = useTranslation();
const {
balanceFormatted,
lien,
totalVestedBalance,
totalLockedBalance,
trancheBalances,
} = useBalances();
const { userTranches } = state;
const filteredTranches = React.useMemo(
() =>
tranches?.filter((tr) => {
const balance = userTrancheBalances.find(
userTranches.filter((tr) => {
const balance = trancheBalances.find(
({ id }) => id.toString() === tr.tranche_id.toString()
);
return (
balance?.locked.isGreaterThan(0) || balance?.vested.isGreaterThan(0)
);
}) || [],
[userTrancheBalances, tranches]
}),
[trancheBalances, userTranches]
);
const { totalLocked, totalVested } = useMemo(() => {
return {
totalLocked: BigNumber.sum.apply(null, [
new BigNumber(0),
...userTrancheBalances.map(({ locked }) => locked),
]),
totalVested: BigNumber.sum.apply(null, [
new BigNumber(0),
...userTrancheBalances.map(({ vested }) => vested),
]),
};
}, [userTrancheBalances]);
const zeroTranche = useMemo(() => {
const zeroTranche = userTrancheBalances.find((t) => t.id === 0);
const zeroTranche = React.useMemo(() => {
const zeroTranche = trancheBalances.find((t) => t.id === 0);
if (zeroTranche && zeroTranche.locked.isGreaterThan(0)) {
return zeroTranche;
}
return null;
}, [userTrancheBalances]);
}, [trancheBalances]);
const isAccountValid = useMemo(
() => address && address.length === 42 && address.startsWith('0x'),
[address]
);
if (!isAccountValid || !address) {
return <div>The address {address} is not a valid Ethereum address</div>;
}
if (!filteredTranches.length || !userBalances) {
if (!filteredTranches.length) {
return (
<section data-testid="redemption-page">
<div className="mb-8">
@@ -104,17 +79,17 @@ export const RedemptionInformation = () => {
{t(
'{{address}} has {{balance}} VEGA tokens in {{tranches}} tranches of the vesting contract.',
{
address: truncateMiddle(address),
balance: formatNumber(userBalances.balanceFormatted),
address: truncateMiddle(account),
balance: formatNumber(balanceFormatted),
tranches: filteredTranches.length,
}
)}
</p>
<div className="mb-24">
<VestingTable
associated={userBalances.lien}
locked={totalLocked}
vested={totalVested}
associated={lien}
locked={totalLockedBalance}
vested={totalVestedBalance}
/>
</div>
{filteredTranches.length ? <h2>{t('Tranche breakdown')}</h2> : null}
@@ -123,7 +98,7 @@ export const RedemptionInformation = () => {
trancheId={0}
total={
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
userTrancheBalances.find(
trancheBalances.find(
({ id }) => id.toString() === zeroTranche.id.toString()
)!.locked
}
@@ -133,25 +108,22 @@ export const RedemptionInformation = () => {
<TrancheTable
key={tr.tranche_id}
tranche={tr}
lien={userBalances.lien}
lien={lien}
locked={
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
userTrancheBalances.find(
trancheBalances.find(
({ id }) => id.toString() === tr.tranche_id.toString()
)!.locked
}
vested={
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
userTrancheBalances.find(
trancheBalances.find(
({ id }) => id.toString() === tr.tranche_id.toString()
)!.vested
}
totalVested={totalVested}
totalLocked={totalLocked}
onClick={() =>
navigate(`${Routes.REDEEM}/${address}/${tr.tranche_id}`)
}
address={address}
totalVested={totalVestedBalance}
totalLocked={totalLockedBalance}
onClick={() => navigate(`/vesting/${tr.tranche_id}`)}
/>
))}
<Callout
@@ -160,7 +132,7 @@ export const RedemptionInformation = () => {
intent={Intent.Warning}
>
<p>{t('Find out more about Staking.')}</p>
<Link to={Routes.VALIDATORS} className="underline text-white">
<Link to="/staking" className="underline text-white">
{t('Stake VEGA tokens')}
</Link>
</Callout>
@@ -12,7 +12,7 @@ export interface VestingTableProps {
}
const VestingTableIndicatorSquare = ({ colour }: { colour: string }) => (
<span className={`bg-${colour} inline-block h-4 w-4 mr-1`} />
<span className={`bg-${colour} inline-block h-12 w-12 mr-4`} />
);
export const VestingTable = ({
@@ -65,17 +65,17 @@ export const VestingTable = ({
</KeyValueTable>
<div className="flex border-white border">
<div
className="bg-vega-pink h-4"
className="bg-vega-pink h-16"
style={{ flex: lockedPercentage.toNumber() }}
/>
<div
className="bg-vega-green h-4"
className="bg-vega-green h-16"
style={{ flex: vestedPercentage.toNumber() }}
/>
</div>
<div className="flex h-1 mt-1">
<div className="flex h-4 mt-4">
<div
className="bg-vega-yellow h-1"
className="bg-vega-yellow h-4"
style={{ flex: stakedPercentage.toNumber() }}
/>
<div
-54
View File
@@ -1,54 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranches } from '../../lib/tranches/tranches-store';
import { useContracts } from '../../contexts/contracts/contracts-context';
import BigNumber from 'bignumber.js';
import { toBigNum } from '@vegaprotocol/react-helpers';
import { useAppState } from '../../contexts/app-state/app-state-context';
export const useUserTrancheBalances = (address: string | undefined) => {
const [userTrancheBalances, setUserTrancheBalances] = useState<
{
id: number;
locked: BigNumber;
vested: BigNumber;
}[]
>([]);
const {
appState: { decimals },
} = useAppState();
const { vesting } = useContracts();
const tranches = useTranches((state) => state.tranches);
const loadUserTrancheBalances = useCallback(async () => {
if (!address) return;
const userTranches =
tranches?.filter((t) =>
t.users.some(
(a) => a && address && a.toLowerCase() === address.toLowerCase()
)
) || [];
const trancheIds = [0, ...userTranches.map((t) => t.tranche_id)];
const promises = trancheIds.map(async (tId) => {
const [t, v] = await Promise.all([
vesting.get_tranche_balance(address, tId),
vesting.get_vested_for_tranche(address, tId),
]);
const total = toBigNum(t, decimals);
const vested = toBigNum(v, decimals);
return {
id: tId,
locked: tId === 0 ? total : total.minus(vested),
vested: tId === 0 ? new BigNumber(0) : vested,
};
});
const trancheBalances = await Promise.all(promises);
setUserTrancheBalances(trancheBalances);
}, [address, decimals, tranches, vesting]);
useEffect(() => {
loadUserTrancheBalances();
}, [loadUserTrancheBalances]);
return userTrancheBalances;
};
@@ -0,0 +1,38 @@
import type { Tranche } from '@vegaprotocol/smart-contracts';
import type { BigNumber } from '../../lib/bignumber';
export interface TrancheBalance {
id: number;
locked: BigNumber;
vested: BigNumber;
}
export interface RedemptionState {
userTranches: Tranche[];
}
export const initialRedemptionState: RedemptionState = {
userTranches: [],
};
export enum RedemptionActionType {
SET_USER_TRANCHES,
}
export type RedemptionAction = {
type: RedemptionActionType.SET_USER_TRANCHES;
userTranches: Tranche[];
};
export function redemptionReducer(
state: RedemptionState,
action: RedemptionAction
): RedemptionState {
switch (action.type) {
case RedemptionActionType.SET_USER_TRANCHES:
return {
...state,
userTranches: action.userTranches,
};
}
}
+56 -85
View File
@@ -1,62 +1,51 @@
import {
Button,
Callout,
FormGroup,
Input,
InputError,
Intent,
Splash,
} from '@vegaprotocol/ui-toolkit';
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import { Outlet } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { EthConnectPrompt } from '../../components/eth-connect-prompt';
import { SplashLoader } from '../../components/splash-loader';
import { useTranches } from '../../lib/tranches/tranches-store';
import { useTranches } from '../../hooks/use-tranches';
import { useBalances } from '../../lib/balances/balances-store';
import RoutesConfig from '../routes';
interface FormFields {
address: string;
}
import {
initialRedemptionState,
RedemptionActionType,
redemptionReducer,
} from './redemption-reducer';
const RedemptionRouter = () => {
const { address } = useParams<{ address: string }>();
const navigate = useNavigate();
const { t } = useTranslation();
const validatePubkey = useCallback(
(value: string) => {
if (!value.startsWith('0x')) {
return t('Address must begin with 0x');
} else if (value.length !== 42) {
return t('Pubkey must be 42 characters in length');
} else if (Number.isNaN(+value)) {
return t('Pubkey must be be valid hex');
}
return true;
},
[t]
const [state, dispatch] = React.useReducer(
redemptionReducer,
initialRedemptionState
);
const { trancheBalances } = useBalances();
const { account } = useWeb3React();
const { tranches, error, loading } = useTranches((state) => ({
loading: state.loading,
error: state.error,
tranches: state.tranches,
}));
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormFields>();
const { tranches, error, loading } = useTranches();
const onSubmit = useCallback(
(fields: FormFields) => {
navigate(`${RoutesConfig.REDEEM}/${fields.address}`);
},
[navigate]
);
React.useEffect(() => {
const run = (address: string) => {
const userTranches = tranches?.filter((t) =>
t.users.some(
({ address: a }) => a.toLowerCase() === address.toLowerCase()
)
);
if (userTranches) {
dispatch({
type: RedemptionActionType.SET_USER_TRANCHES,
userTranches,
});
}
};
if (account) {
run(account);
}
}, [account, tranches]);
if (error) {
return (
@@ -74,48 +63,30 @@ const RedemptionRouter = () => {
);
}
if (!address) {
if (!account) {
return (
<div className="max-w-md">
{!account ? (
<EthConnectPrompt />
) : (
<Button
fill={true}
variant="primary"
onClick={() => navigate(`${RoutesConfig.REDEEM}/${account}`)}
>
{t('View connected Eth Wallet')}
</Button>
)}
<p className="py-4 flex justify-center">{t('OR')}</p>
<form
onSubmit={handleSubmit(onSubmit)}
data-testid="view-connector-form"
>
<FormGroup label={'View Ethereum as user:'} labelFor="address">
<Input
{...register('address', {
required: t('Required'),
validate: validatePubkey,
})}
id="address"
data-testid="address"
type="text"
/>
{errors.address?.message && (
<InputError intent="danger">{errors.address.message}</InputError>
)}
</FormGroup>
<Button data-testid="connect" type="submit" fill={true}>
{t('View Ethereum user')}
</Button>
</form>
</div>
<EthConnectPrompt>
<p data-testid="eth-connect-prompt">
{t(
"Use the Ethereum wallet you want to send your tokens to. You'll also need enough Ethereum to pay gas."
)}
</p>
</EthConnectPrompt>
);
}
return <Outlet />;
if (!trancheBalances.length) {
return (
<>
<Callout>
<p>{t('You have no VEGA tokens currently vesting.')}</p>
</Callout>
<Link to={RoutesConfig.SUPPLY}>{t('viewAllTranches')}</Link>
</>
);
}
return <Outlet context={{ state, account }} />;
};
export default RedemptionRouter;
@@ -33,10 +33,10 @@ export const TrancheItem = ({
}: TrancheItemProps) => {
const { t } = useTranslation();
const labelClasses =
'inline-block uppercase bg-white text-black py-1 px-2 font-mono';
'inline-block uppercase bg-white text-black py-4 px-8 font-mono';
return (
<section data-testid="tranche-item" className="mb-8">
<section data-testid="tranche-item" className="mb-40">
<div className="flex border-b">
{link ? (
<Link to={link}>
@@ -8,7 +8,6 @@ import { formatNumber } from '../../lib/format-number';
import Routes from '../routes';
import { TrancheItem } from './tranche-item';
import { Button } from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
export interface TrancheTableProps {
tranche: {
@@ -23,7 +22,6 @@ export interface TrancheTableProps {
totalLocked: BigNumber;
onClick: () => void;
disabled?: boolean;
address: string | null;
}
export const Tranche0Table = ({
@@ -67,11 +65,9 @@ export const TrancheTable = ({
totalVested,
totalLocked,
disabled = false,
address,
}: TrancheTableProps) => {
const { t } = useTranslation();
const total = vested.plus(locked);
const { account: connectedAddress } = useWeb3React();
const trancheFullyLocked =
tranche.tranche_start.getTime() > new Date().getTime();
const totalAllTranches = totalVested.plus(totalLocked);
@@ -97,20 +93,13 @@ export const TrancheTable = ({
amount: reduceAmount,
}}
components={{
stakeLink: <Link className="underline" to={Routes.VALIDATORS} />,
disassociateLink: (
<Link className="underline" to={Routes.DISASSOCIATE} />
),
stakeLink: <Link to={`/staking`} />,
disassociateLink: <Link to={`/staking/disassociate`} />,
}}
/>
</div>
);
} else if (
!trancheFullyLocked &&
redeemable &&
connectedAddress &&
address === connectedAddress
) {
} else if (!trancheFullyLocked && redeemable) {
message = (
<Button onClick={onClick} disabled={disabled}>
{t('Redeem unlocked VEGA from tranche {{id}}', {
@@ -1,7 +1,7 @@
import { useBalances } from '../../../lib/balances/balances-store';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Link, useParams } from 'react-router-dom';
import { Link, useParams, useOutletContext } from 'react-router-dom';
import { TransactionCallout } from '../../../components/transaction-callout';
import { useContracts } from '../../../contexts/contracts/contracts-context';
@@ -9,36 +9,32 @@ import {
TransactionActionType,
TxState,
} from '../../../hooks/transaction-reducer';
import { useGetUserTrancheBalances } from '../../../hooks/use-get-user-tranche-balances';
import { useRefreshBalances } from '../../../hooks/use-refresh-balances';
import { useTransaction } from '../../../hooks/use-transaction';
import { BigNumber } from '../../../lib/bignumber';
import { formatNumber } from '../../../lib/format-number';
import Routes from '../../routes';
import type { RedemptionState } from '../redemption-reducer';
import { TrancheTable } from '../tranche-table';
import { useTranches } from '../../../lib/tranches/tranches-store';
import { useWeb3React } from '@web3-react/core';
import { EthConnectPrompt } from '../../../components/eth-connect-prompt';
import { useUserTrancheBalances } from '../hooks';
import { useAppState } from '../../../contexts/app-state/app-state-context';
export const RedeemFromTranche = () => {
const { account: address } = useWeb3React();
const { state, address } = useOutletContext<{
state: RedemptionState;
address: string;
}>();
const { vesting } = useContracts();
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
const { lien, totalVestedBalance, totalLockedBalance } = useBalances();
const refreshBalances = useRefreshBalances(address || '');
const { tranches, getTranches } = useTranches((state) => ({
tranches: state.tranches,
getTranches: state.getTranches,
}));
const { lien, totalVestedBalance, trancheBalances, totalLockedBalance } =
useBalances();
const refreshBalances = useRefreshBalances(address);
const getUserTrancheBalances = useGetUserTrancheBalances(address, vesting);
const { id } = useParams<{ id: string }>();
const numberId = Number(id);
const { userTranches } = state;
const tranche = React.useMemo(
() => tranches?.find(({ tranche_id }) => tranche_id === numberId) || null,
[numberId, tranches]
() => userTranches.find(({ tranche_id }) => tranche_id === numberId),
[numberId, userTranches]
);
const {
state: txState,
@@ -46,7 +42,7 @@ export const RedeemFromTranche = () => {
dispatch: txDispatch,
} = useTransaction(() => vesting.withdraw_from_tranche(numberId));
const { token } = useContracts();
const trancheBalances = useUserTrancheBalances(address || '');
const redeemedAmount = React.useMemo(() => {
return (
trancheBalances.find(({ id: bId }) => bId.toString() === id?.toString())
@@ -59,10 +55,10 @@ export const RedeemFromTranche = () => {
// If the claim has been committed refetch the new VEGA balance
React.useEffect(() => {
if (txState.txState === TxState.Complete && address) {
getUserTrancheBalances();
refreshBalances();
getTranches(decimals);
}
}, [address, decimals, getTranches, refreshBalances, txState.txState]);
}, [address, getUserTrancheBalances, refreshBalances, txState.txState]);
const trancheBalance = React.useMemo(() => {
return trancheBalances.find(
@@ -70,10 +66,6 @@ export const RedeemFromTranche = () => {
);
}, [id, trancheBalances]);
if (!address) {
return <EthConnectPrompt />;
}
if (
!tranche ||
tranche.total_removed.isEqualTo(tranche.total_added) ||
@@ -155,7 +147,6 @@ export const RedeemFromTranche = () => {
locked={trancheBalance.locked}
vested={trancheBalance.vested}
onClick={perform}
address={address || ''}
/>
)}
</section>
+6 -11
View File
@@ -300,17 +300,12 @@ const routerConfig = [
element: <LazyRedemption name="Vesting" />,
children: [
{
path: ':address',
children: [
{
index: true,
element: <LazyRedemptionIndex />,
},
{
path: ':id',
element: <LazyRedemptionTranche />,
},
],
index: true,
element: <LazyRedemptionIndex />,
},
{
path: ':id',
element: <LazyRedemptionTranche />,
},
],
},
@@ -16,11 +16,10 @@ export const StakingWalletsContainer = ({
if (!account) {
return (
<>
<EthConnectPrompt>
<p>{t('associateInfo1')}</p>
<p>{t('associateInfo2')}</p>
<EthConnectPrompt />
</>
</EthConnectPrompt>
);
}
@@ -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() + '%';

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