Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b297ffda64 | ||
|
|
b49009e716 | ||
|
|
97a8751ab1 | ||
|
|
43169659a6 | ||
|
|
48cdb44e9d | ||
|
|
0697acdac9 | ||
|
|
92ec7166bc | ||
|
|
56b5214dbf | ||
|
|
94509a29c5 | ||
|
|
4e2b6b2d04 | ||
|
|
c29087cc96 | ||
|
|
5bde096977 | ||
|
|
923dc09eb6 | ||
|
|
4fbdb337dd | ||
|
|
6f4a5b9097 |
@@ -8,7 +8,7 @@ context('Asset page', { tags: '@regression' }, () => {
|
||||
|
||||
it('should be able to see full assets list', () => {
|
||||
cy.getAssets().then((assets) => {
|
||||
Object.values(assets).forEach((asset) => {
|
||||
assets.forEach((asset) => {
|
||||
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,7 @@ context('Asset page', { tags: '@regression' }, () => {
|
||||
});
|
||||
|
||||
cy.getAssets().then((assets) => {
|
||||
Object.values(assets).forEach((asset) => {
|
||||
assets.forEach((asset) => {
|
||||
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,7 @@ context('Asset page', { tags: '@regression' }, () => {
|
||||
|
||||
it('should open details page when clicked on "View details"', () => {
|
||||
cy.getAssets().then((assets) => {
|
||||
Object.values(assets).forEach((asset) => {
|
||||
assets.forEach((asset) => {
|
||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||
.eq(0)
|
||||
.should('contain.text', 'View details');
|
||||
|
||||
@@ -1,303 +1,15 @@
|
||||
context('Validator page', { tags: '@smoke' }, function () {
|
||||
const validatorMenuHeading = 'a[href="/validators"]';
|
||||
const tendermintDataHeader = '[data-testid="tendermint-header"]';
|
||||
const vegaDataHeader = '[data-testid="vega-header"]';
|
||||
const jsonSection = '.language-json';
|
||||
|
||||
before('Visit validators page and obtain data', function () {
|
||||
cy.visit('/');
|
||||
cy.get(validatorMenuHeading).click();
|
||||
cy.get_validators().as('validators');
|
||||
cy.get_nodes().as('nodes');
|
||||
cy.visit('/validators');
|
||||
});
|
||||
|
||||
describe('Verify elements on page', function () {
|
||||
before('Ensure at least two validators are present', function () {
|
||||
assert.isAtLeast(
|
||||
this.validators.length,
|
||||
2,
|
||||
'Ensuring at least two validators exist'
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to see validator page sections', function () {
|
||||
cy.get(vegaDataHeader)
|
||||
.contains('Vega data')
|
||||
.and('is.visible')
|
||||
.next()
|
||||
.within(() => {
|
||||
cy.get(jsonSection).should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(tendermintDataHeader)
|
||||
.contains('Tendermint data')
|
||||
.and('is.visible')
|
||||
.next()
|
||||
.within(() => {
|
||||
cy.get(jsonSection).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to see relevant validator information in tendermint section', function () {
|
||||
cy.get(tendermintDataHeader)
|
||||
.contains('Tendermint data')
|
||||
.next()
|
||||
.within(() => {
|
||||
cy.get(jsonSection)
|
||||
.invoke('text')
|
||||
.convert_string_json_to_js_object()
|
||||
.then((validatorsInJson) => {
|
||||
this.validators.forEach((validator, index) => {
|
||||
const validatorInJson =
|
||||
validatorsInJson.result.validators[index];
|
||||
|
||||
assert.equal(
|
||||
validatorInJson.address,
|
||||
validator.address,
|
||||
`Checking that validator address shown in json matches system data`
|
||||
);
|
||||
cy.contains(validator.address).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
validatorInJson.pub_key.type,
|
||||
validator.pub_key.type,
|
||||
`Checking that validator public key type shown in json matches system data`
|
||||
);
|
||||
cy.contains(validator.pub_key.type).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
validatorInJson.pub_key.value,
|
||||
validator.pub_key.value,
|
||||
`Checking that validator public key value shown in json matches system data`
|
||||
);
|
||||
cy.contains(validator.pub_key.value).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
validatorInJson.voting_power,
|
||||
validator.voting_power,
|
||||
`Checking that validator voting power in json matches system data`
|
||||
);
|
||||
cy.contains(validator.voting_power).should('be.visible');
|
||||
|
||||
// Proposer priority can change frequently mid test
|
||||
// Therefore only checking the field name is present.
|
||||
cy.contains('proposer_priority').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test disabled 2022/11/15 during the 0.62.1 upgrade. The JSON structure changed, and
|
||||
// this test failed. Rather than fix it, it will be replaced when the validator view displays
|
||||
// something useful rather than just dumping out the JSON.
|
||||
xit('should be able to see relevant node information in vega data section', function () {
|
||||
cy.get(vegaDataHeader)
|
||||
.contains('Vega data')
|
||||
.next()
|
||||
.within(() => {
|
||||
cy.get(jsonSection)
|
||||
.invoke('text')
|
||||
.convert_string_json_to_js_object()
|
||||
.then((nodesInJson) => {
|
||||
this.nodes.forEach((node, index) => {
|
||||
const nodeInJson = nodesInJson.edges[index].node;
|
||||
|
||||
// Vegacapsule shows no info or null for following fields:
|
||||
// name, infoURL, avatarUrl, location, epoch data
|
||||
// Therefore, these values remain unchecked.
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.__typename,
|
||||
node.__typename,
|
||||
`Checking that node __typename shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.__typename).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.id,
|
||||
node.id,
|
||||
`Checking that node id shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.id).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.pubkey,
|
||||
node.pubkey,
|
||||
`Checking that node pubkey shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.pubkey).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.tmPubkey,
|
||||
node.tmPubkey,
|
||||
`Checking that node tmPubkey shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.tmPubkey).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.ethereumAddress,
|
||||
node.ethereumAddress,
|
||||
`Checking that node ethereumAddress shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.ethereumAddress).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.stakedByOperator,
|
||||
node.stakedByOperator,
|
||||
`Checking that node stakedByOperator value shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.stakedByOperator).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.stakedByDelegates,
|
||||
node.stakedByDelegates,
|
||||
`Checking that node stakedByDelegates value shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.stakedByDelegates).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.stakedTotal,
|
||||
node.stakedTotal,
|
||||
`Checking that node stakedTotal shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.stakedTotal).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.pendingStake,
|
||||
node.pendingStake,
|
||||
`Checking that node pendingStake shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.pendingStake).should('be.visible');
|
||||
|
||||
assert.equal(
|
||||
nodeInJson.status,
|
||||
node.status,
|
||||
`Checking that node status shown in json matches system data`
|
||||
);
|
||||
cy.contains(node.status).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to see validator page displayed on mobile', function () {
|
||||
cy.common_switch_to_mobile_and_click_toggle();
|
||||
cy.get(validatorMenuHeading).click();
|
||||
cy.get(vegaDataHeader)
|
||||
.contains('Vega data')
|
||||
.and('is.visible')
|
||||
.next()
|
||||
.within(() => {
|
||||
cy.get(jsonSection).should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(tendermintDataHeader)
|
||||
.contains('Tendermint data')
|
||||
.and('is.visible')
|
||||
.next()
|
||||
.within(() => {
|
||||
cy.get(jsonSection).should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(tendermintDataHeader)
|
||||
.contains('Tendermint data')
|
||||
.next()
|
||||
.within(() => {
|
||||
this.validators.forEach((validator) => {
|
||||
cy.contains(validator.address).should('be.visible');
|
||||
cy.contains(validator.pub_key.type).should('be.visible');
|
||||
cy.contains(validator.pub_key.value).should('be.visible');
|
||||
cy.contains(validator.voting_power).should('be.visible');
|
||||
// Proposer priority can change frequently mid test
|
||||
// Therefore only checking the field name is present.
|
||||
cy.contains('proposer_priority').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to switch validator page between light and dark mode', function () {
|
||||
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
|
||||
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
|
||||
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
|
||||
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
|
||||
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
|
||||
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
|
||||
const themeSwitcher = '[data-testid="theme-switcher"]';
|
||||
const jsonFields = '.hljs';
|
||||
const sideMenuBackground = '.absolute';
|
||||
|
||||
// Engage dark mode if not allready set
|
||||
cy.get(sideMenuBackground)
|
||||
.should('have.css', 'background-color')
|
||||
.then((background_color) => {
|
||||
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
|
||||
cy.get(themeSwitcher).click();
|
||||
});
|
||||
|
||||
// Engage white mode
|
||||
cy.get(themeSwitcher).click();
|
||||
|
||||
// White Mode
|
||||
cy.get(validatorMenuHeading)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', whiteThemeSelectedMenuOptionColor);
|
||||
cy.get(jsonFields)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', whiteThemeJsonFieldBackColor);
|
||||
cy.get(sideMenuBackground)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', whiteThemeSideMenuBackgroundColor);
|
||||
|
||||
// Dark Mode
|
||||
cy.get(themeSwitcher).click();
|
||||
cy.get(validatorMenuHeading)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', darkThemeSelectedMenuOptionColor);
|
||||
cy.get(jsonFields)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', darkThemeJsonFieldBackColor);
|
||||
cy.get(sideMenuBackground)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', darkThemeSideMenuBackgroundColor);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('get_validators', () => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:26617/validators`,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
})
|
||||
.its(`body.result.validators`)
|
||||
.then(function (response) {
|
||||
let validators = [];
|
||||
response.forEach((account, index) => {
|
||||
validators[index] = account;
|
||||
});
|
||||
return validators;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('get_nodes', () => {
|
||||
const mutation =
|
||||
'{nodesConnection { edges { node { id name infoUrl avatarUrl pubkey tmPubkey ethereumAddress \
|
||||
location stakedByOperator stakedByDelegates stakedTotal pendingStake \
|
||||
epochData { total offline online __typename } status name __typename}}}}';
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `http://localhost:3028/query`,
|
||||
body: {
|
||||
query: mutation,
|
||||
},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
})
|
||||
.its(`body.data.nodesConnection.edges`)
|
||||
.then(function (response) {
|
||||
let nodes = [];
|
||||
response.forEach((node) => {
|
||||
nodes.push(node);
|
||||
});
|
||||
return nodes;
|
||||
it('should be able to see validator tiles', function () {
|
||||
cy.getNodes().then((nodes) => {
|
||||
nodes.forEach((node) => {
|
||||
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './page-actions';
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
export const PageActions = ({
|
||||
children,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className="flex flex-row items-start gap-1" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import { RouteTitle } from '../route-title';
|
||||
import { PageActions } from './page-actions';
|
||||
|
||||
type PageTitleProps = {
|
||||
/**
|
||||
* The page title
|
||||
* (also sets the document title unless overwritten by
|
||||
* `documentTitle` property)
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The react node that consists of CTA buttons, links, etc.
|
||||
*/
|
||||
actions?: ReactNode;
|
||||
/**
|
||||
* Overwrites the document title
|
||||
*/
|
||||
documentTitle?: Parameters<typeof useDocumentTitle>[0];
|
||||
} & Omit<HTMLAttributes<HTMLHeadingElement>, 'children'>;
|
||||
|
||||
export const PageTitle = ({
|
||||
title,
|
||||
actions,
|
||||
documentTitle,
|
||||
className,
|
||||
...props
|
||||
}: PageTitleProps) => {
|
||||
useDocumentTitle(
|
||||
documentTitle && documentTitle.length > 0 ? documentTitle : [title]
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col md:flex-row gap-1 justify-between content-start mb-8"
|
||||
data-testid="page-title"
|
||||
>
|
||||
<RouteTitle className={classNames('mb-1', className)} {...props}>
|
||||
{title}
|
||||
</RouteTitle>
|
||||
{actions && <PageActions>{actions}</PageActions>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { DATA_SOURCES } from '../config';
|
||||
|
||||
type PubKey = {
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type Validator = {
|
||||
address: string;
|
||||
pub_key: PubKey;
|
||||
voting_power: string;
|
||||
proposer_priority: string;
|
||||
};
|
||||
|
||||
type Result = {
|
||||
block_height: string;
|
||||
validators: Validator[];
|
||||
count: string;
|
||||
total: string;
|
||||
};
|
||||
|
||||
type TendermintValidatorsResponse = {
|
||||
jsonrpc: string;
|
||||
id: number;
|
||||
result: Result;
|
||||
};
|
||||
|
||||
export const useTendermintValidators = (pollInterval?: number) => {
|
||||
const {
|
||||
state: { data, loading, error },
|
||||
refetch,
|
||||
} = useFetch<TendermintValidatorsResponse>(
|
||||
`${DATA_SOURCES.tendermintUrl}/validators`
|
||||
);
|
||||
|
||||
const ref = useRef<TendermintValidatorsResponse | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (data) ref.current = data;
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval =
|
||||
pollInterval &&
|
||||
setInterval(() => {
|
||||
refetch();
|
||||
}, pollInterval);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [pollInterval, refetch]);
|
||||
|
||||
return { data: ref.current, loading, error, refetch };
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
@@ -8,6 +7,7 @@ import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { useState } from 'react';
|
||||
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||
|
||||
export const AssetPage = () => {
|
||||
useDocumentTitle(['Assets']);
|
||||
@@ -22,18 +22,25 @@ export const AssetPage = () => {
|
||||
return (
|
||||
<>
|
||||
<section className="relative">
|
||||
<RouteTitle data-testid="asset-header">{title}</RouteTitle>
|
||||
<PageTitle
|
||||
data-testid="asset-header"
|
||||
title={title}
|
||||
actions={
|
||||
<Button
|
||||
disabled={!data}
|
||||
size="xs"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
{t('View JSON')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<AsyncRenderer
|
||||
noDataMessage={t('Asset not found')}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<div className="absolute top-0 right-0">
|
||||
<Button size="xs" onClick={() => setDialogOpen(true)}>
|
||||
{t('View JSON')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full relative">
|
||||
<AssetDetailsTable asset={data as AssetFieldsFragment} />
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,12 @@ import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { MarketDetails } from '../../components/markets/market-details';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import compact from 'lodash/compact';
|
||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { marketInfoNoCandlesDataProvider } from '@vegaprotocol/market-info';
|
||||
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||
|
||||
export const MarketPage = () => {
|
||||
useScrollToLocation();
|
||||
@@ -40,20 +40,25 @@ export const MarketPage = () => {
|
||||
return (
|
||||
<>
|
||||
<section className="relative">
|
||||
<RouteTitle data-testid="markets-heading">
|
||||
{data?.market?.tradableInstrument.instrument.name}
|
||||
</RouteTitle>
|
||||
<PageTitle
|
||||
data-testid="markets-heading"
|
||||
title={data?.market?.tradableInstrument.instrument.name || ''}
|
||||
actions={
|
||||
<Button
|
||||
disabled={!data?.market}
|
||||
size="xs"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
{t('View JSON')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<AsyncRenderer
|
||||
noDataMessage={t('This chain has no markets')}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<div className="absolute top-0 right-0">
|
||||
<Button size="xs" onClick={() => setDialogOpen(true)}>
|
||||
{t('View JSON')}
|
||||
</Button>
|
||||
</div>
|
||||
<MarketDetails market={data?.market} />
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
|
||||
@@ -9,7 +9,7 @@ import Party from './parties';
|
||||
import { Parties } from './parties/home';
|
||||
import { Party as PartySingle } from './parties/id';
|
||||
import Txs from './txs';
|
||||
import Validators from './validators';
|
||||
import { ValidatorsPage } from './validators';
|
||||
import Genesis from './genesis';
|
||||
import { Block } from './blocks/id';
|
||||
import { Blocks } from './blocks/home';
|
||||
@@ -125,7 +125,7 @@ const validators: Route[] = flags.validators
|
||||
path: Routes.VALIDATORS,
|
||||
name: 'Validators',
|
||||
text: t('Validators'),
|
||||
element: <Validators />,
|
||||
element: <ValidatorsPage />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
@@ -1,47 +1 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import type { TendermintValidatorsResponse } from './tendermint-validator-response';
|
||||
import { useExplorerNodesQuery } from './__generated__/Nodes';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
|
||||
const Validators = () => {
|
||||
const {
|
||||
state: { data: validators },
|
||||
} = useFetch<TendermintValidatorsResponse>(
|
||||
`${DATA_SOURCES.tendermintUrl}/validators`
|
||||
);
|
||||
|
||||
useDocumentTitle(['Validators']);
|
||||
|
||||
const { data } = useExplorerNodesQuery();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="validators-header">{t('Validators')}</RouteTitle>
|
||||
{data ? (
|
||||
<>
|
||||
<SubHeading data-testid="vega-header">{t('Vega data')}</SubHeading>
|
||||
<SyntaxHighlighter data-testid="vega-data" data={data} />
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
{validators ? (
|
||||
<>
|
||||
<SubHeading data-testid="tendermint-header">
|
||||
{t('Tendermint data')}
|
||||
</SubHeading>
|
||||
<SyntaxHighlighter data-testid="tendermint-data" data={validators} />
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Validators;
|
||||
export * from './validators-page';
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export interface PubKey {
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Validator {
|
||||
address: string;
|
||||
pub_key: PubKey;
|
||||
voting_power: string;
|
||||
proposer_priority: string;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
block_height: string;
|
||||
validators: Validator[];
|
||||
count: string;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export interface TendermintValidatorsResponse {
|
||||
jsonrpc: string;
|
||||
id: number;
|
||||
result: Result;
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import { countryCodeToFlagEmoji, t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Button,
|
||||
CopyWithTooltip,
|
||||
ExternalLink,
|
||||
Icon,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Tooltip,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useExplorerNodesQuery } from './__generated__/Nodes';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import compact from 'lodash/compact';
|
||||
import { useTendermintValidators } from '../../hooks/use-tendermint-validators';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
ContractAddressLink,
|
||||
DApp,
|
||||
TOKEN_VALIDATOR,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import classNames from 'classnames';
|
||||
import { NodeStatus, NodeStatusMapping } from '@vegaprotocol/types';
|
||||
|
||||
type RateProps = {
|
||||
value: BigNumber | number | undefined;
|
||||
className?: string;
|
||||
colour?: 'green' | 'blue' | 'pink' | 'orange';
|
||||
asPoint?: boolean;
|
||||
zero?: boolean;
|
||||
};
|
||||
const Rate = ({
|
||||
value,
|
||||
className,
|
||||
colour = 'blue',
|
||||
asPoint = false,
|
||||
}: RateProps) => {
|
||||
const val =
|
||||
typeof value === 'undefined'
|
||||
? new BigNumber(0)
|
||||
: typeof value === 'number'
|
||||
? new BigNumber(value)
|
||||
: value;
|
||||
const bar = asPoint
|
||||
? {
|
||||
right: `${val.times(100).toFixed(2)}%`,
|
||||
}
|
||||
: { width: `${val.times(100).toFixed(2)}%` };
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'overflow-hidden rounded h-[9px] flex w-full bg-vega-light-100 dark:bg-vega-dark-150',
|
||||
{ 'pl-[9px]': asPoint },
|
||||
{
|
||||
'bg-gradient-to-l to-vega-orange-500 dark:to-vega-orange-550 from-vega-light-100 dark:from-vega-dark-150':
|
||||
asPoint && colour === 'orange',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<div
|
||||
className={classNames(
|
||||
'w-[9px] h-[9px] absolute top-0 right-0 transition-all rounded',
|
||||
{
|
||||
'bg-vega-green-550 dark:bg-vega-green-500': colour === 'green',
|
||||
'bg-vega-blue-550 dark:bg-vega-blue-500': colour === 'blue',
|
||||
'bg-vega-pink-550 dark:bg-vega-pink-500': colour === 'pink',
|
||||
'bg-vega-orange-550 dark:bg-vega-orange-500': colour === 'orange',
|
||||
},
|
||||
'bg-vega',
|
||||
className
|
||||
)}
|
||||
style={bar}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ValidatorsPage = () => {
|
||||
useDocumentTitle(['Validators']);
|
||||
|
||||
const { data: tmData } = useTendermintValidators(5000);
|
||||
const { data, loading, error, refetch } = useExplorerNodesQuery();
|
||||
|
||||
const validators = compact(data?.nodesConnection.edges?.map((e) => e?.node));
|
||||
|
||||
// voting power
|
||||
const powers = compact(tmData?.result.validators).map(
|
||||
(v) => new BigNumber(v.voting_power)
|
||||
);
|
||||
const totalVotingPower = BigNumber.sum(...powers);
|
||||
|
||||
// proposer priority
|
||||
const priorities = compact(tmData?.result.validators).map(
|
||||
(v) => new BigNumber(v.proposer_priority)
|
||||
);
|
||||
const absoluteProposerPriority = BigNumber.max(
|
||||
...priorities.map((p) => p.abs())
|
||||
);
|
||||
|
||||
const tmValidators = useMemo(() => {
|
||||
return tmData?.result.validators.map((v) => {
|
||||
const data = {
|
||||
key: v.pub_key.value,
|
||||
votingPower: new BigNumber(v.voting_power),
|
||||
proposerPriority: new BigNumber(v.proposer_priority),
|
||||
};
|
||||
return {
|
||||
...data,
|
||||
votingPowerRatio: data.votingPower.dividedBy(totalVotingPower),
|
||||
proposerPriorityRatio: absoluteProposerPriority
|
||||
.plus(data.proposerPriority)
|
||||
.dividedBy(absoluteProposerPriority.times(2)),
|
||||
};
|
||||
});
|
||||
}, [tmData?.result.validators, totalVotingPower, absoluteProposerPriority]);
|
||||
|
||||
const totalStaked = BigNumber.sum(
|
||||
...validators.map((v) => new BigNumber(v.stakedTotal))
|
||||
);
|
||||
|
||||
const [vegaDialog, setVegaDialog] = useState<boolean>(false);
|
||||
const [tmDialog, setTmDialog] = useState<boolean>(false);
|
||||
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
<PageTitle
|
||||
title={t('Validators')}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
disabled={Boolean(!data)}
|
||||
size="xs"
|
||||
onClick={() => setVegaDialog(true)}
|
||||
>
|
||||
{t('View JSON')}
|
||||
</Button>
|
||||
{
|
||||
<Button
|
||||
disabled={Boolean(!tmData)}
|
||||
size="xs"
|
||||
onClick={() => setTmDialog(true)}
|
||||
>
|
||||
{t('View tendermint as JSON')}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<AsyncRenderer
|
||||
data={validators}
|
||||
loading={loading}
|
||||
error={error}
|
||||
reload={refetch}
|
||||
>
|
||||
<ul className="md:columns-2">
|
||||
{validators.map((v) => {
|
||||
const tm = tmValidators?.find((tmv) => tmv.key === v.tmPubkey);
|
||||
const stakedRatio = new BigNumber(v.stakedTotal).dividedBy(
|
||||
totalStaked
|
||||
);
|
||||
const validatorPage = tokenLink(
|
||||
TOKEN_VALIDATOR.replace(':id', v.id)
|
||||
);
|
||||
const validatorName =
|
||||
v.name && v.name.length > 0 ? v.name : truncateMiddle(v.id);
|
||||
return (
|
||||
<li className="mb-5" key={v.id}>
|
||||
<div
|
||||
data-testid="validator-tile"
|
||||
validator-id={v.id}
|
||||
className="border border-vega-light-200 dark:border-vega-dark-200 rounded p-2 overflow-hidden relative flex gap-2 items-start justify-between"
|
||||
>
|
||||
{v.avatarUrl && (
|
||||
<div className="w-20">
|
||||
<ExternalLink href={validatorPage}>
|
||||
<img
|
||||
className="w-full"
|
||||
src={v.avatarUrl}
|
||||
alt={validatorName}
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full">
|
||||
<h2 className="font-alpha text-2xl">
|
||||
<ExternalLink href={validatorPage}>
|
||||
{validatorName}
|
||||
</ExternalLink>
|
||||
</h2>
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('ID')}</div>
|
||||
<div className="break-all text-xs">{v.id}</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Status')}</div>
|
||||
<div className="break-all text-xs">
|
||||
<span
|
||||
className={classNames('mr-1', {
|
||||
'text-vega-green-550 dark:vega-green-500':
|
||||
v.status === NodeStatus.NODE_STATUS_VALIDATOR,
|
||||
'text-vega-pink-550 dark:vega-pink-500':
|
||||
v.status ===
|
||||
NodeStatus.NODE_STATUS_NON_VALIDATOR,
|
||||
})}
|
||||
>
|
||||
<Icon name="tick-circle" size={3} />
|
||||
</span>
|
||||
{NodeStatusMapping[v.status]}
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Location')}</div>
|
||||
<div>
|
||||
{countryCodeToFlagEmoji(v.location)}{' '}
|
||||
<span className="text-[10px]">{v.location}</span>
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Public key')}</div>
|
||||
<div className="break-all text-xs">{v.pubkey}</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Ethereum address')}</div>
|
||||
<div className="break-all text-xs">
|
||||
<ContractAddressLink address={v.ethereumAddress} />{' '}
|
||||
<CopyWithTooltip text={v.ethereumAddress}>
|
||||
<button title={t('Copy address to clipboard')}>
|
||||
<Icon size={3} name="duplicate" />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Tendermint public key')}</div>
|
||||
<div className="break-all text-xs">{v.tmPubkey}</div>
|
||||
</KeyValueTableRow>
|
||||
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Voting power')}</div>
|
||||
<div className="w-44 text-right">
|
||||
<Rate value={tm?.votingPowerRatio} />
|
||||
<div className="text-[10px] leading-3">
|
||||
{tm?.votingPowerRatio.times(100).toFixed(2)}
|
||||
{'% '}({tm?.votingPower.toString()})
|
||||
</div>
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Proposer priority')}</div>
|
||||
<div className="w-44 text-right">
|
||||
<Rate
|
||||
value={tm?.proposerPriorityRatio}
|
||||
colour="orange"
|
||||
asPoint={true}
|
||||
zero={true}
|
||||
/>
|
||||
<div className="text-[10px] leading-3">
|
||||
{tm?.proposerPriority.toString()}
|
||||
</div>
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Stake share')}</div>
|
||||
<div className="w-44 text-right">
|
||||
<Rate value={stakedRatio} colour="green" />
|
||||
<div className="text-[10px] leading-3">
|
||||
<Tooltip
|
||||
description={
|
||||
<KeyValueTable
|
||||
numerical={true}
|
||||
className="mb-1"
|
||||
>
|
||||
<KeyValueTableRow
|
||||
className="text-xs"
|
||||
noBorder={true}
|
||||
>
|
||||
<div>{t('Staked by operator')}</div>
|
||||
<div>{v.stakedByOperator}</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow
|
||||
className="text-xs"
|
||||
noBorder={true}
|
||||
>
|
||||
<div>{t('Staked by delegates')}</div>
|
||||
<div>{v.stakedByDelegates}</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow
|
||||
className="text-xs"
|
||||
noBorder={true}
|
||||
>
|
||||
<div>{t('Staked (total)')}</div>
|
||||
<div>{v.stakedTotal}</div>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{stakedRatio.times(100).toFixed(2)}%
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
<JsonViewerDialog
|
||||
open={vegaDialog}
|
||||
onChange={(isOpen) => setVegaDialog(isOpen)}
|
||||
title={t('Vega Validators')}
|
||||
content={data}
|
||||
/>
|
||||
<JsonViewerDialog
|
||||
open={tmDialog}
|
||||
onChange={(isOpen) => setTmDialog(isOpen)}
|
||||
title={t('Tendermint Validators')}
|
||||
content={tmData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
+137707
-137650
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const connectPrompt = '[data-testid="eth-connect-prompt"]';
|
||||
const connectButton = '[data-testid="connect-to-eth-btn"]';
|
||||
|
||||
context(
|
||||
@@ -18,7 +17,7 @@ context(
|
||||
});
|
||||
|
||||
it('should have connect Eth wallet info', function () {
|
||||
cy.get(connectPrompt).should('be.visible');
|
||||
cy.get(connectButton).should('be.visible');
|
||||
});
|
||||
|
||||
it('should have connect Eth wallet button', function () {
|
||||
|
||||
@@ -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'.
|
||||
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
|
||||
@@ -15,6 +15,7 @@ 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
|
||||
|
||||
@@ -9,3 +9,4 @@ 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
|
||||
@@ -10,3 +10,4 @@ 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
|
||||
@@ -6,3 +6,4 @@ 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
|
||||
@@ -7,3 +7,4 @@ 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
|
||||
@@ -10,3 +10,4 @@ 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 { useGetUserTrancheBalances } from '../../hooks/use-get-user-tranche-balances';
|
||||
import { useGetUserBalances } from '../../hooks/use-get-user-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,7 +24,13 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => {
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
const { updateBalances: updateStoreBalances } = useBalances();
|
||||
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 { config } = useEthereumConfig();
|
||||
|
||||
const numberOfConfirmations = config?.confirmations || 0;
|
||||
@@ -41,10 +47,10 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => {
|
||||
numberOfConfirmations
|
||||
);
|
||||
|
||||
const getUserTrancheBalances = useGetUserTrancheBalances(
|
||||
account || '',
|
||||
contracts?.vesting
|
||||
);
|
||||
const getTranches = useTranches((state) => state.getTranches);
|
||||
useEffect(() => {
|
||||
getTranches(decimals);
|
||||
}, [decimals, getTranches]);
|
||||
const getAssociationBreakdown = useGetAssociationBreakdown(
|
||||
account || '',
|
||||
contracts?.staking,
|
||||
@@ -54,50 +60,14 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => {
|
||||
// update balances on connect to Ethereum
|
||||
useEffect(() => {
|
||||
const updateBalances = async () => {
|
||||
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);
|
||||
const balances = await getUserBalances();
|
||||
if (balances) {
|
||||
updateStoreBalances(balances);
|
||||
}
|
||||
};
|
||||
|
||||
updateBalances();
|
||||
}, [
|
||||
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]);
|
||||
}, [getUserBalances, updateStoreBalances]);
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
|
||||
@@ -6,27 +6,22 @@ import {
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
|
||||
interface EthConnectPrompProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const EthConnectPrompt = ({ children }: EthConnectPrompProps) => {
|
||||
export const EthConnectPrompt = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appDispatch } = useAppState();
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Button
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
data-testid="connect-to-eth-btn"
|
||||
>
|
||||
{t('connectEthWallet')}
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
fill={true}
|
||||
data-testid="connect-to-eth-btn"
|
||||
>
|
||||
{t('connectEthWallet')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ 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,4 +1,3 @@
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import React from 'react';
|
||||
|
||||
import type { BigNumber } from '../../lib/bignumber';
|
||||
@@ -19,21 +18,7 @@ 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;
|
||||
|
||||
@@ -52,9 +37,6 @@ 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;
|
||||
|
||||
@@ -71,12 +53,10 @@ 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,
|
||||
@@ -90,14 +70,6 @@ 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,11 +14,9 @@ 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: '',
|
||||
@@ -34,17 +32,6 @@ 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,
|
||||
|
||||
@@ -1,666 +0,0 @@
|
||||
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;
|
||||
@@ -1,21 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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]);
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
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]);
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
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,21 +0,0 @@
|
||||
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));
|
||||
}
|
||||
+8
-3
@@ -1,6 +1,11 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { create } from 'zustand';
|
||||
import type { UserTrancheBalance } from '../../contexts/app-state/app-state-context';
|
||||
|
||||
interface UserTrancheBalance {
|
||||
id: number;
|
||||
locked: BigNumber;
|
||||
vested: BigNumber;
|
||||
}
|
||||
|
||||
export interface AssociationBreakdown {
|
||||
stakingAssociations: { [vegaKey: string]: BigNumber };
|
||||
@@ -13,8 +18,8 @@ export type BalancesStore = {
|
||||
balanceFormatted: BigNumber;
|
||||
totalVestedBalance: BigNumber;
|
||||
totalLockedBalance: BigNumber;
|
||||
walletBalance: BigNumber;
|
||||
trancheBalances: UserTrancheBalance[];
|
||||
walletBalance: BigNumber;
|
||||
lien: BigNumber;
|
||||
walletAssociatedBalance: BigNumber;
|
||||
vestingAssociatedBalance: BigNumber;
|
||||
@@ -38,8 +43,8 @@ export const useBalances = create<BalancesStore>((set) => ({
|
||||
stakingAssociations: {},
|
||||
vestingAssociations: {},
|
||||
},
|
||||
trancheBalances: [],
|
||||
allowance: new BigNumber(0),
|
||||
trancheBalances: [],
|
||||
totalVestedBalance: new BigNumber(0),
|
||||
totalLockedBalance: new BigNumber(0),
|
||||
balanceFormatted: new BigNumber(0),
|
||||
@@ -0,0 +1,80 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -22,6 +21,7 @@ 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,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;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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';
|
||||
@@ -13,7 +11,7 @@ import {
|
||||
initialClaimState,
|
||||
} from './claim-reducer';
|
||||
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import type { Tranche } from '../../lib/tranches/tranches-store';
|
||||
|
||||
const Claim = ({
|
||||
address,
|
||||
@@ -23,10 +21,8 @@ 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(() => {
|
||||
@@ -48,10 +44,9 @@ const Claim = ({
|
||||
// If the claim has been committed refetch the new VEGA balance
|
||||
React.useEffect(() => {
|
||||
if (state.claimStatus === ClaimStatus.Finished && address) {
|
||||
getUserTrancheBalances();
|
||||
refreshBalances();
|
||||
}
|
||||
}, [address, getUserTrancheBalances, refreshBalances, state.claimStatus]);
|
||||
}, [address, refreshBalances, state.claimStatus]);
|
||||
|
||||
if (state.error) {
|
||||
return <ClaimError />;
|
||||
|
||||
@@ -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 '../../hooks/use-tranches';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
import type { RouteChildProps } from '..';
|
||||
import Claim from './claim';
|
||||
import { ClaimRestricted } from './claim-restricted';
|
||||
@@ -16,7 +16,11 @@ const ClaimIndex = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const { account } = useWeb3React();
|
||||
const { tranches, loading, error } = useTranches();
|
||||
const { tranches, loading, error } = useTranches((state) => ({
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
tranches: state.tranches,
|
||||
}));
|
||||
|
||||
if (loading || !tranches) {
|
||||
return (
|
||||
@@ -38,13 +42,14 @@ 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,11 +51,9 @@ 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,11 +14,9 @@ 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,56 +1,81 @@
|
||||
import { Callout, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useBalances } from '../../../lib/balances/balances-store';
|
||||
import React from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { Link, useNavigate, useParams } 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 { state, account } = useOutletContext<{
|
||||
state: RedemptionState;
|
||||
account: string;
|
||||
}>();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
balanceFormatted,
|
||||
lien,
|
||||
totalVestedBalance,
|
||||
totalLockedBalance,
|
||||
trancheBalances,
|
||||
} = useBalances();
|
||||
|
||||
const { userTranches } = state;
|
||||
|
||||
const filteredTranches = React.useMemo(
|
||||
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(
|
||||
() =>
|
||||
userTranches.filter((tr) => {
|
||||
const balance = trancheBalances.find(
|
||||
tranches?.filter((tr) => {
|
||||
const balance = userTrancheBalances.find(
|
||||
({ id }) => id.toString() === tr.tranche_id.toString()
|
||||
);
|
||||
return (
|
||||
balance?.locked.isGreaterThan(0) || balance?.vested.isGreaterThan(0)
|
||||
);
|
||||
}),
|
||||
[trancheBalances, userTranches]
|
||||
}) || [],
|
||||
[userTrancheBalances, tranches]
|
||||
);
|
||||
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 = React.useMemo(() => {
|
||||
const zeroTranche = trancheBalances.find((t) => t.id === 0);
|
||||
const zeroTranche = useMemo(() => {
|
||||
const zeroTranche = userTrancheBalances.find((t) => t.id === 0);
|
||||
if (zeroTranche && zeroTranche.locked.isGreaterThan(0)) {
|
||||
return zeroTranche;
|
||||
}
|
||||
return null;
|
||||
}, [trancheBalances]);
|
||||
}, [userTrancheBalances]);
|
||||
|
||||
if (!filteredTranches.length) {
|
||||
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) {
|
||||
return (
|
||||
<section data-testid="redemption-page">
|
||||
<div className="mb-8">
|
||||
@@ -79,17 +104,17 @@ export const RedemptionInformation = () => {
|
||||
{t(
|
||||
'{{address}} has {{balance}} VEGA tokens in {{tranches}} tranches of the vesting contract.',
|
||||
{
|
||||
address: truncateMiddle(account),
|
||||
balance: formatNumber(balanceFormatted),
|
||||
address: truncateMiddle(address),
|
||||
balance: formatNumber(userBalances.balanceFormatted),
|
||||
tranches: filteredTranches.length,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<div className="mb-24">
|
||||
<VestingTable
|
||||
associated={lien}
|
||||
locked={totalLockedBalance}
|
||||
vested={totalVestedBalance}
|
||||
associated={userBalances.lien}
|
||||
locked={totalLocked}
|
||||
vested={totalVested}
|
||||
/>
|
||||
</div>
|
||||
{filteredTranches.length ? <h2>{t('Tranche breakdown')}</h2> : null}
|
||||
@@ -98,7 +123,7 @@ export const RedemptionInformation = () => {
|
||||
trancheId={0}
|
||||
total={
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
trancheBalances.find(
|
||||
userTrancheBalances.find(
|
||||
({ id }) => id.toString() === zeroTranche.id.toString()
|
||||
)!.locked
|
||||
}
|
||||
@@ -108,22 +133,25 @@ export const RedemptionInformation = () => {
|
||||
<TrancheTable
|
||||
key={tr.tranche_id}
|
||||
tranche={tr}
|
||||
lien={lien}
|
||||
lien={userBalances.lien}
|
||||
locked={
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
trancheBalances.find(
|
||||
userTrancheBalances.find(
|
||||
({ id }) => id.toString() === tr.tranche_id.toString()
|
||||
)!.locked
|
||||
}
|
||||
vested={
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
trancheBalances.find(
|
||||
userTrancheBalances.find(
|
||||
({ id }) => id.toString() === tr.tranche_id.toString()
|
||||
)!.vested
|
||||
}
|
||||
totalVested={totalVestedBalance}
|
||||
totalLocked={totalLockedBalance}
|
||||
onClick={() => navigate(`/vesting/${tr.tranche_id}`)}
|
||||
totalVested={totalVested}
|
||||
totalLocked={totalLocked}
|
||||
onClick={() =>
|
||||
navigate(`${Routes.REDEEM}/${address}/${tr.tranche_id}`)
|
||||
}
|
||||
address={address}
|
||||
/>
|
||||
))}
|
||||
<Callout
|
||||
@@ -132,7 +160,7 @@ export const RedemptionInformation = () => {
|
||||
intent={Intent.Warning}
|
||||
>
|
||||
<p>{t('Find out more about Staking.')}</p>
|
||||
<Link to="/staking" className="underline text-white">
|
||||
<Link to={Routes.VALIDATORS} 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-12 w-12 mr-4`} />
|
||||
<span className={`bg-${colour} inline-block h-4 w-4 mr-1`} />
|
||||
);
|
||||
|
||||
export const VestingTable = ({
|
||||
@@ -65,17 +65,17 @@ export const VestingTable = ({
|
||||
</KeyValueTable>
|
||||
<div className="flex border-white border">
|
||||
<div
|
||||
className="bg-vega-pink h-16"
|
||||
className="bg-vega-pink h-4"
|
||||
style={{ flex: lockedPercentage.toNumber() }}
|
||||
/>
|
||||
<div
|
||||
className="bg-vega-green h-16"
|
||||
className="bg-vega-green h-4"
|
||||
style={{ flex: vestedPercentage.toNumber() }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-4 mt-4">
|
||||
<div className="flex h-1 mt-1">
|
||||
<div
|
||||
className="bg-vega-yellow h-4"
|
||||
className="bg-vega-yellow h-1"
|
||||
style={{ flex: stakedPercentage.toNumber() }}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,62 @@
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Button,
|
||||
Callout,
|
||||
FormGroup,
|
||||
Input,
|
||||
InputError,
|
||||
Intent,
|
||||
Splash,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { EthConnectPrompt } from '../../components/eth-connect-prompt';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { useTranches } from '../../hooks/use-tranches';
|
||||
import { useBalances } from '../../lib/balances/balances-store';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
import RoutesConfig from '../routes';
|
||||
import {
|
||||
initialRedemptionState,
|
||||
RedemptionActionType,
|
||||
redemptionReducer,
|
||||
} from './redemption-reducer';
|
||||
|
||||
interface FormFields {
|
||||
address: string;
|
||||
}
|
||||
|
||||
const RedemptionRouter = () => {
|
||||
const { address } = useParams<{ address: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [state, dispatch] = React.useReducer(
|
||||
redemptionReducer,
|
||||
initialRedemptionState
|
||||
);
|
||||
const { trancheBalances } = useBalances();
|
||||
const { account } = useWeb3React();
|
||||
const { tranches, error, loading } = useTranches();
|
||||
|
||||
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,
|
||||
});
|
||||
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 { account } = useWeb3React();
|
||||
const { tranches, error, loading } = useTranches((state) => ({
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
tranches: state.tranches,
|
||||
}));
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>();
|
||||
|
||||
if (account) {
|
||||
run(account);
|
||||
}
|
||||
}, [account, tranches]);
|
||||
const onSubmit = useCallback(
|
||||
(fields: FormFields) => {
|
||||
navigate(`${RoutesConfig.REDEEM}/${fields.address}`);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -63,30 +74,48 @@ const RedemptionRouter = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
if (!address) {
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 }} />;
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default RedemptionRouter;
|
||||
|
||||
@@ -33,10 +33,10 @@ export const TrancheItem = ({
|
||||
}: TrancheItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const labelClasses =
|
||||
'inline-block uppercase bg-white text-black py-4 px-8 font-mono';
|
||||
'inline-block uppercase bg-white text-black py-1 px-2 font-mono';
|
||||
|
||||
return (
|
||||
<section data-testid="tranche-item" className="mb-40">
|
||||
<section data-testid="tranche-item" className="mb-8">
|
||||
<div className="flex border-b">
|
||||
{link ? (
|
||||
<Link to={link}>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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: {
|
||||
@@ -22,6 +23,7 @@ export interface TrancheTableProps {
|
||||
totalLocked: BigNumber;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
address: string | null;
|
||||
}
|
||||
|
||||
export const Tranche0Table = ({
|
||||
@@ -65,9 +67,11 @@ 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);
|
||||
@@ -93,13 +97,20 @@ export const TrancheTable = ({
|
||||
amount: reduceAmount,
|
||||
}}
|
||||
components={{
|
||||
stakeLink: <Link to={`/staking`} />,
|
||||
disassociateLink: <Link to={`/staking/disassociate`} />,
|
||||
stakeLink: <Link className="underline" to={Routes.VALIDATORS} />,
|
||||
disassociateLink: (
|
||||
<Link className="underline" to={Routes.DISASSOCIATE} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (!trancheFullyLocked && redeemable) {
|
||||
} else if (
|
||||
!trancheFullyLocked &&
|
||||
redeemable &&
|
||||
connectedAddress &&
|
||||
address === connectedAddress
|
||||
) {
|
||||
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, useOutletContext } from 'react-router-dom';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import { TransactionCallout } from '../../../components/transaction-callout';
|
||||
import { useContracts } from '../../../contexts/contracts/contracts-context';
|
||||
@@ -9,32 +9,36 @@ 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 { state, address } = useOutletContext<{
|
||||
state: RedemptionState;
|
||||
address: string;
|
||||
}>();
|
||||
const { account: address } = useWeb3React();
|
||||
const { vesting } = useContracts();
|
||||
const { t } = useTranslation();
|
||||
const { lien, totalVestedBalance, trancheBalances, totalLockedBalance } =
|
||||
useBalances();
|
||||
const refreshBalances = useRefreshBalances(address);
|
||||
const getUserTrancheBalances = useGetUserTrancheBalances(address, vesting);
|
||||
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 { id } = useParams<{ id: string }>();
|
||||
const numberId = Number(id);
|
||||
const { userTranches } = state;
|
||||
const tranche = React.useMemo(
|
||||
() => userTranches.find(({ tranche_id }) => tranche_id === numberId),
|
||||
[numberId, userTranches]
|
||||
() => tranches?.find(({ tranche_id }) => tranche_id === numberId) || null,
|
||||
[numberId, tranches]
|
||||
);
|
||||
const {
|
||||
state: txState,
|
||||
@@ -42,7 +46,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())
|
||||
@@ -55,10 +59,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, getUserTrancheBalances, refreshBalances, txState.txState]);
|
||||
}, [address, decimals, getTranches, refreshBalances, txState.txState]);
|
||||
|
||||
const trancheBalance = React.useMemo(() => {
|
||||
return trancheBalances.find(
|
||||
@@ -66,6 +70,10 @@ export const RedeemFromTranche = () => {
|
||||
);
|
||||
}, [id, trancheBalances]);
|
||||
|
||||
if (!address) {
|
||||
return <EthConnectPrompt />;
|
||||
}
|
||||
|
||||
if (
|
||||
!tranche ||
|
||||
tranche.total_removed.isEqualTo(tranche.total_added) ||
|
||||
@@ -147,6 +155,7 @@ export const RedeemFromTranche = () => {
|
||||
locked={trancheBalance.locked}
|
||||
vested={trancheBalance.vested}
|
||||
onClick={perform}
|
||||
address={address || ''}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -300,12 +300,17 @@ const routerConfig = [
|
||||
element: <LazyRedemption name="Vesting" />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <LazyRedemptionIndex />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <LazyRedemptionTranche />,
|
||||
path: ':address',
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <LazyRedemptionIndex />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <LazyRedemptionTranche />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+3
-2
@@ -16,10 +16,11 @@ export const StakingWalletsContainer = ({
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<EthConnectPrompt>
|
||||
<>
|
||||
<p>{t('associateInfo1')}</p>
|
||||
<p>{t('associateInfo2')}</p>
|
||||
</EthConnectPrompt>
|
||||
<EthConnectPrompt />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { sumCirculatingTokens } from './token-details-circulating';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
it('It sums some easy tranches correctly', () => {
|
||||
const tranches: Partial<Tranche>[] = [
|
||||
{ total_added: new BigNumber('100'), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber('100'), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber('100'), locked_amount: new BigNumber(0) },
|
||||
const tranches = [
|
||||
{ total_added: new BigNumber(100), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber(100), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber(100), locked_amount: new BigNumber(0) },
|
||||
];
|
||||
|
||||
const result = sumCirculatingTokens(tranches as Tranche[]);
|
||||
const result = sumCirculatingTokens(tranches);
|
||||
expect(result.toString()).toEqual('300');
|
||||
});
|
||||
|
||||
it('It sums some longer tranches correctly', () => {
|
||||
const tranches: Partial<Tranche>[] = [
|
||||
const tranches = [
|
||||
{
|
||||
total_added: new BigNumber('10000000000'),
|
||||
total_added: new BigNumber(10000000000),
|
||||
locked_amount: new BigNumber(0),
|
||||
},
|
||||
{ total_added: new BigNumber('20'), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber('3000'), locked_amount: new BigNumber(3020) },
|
||||
{ total_added: new BigNumber(20), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber(3000), locked_amount: new BigNumber(3020) },
|
||||
];
|
||||
|
||||
const result = sumCirculatingTokens(tranches as Tranche[]);
|
||||
const result = sumCirculatingTokens(tranches);
|
||||
expect(result.toString()).toEqual('10000000000');
|
||||
});
|
||||
|
||||
it('Handles null tranche array', () => {
|
||||
const tranches = null;
|
||||
|
||||
const result = sumCirculatingTokens(tranches as unknown as Tranche[]);
|
||||
const result = sumCirculatingTokens(tranches);
|
||||
expect(result.toString()).toEqual('0');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import type { Tranche } from '../../../lib/tranches/tranches-store';
|
||||
|
||||
/**
|
||||
* Add together the circulating tokens from all tranches
|
||||
@@ -9,7 +9,9 @@ import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
* @param decimals decimal places for the formatted result
|
||||
* @return The total circulating tokens from all tranches
|
||||
*/
|
||||
export function sumCirculatingTokens(tranches: Tranche[] | null): BigNumber {
|
||||
export function sumCirculatingTokens(
|
||||
tranches: { total_added: BigNumber; locked_amount: BigNumber }[] | null
|
||||
): BigNumber {
|
||||
let totalCirculating: BigNumber = new BigNumber(0);
|
||||
|
||||
tranches?.forEach(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranches } from '../../../hooks/use-tranches';
|
||||
import { useTranches } from '../../../lib/tranches/tranches-store';
|
||||
import type { BigNumber } from '../../../lib/bignumber';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
import { TokenDetailsCirculating } from './token-details-circulating';
|
||||
@@ -25,7 +25,11 @@ export const TokenDetails = ({
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { tranches, loading, error } = useTranches();
|
||||
const { tranches, loading, error } = useTranches((state) => ({
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
tranches: state.tranches,
|
||||
}));
|
||||
const { config } = useEthereumConfig();
|
||||
const { token } = useContracts();
|
||||
|
||||
|
||||
@@ -4,14 +4,18 @@ import { Outlet } from 'react-router-dom';
|
||||
import { Heading } from '../../components/heading';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import { useTranches } from '../../hooks/use-tranches';
|
||||
import type { RouteChildProps } from '..';
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
|
||||
const TrancheRouter = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const { tranches, error, loading } = useTranches();
|
||||
const { tranches, error, loading } = useTranches((state) => ({
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
tranches: state.tranches,
|
||||
}));
|
||||
|
||||
if (!tranches || loading) {
|
||||
return (
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
import type { Tranche as ITranche } from '@vegaprotocol/smart-contracts';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Link,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { formatNumber } from '@vegaprotocol/react-helpers';
|
||||
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import { formatNumber } from '../../lib/format-number';
|
||||
import { TrancheItem } from '../redemption/tranche-item';
|
||||
import Routes from '../routes';
|
||||
import { TrancheLabel } from './tranche-label';
|
||||
|
||||
const TrancheProgressContents = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="flex justify-between gap-4 font-mono py-2 px-4">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
|
||||
export const Tranche = () => {
|
||||
const tranches = useOutletContext<ITranche[]>();
|
||||
const tranches = useTranches((state) => state.tranches);
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const { trancheId } = useParams<{ trancheId: string }>();
|
||||
const { trancheId } = useParams<{ trancheId: string; address: string }>();
|
||||
const { chainId } = useWeb3React();
|
||||
const tranche = tranches.find(
|
||||
const tranche = tranches?.find(
|
||||
(tranche) => trancheId && parseInt(trancheId) === tranche.tranche_id
|
||||
);
|
||||
|
||||
const lockedData = React.useMemo(() => {
|
||||
if (!tranche) return null;
|
||||
const locked = tranche.locked_amount.div(tranche.total_added);
|
||||
return {
|
||||
locked,
|
||||
unlocked: new BigNumber(1).minus(locked),
|
||||
};
|
||||
}, [tranche]);
|
||||
|
||||
if (!tranche) {
|
||||
return <Navigate to={Routes.NOT_FOUND} />;
|
||||
}
|
||||
@@ -67,33 +51,36 @@ export const Tranche = () => {
|
||||
</div>
|
||||
<h2>{t('Holders')}</h2>
|
||||
{tranche.users.length ? (
|
||||
<ul role="list">
|
||||
{tranche.users.map((user, i) => {
|
||||
const unlocked = user.remaining_tokens.times(
|
||||
lockedData?.unlocked || 0
|
||||
);
|
||||
const locked = user.remaining_tokens.times(lockedData?.locked || 0);
|
||||
return (
|
||||
<li className="pb-4" key={i}>
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ETHERSCAN_URL}/tx/${user.address}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user.address}
|
||||
</Link>
|
||||
<TrancheProgressContents>
|
||||
<span>{t('Locked')}</span>
|
||||
<span>{t('Unlocked')}</span>
|
||||
</TrancheProgressContents>
|
||||
<TrancheProgressContents>
|
||||
<span>{formatNumber(locked)}</span>
|
||||
<span>{formatNumber(unlocked)}</span>
|
||||
</TrancheProgressContents>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
<h1>{t('Ethereum Address')}</h1>
|
||||
<h1>{t('View tranche data')}</h1>
|
||||
</KeyValueTableRow>
|
||||
{tranche.users.map((user) => (
|
||||
<KeyValueTableRow key={user}>
|
||||
{
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ETHERSCAN_URL}/address/${user}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user}
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
<RouterLink
|
||||
className="underline"
|
||||
title={t('View vesting information')}
|
||||
to={`${Routes.REDEEM}/${user}`}
|
||||
>
|
||||
{t('View vesting information')}
|
||||
</RouterLink>
|
||||
}
|
||||
</KeyValueTableRow>
|
||||
))}
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
) : (
|
||||
<p>{t('No users')}</p>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -10,6 +8,8 @@ import { TrancheLabel } from './tranche-label';
|
||||
import { VestingChart } from './vesting-chart';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import type { Tranche } from '../../lib/tranches/tranches-store';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
|
||||
const trancheMinimum = 10;
|
||||
|
||||
@@ -17,7 +17,7 @@ const shouldShowTranche = (t: Tranche) =>
|
||||
!t.total_added.isLessThanOrEqualTo(trancheMinimum);
|
||||
|
||||
export const Tranches = () => {
|
||||
const tranches = useOutletContext<Tranche[]>();
|
||||
const tranches = useTranches((state) => state.tranches);
|
||||
const [showAll, setShowAll] = React.useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const { chainId } = useWeb3React();
|
||||
@@ -38,25 +38,24 @@ export const Tranches = () => {
|
||||
<ul role="list">
|
||||
{(showAll ? tranches : filteredTranches).map((tranche) => {
|
||||
return (
|
||||
<React.Fragment key={tranche.tranche_id}>
|
||||
<TrancheItem
|
||||
link={`${tranche.tranche_id}`}
|
||||
tranche={tranche}
|
||||
locked={tranche.locked_amount}
|
||||
unlocked={tranche.total_added.minus(tranche.locked_amount)}
|
||||
total={tranche.total_added}
|
||||
secondaryHeader={
|
||||
<TrancheLabel chainId={chainId} id={tranche.tranche_id} />
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
<TrancheItem
|
||||
key={tranche.tranche_id}
|
||||
link={`${tranche.tranche_id}`}
|
||||
tranche={tranche}
|
||||
locked={tranche.locked_amount}
|
||||
unlocked={tranche.total_added.minus(tranche.locked_amount)}
|
||||
total={tranche.total_added}
|
||||
secondaryHeader={
|
||||
<TrancheLabel chainId={chainId} id={tranche.tranche_id} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p>{t('No tranches')}</p>
|
||||
)}
|
||||
<section className="text-center mt-32">
|
||||
<section className="text-center mt-4">
|
||||
<ButtonLink onClick={() => setShowAll(!showAll)}>
|
||||
{showAll
|
||||
? t(
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import compact from 'lodash/compact';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
|
||||
@@ -134,15 +135,9 @@ const AccountHistoryManager = ({
|
||||
?.filter((item: Account) => Boolean(item && item.market))
|
||||
.map<Market>((item) => item.market as Market) ?? null;
|
||||
return arr
|
||||
? Array.from(
|
||||
new Set(
|
||||
arr
|
||||
.filter(marketFilterCb)
|
||||
.sort((a, b) =>
|
||||
a.tradableInstrument.instrument.code.localeCompare(
|
||||
b.tradableInstrument.instrument.code
|
||||
)
|
||||
)
|
||||
? uniqBy(arr.filter(marketFilterCb), 'id').sort((a, b) =>
|
||||
a.tradableInstrument.instrument.code.localeCompare(
|
||||
b.tradableInstrument.instrument.code
|
||||
)
|
||||
)
|
||||
: null;
|
||||
@@ -240,6 +235,11 @@ const AccountHistoryManager = ({
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{market && (
|
||||
<DropdownMenuItem key="0" onClick={() => setMarket(null)}>
|
||||
{t('All markets')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{markets?.map((m) => (
|
||||
<DropdownMenuItem key={m.id} onClick={() => resolveMarket(m)}>
|
||||
{m.tradableInstrument.instrument.code}
|
||||
@@ -253,14 +253,12 @@ const AccountHistoryManager = ({
|
||||
useEffect(() => {
|
||||
if (
|
||||
accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN ||
|
||||
(asset &&
|
||||
market &&
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id !==
|
||||
asset.id)
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.id !==
|
||||
asset?.id
|
||||
) {
|
||||
setMarket(null);
|
||||
}
|
||||
}, [accountType, asset, market]);
|
||||
}, [accountType, asset?.id, market]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col gap-8">
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ReactNode } from 'react';
|
||||
import { AppFailure } from './app-failure';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
|
||||
const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
|
||||
export const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
|
||||
loading: () => <>Loading...</>,
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import { Connectors } from '../lib/vega-connectors';
|
||||
import { ViewingBanner } from '../components/viewing-banner';
|
||||
import { Banner } from '../components/banner';
|
||||
import classNames from 'classnames';
|
||||
import { AppLoader } from '../components/app-loader';
|
||||
import { AppLoader, DynamicLoader } from '../components/app-loader';
|
||||
|
||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||
|
||||
@@ -115,7 +115,7 @@ function VegaTradingApp(props: AppProps) {
|
||||
// Prevent HashRouter from being server side rendered as it
|
||||
// relies on presence of document object
|
||||
if (status === 'default') {
|
||||
return null;
|
||||
return <DynamicLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,8 @@ export default function Document() {
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/preloader.css" media="all" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
.pre-loader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(0) {
|
||||
animation-delay: 0ms;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:first-child {
|
||||
animation-delay: -0.2s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(4) {
|
||||
animation-delay: 1s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(5) {
|
||||
animation-delay: -0.25s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(6) {
|
||||
animation-delay: 0.3s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(7) {
|
||||
animation-delay: -1.05s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(8) {
|
||||
animation-delay: 0.8s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(9) {
|
||||
animation-delay: -0.9s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(10) {
|
||||
animation-delay: 0.5s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(11) {
|
||||
animation-delay: -2.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(12) {
|
||||
animation-delay: 2.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(13) {
|
||||
animation-delay: -0.65s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(14) {
|
||||
animation-delay: 0.7s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(15) {
|
||||
animation-delay: -3s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(16) {
|
||||
animation-delay: 2.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .pre-loader-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pre-loader .pre-loader-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pre-loader .loader-item {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #000;
|
||||
animation: flickering 0.4s steps(2, jump-none) infinite alternate;
|
||||
}
|
||||
@keyframes flickering {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
26% {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useEtherscanLink } from '@vegaprotocol/environment';
|
||||
import { ContractAddressLink } from '@vegaprotocol/environment';
|
||||
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
|
||||
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
@@ -276,16 +275,3 @@ export const AssetDetailsTable = ({
|
||||
</KeyValueTable>
|
||||
);
|
||||
};
|
||||
|
||||
// Separate component for the link as otherwise eslint will complain
|
||||
// about useEnvironment being used in a component
|
||||
// named with a lowercase 'value'
|
||||
const ContractAddressLink = ({ address }: { address: string }) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
const href = etherscanLink(`/address/${address}`);
|
||||
return (
|
||||
<Link href={href} target="_blank" title={t('View on etherscan')}>
|
||||
{address}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import { addMockTransactionResponse } from './lib/commands/mock-transaction-resp
|
||||
import { addCreateMarket } from './lib/commands/create-market';
|
||||
import { addConnectPublicKey } from './lib/commands/add-connect-public-key';
|
||||
import { addVegaWalletSubmitProposal } from './lib/commands/vega-wallet-submit-proposal';
|
||||
import { addGetNodes } from './lib/commands/get-nodes';
|
||||
|
||||
addGetTestIdcommand();
|
||||
addSlackCommand();
|
||||
@@ -28,6 +29,7 @@ addMockWeb3ProviderCommand();
|
||||
addHighlightLog();
|
||||
addVegaWalletReceiveFaucetedAsset();
|
||||
addGetAssets();
|
||||
addGetNodes();
|
||||
addContainsExactly();
|
||||
addGetNetworkParameters();
|
||||
addUpdateCapsuleMultiSig();
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { print } from 'graphql';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { edgesToList } from '../utils';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Chainable<Subject> {
|
||||
getAssets(): Chainable<Record<string, AssetFieldsFragment>>;
|
||||
getAssets(): Chainable<Array<AssetFieldsFragment>>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,12 +73,6 @@ export function addGetAssets() {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
})
|
||||
.its('body.data.assetsConnection.edges')
|
||||
.then((edges) => {
|
||||
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
|
||||
return edges.reduce((list, edge) => {
|
||||
list[edge.node.name] = edge.node;
|
||||
return list;
|
||||
}, {});
|
||||
});
|
||||
.then(edgesToList);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import type { Node } from '@vegaprotocol/types';
|
||||
import { print } from 'graphql';
|
||||
import { edgesToList } from '../utils';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Chainable<Subject> {
|
||||
getNodes(): Chainable<Array<Partial<Node>>>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function addGetNodes() {
|
||||
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
|
||||
Cypress.Commands.add('getNodes', () => {
|
||||
const query = gql`
|
||||
query Nodes {
|
||||
nodesConnection {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
avatarUrl
|
||||
ethereumAddress
|
||||
id
|
||||
infoUrl
|
||||
location
|
||||
name
|
||||
pendingStake
|
||||
pubkey
|
||||
stakedByDelegates
|
||||
stakedByOperator
|
||||
stakedTotal
|
||||
status
|
||||
tmPubkey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `http://localhost:3028/query`,
|
||||
body: {
|
||||
query: print(query),
|
||||
},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
})
|
||||
.its(`body.data.nodesConnection.edges`)
|
||||
.then(edgesToList);
|
||||
});
|
||||
}
|
||||
@@ -23,8 +23,8 @@ export function addVegaWalletReceiveFaucetedAsset() {
|
||||
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
|
||||
cy.getAssets().then((assets) => {
|
||||
console.log(assets);
|
||||
const asset = assets[assetName];
|
||||
if (assets[assetName] !== undefined) {
|
||||
const asset = assets.find((a) => a.name === assetName);
|
||||
if (asset) {
|
||||
for (let i = 0; i < asset.decimals; i++) amount += '0';
|
||||
cy.exec(
|
||||
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset.id}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
|
||||
@@ -38,15 +38,9 @@ export function addVegaWalletReceiveFaucetedAsset() {
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const validAssets = Object.keys(assets)
|
||||
.filter((key) => key.includes('fake'))
|
||||
.reduce((obj, key) => {
|
||||
return Object.assign(obj, {
|
||||
[key]: assets[key],
|
||||
});
|
||||
}, {});
|
||||
const validAssets = assets.filter((a) => a.name.includes('fake'));
|
||||
assert.exists(
|
||||
assets[assetName],
|
||||
asset,
|
||||
`${assetName} is not a faucet-able asset, only the following assets can be faucet-ed: ${validAssets}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,3 +62,11 @@ const checkSortChange = (tabsArr: string[], column: string) => {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
type Edges = { node: unknown }[];
|
||||
export function edgesToList(edges: Edges) {
|
||||
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
|
||||
return edges.map((edge) => {
|
||||
return edge.node;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEtherscanLink } from '../hooks';
|
||||
|
||||
export const ContractAddressLink = ({ address }: { address: string }) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
const href = etherscanLink(`/address/${address}`);
|
||||
return (
|
||||
<Link href={href} target="_blank" title={t('View on etherscan')}>
|
||||
{address}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -2,3 +2,4 @@ export * from './network-loader';
|
||||
export * from './network-switcher';
|
||||
export * from './node-guard';
|
||||
export * from './node-switcher';
|
||||
export * from './contract-address-link';
|
||||
|
||||
@@ -94,6 +94,7 @@ export const TOKEN_NEW_NETWORK_PARAM_PROPOSAL =
|
||||
export const TOKEN_GOVERNANCE = '/proposals';
|
||||
export const TOKEN_PROPOSALS = '/proposals';
|
||||
export const TOKEN_PROPOSAL = '/proposals/:id';
|
||||
export const TOKEN_VALIDATOR = '/validators/:id';
|
||||
|
||||
// Explorer pages
|
||||
export const EXPLORER_TX = '/txs/:hash';
|
||||
|
||||
@@ -45,8 +45,9 @@ export const FillsManager = ({
|
||||
datasource={{ getRows }}
|
||||
onBodyScrollEnd={onBodyScrollEnd}
|
||||
onBodyScroll={onBodyScroll}
|
||||
noRowsOverlayComponent={() => null}
|
||||
onMarketClick={onMarketClick}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -2,10 +2,8 @@ import type { Asset } from '@vegaprotocol/assets';
|
||||
import { assetsProvider } from '@vegaprotocol/assets';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { marketsProvider } from '@vegaprotocol/market-list';
|
||||
import type { PageInfo } from '@vegaprotocol/react-helpers';
|
||||
import { makeInfiniteScrollGetRows } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
defaultAppend as append,
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
useDataProvider,
|
||||
@@ -27,13 +25,15 @@ import type {
|
||||
import { LedgerEntriesDocument } from './__generated__/LedgerEntries';
|
||||
|
||||
export type LedgerEntry = LedgerEntryFragment & {
|
||||
id: number;
|
||||
asset: Asset | null | undefined;
|
||||
marketSender: Market | null | undefined;
|
||||
marketReceiver: Market | null | undefined;
|
||||
};
|
||||
|
||||
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
|
||||
export type AggregatedLedgerEntriesNode = AggregatedLedgerEntriesEdge & {
|
||||
node: LedgerEntry;
|
||||
};
|
||||
|
||||
const getData = (responseData: LedgerEntriesQuery | null) => {
|
||||
return responseData?.ledgerEntries?.edges || [];
|
||||
@@ -94,33 +94,29 @@ export const update = (
|
||||
});
|
||||
};
|
||||
|
||||
const getPageInfo = (responseData: LedgerEntriesQuery): PageInfo | null =>
|
||||
responseData.ledgerEntries?.pageInfo || null;
|
||||
|
||||
const ledgerEntriesOnlyProvider = makeDataProvider({
|
||||
query: LedgerEntriesDocument,
|
||||
getData,
|
||||
getDelta: getData,
|
||||
update,
|
||||
pagination: {
|
||||
getPageInfo,
|
||||
append,
|
||||
first: 100,
|
||||
},
|
||||
additionalContext: {
|
||||
isEnlargedTimeout: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const ledgerEntriesProvider = makeDerivedDataProvider<
|
||||
(AggregatedLedgerEntriesEdge | null)[],
|
||||
AggregatedLedgerEntriesEdge[],
|
||||
AggregatedLedgerEntriesNode[],
|
||||
AggregatedLedgerEntriesNode[],
|
||||
LedgerEntriesQueryVariables
|
||||
>(
|
||||
[ledgerEntriesOnlyProvider, assetsProvider, marketsProvider],
|
||||
[
|
||||
ledgerEntriesOnlyProvider,
|
||||
(callback, client) => assetsProvider(callback, client),
|
||||
marketsProvider,
|
||||
],
|
||||
([entries, assets, markets]) => {
|
||||
return entries.map((edge: AggregatedLedgerEntriesEdge) => {
|
||||
const entry = edge?.node;
|
||||
const entry = edge.node;
|
||||
const asset = assets.find((asset: Asset) => asset.id === entry.assetId);
|
||||
const marketSender = markets.find(
|
||||
(market: Market) => market.id === entry.fromAccountMarketId
|
||||
@@ -148,21 +144,22 @@ export const useLedgerEntriesDataProvider = ({
|
||||
filter,
|
||||
gridRef,
|
||||
}: Props) => {
|
||||
const dataRef = useRef<(AggregatedLedgerEntriesEdge | null)[] | null>(null);
|
||||
const dataRef = useRef<AggregatedLedgerEntriesEdge[] | null>(null);
|
||||
const totalCountRef = useRef<number>();
|
||||
|
||||
const variables = useMemo<LedgerEntriesQueryVariables>(
|
||||
() => ({
|
||||
partyId,
|
||||
dateRange: filter?.vegaTime?.value,
|
||||
fromAccountType: filter?.fromAccountType?.value ?? null,
|
||||
toAccountType: filter?.toAccountType?.value ?? null,
|
||||
pagination: {
|
||||
first: 5000,
|
||||
},
|
||||
}),
|
||||
[partyId, filter]
|
||||
[partyId, filter?.vegaTime?.value]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
({ data }: { data: (AggregatedLedgerEntriesEdge | null)[] | null }) => {
|
||||
({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => {
|
||||
return updateGridData(dataRef, data, gridRef);
|
||||
},
|
||||
[gridRef]
|
||||
@@ -173,7 +170,7 @@ export const useLedgerEntriesDataProvider = ({
|
||||
data,
|
||||
totalCount,
|
||||
}: {
|
||||
data: (AggregatedLedgerEntriesEdge | null)[] | null;
|
||||
data: AggregatedLedgerEntriesEdge[] | null;
|
||||
totalCount?: number;
|
||||
}) => {
|
||||
totalCountRef.current = totalCount;
|
||||
|
||||
@@ -3,7 +3,9 @@ import type * as Schema from '@vegaprotocol/types';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { subDays, formatRFC3339 } from 'date-fns';
|
||||
import type { AggregatedLedgerEntriesNode } from './ledger-entries-data-provider';
|
||||
import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider';
|
||||
import { LedgerTable } from './ledger-table';
|
||||
import type * as Types from '@vegaprotocol/types';
|
||||
@@ -15,43 +17,43 @@ export interface Filter {
|
||||
fromAccountType?: { value: Types.AccountType[] };
|
||||
toAccountType?: { value: Types.AccountType[] };
|
||||
}
|
||||
|
||||
type LedgerManagerProps = { partyId: string };
|
||||
export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
||||
const defaultFilter = {
|
||||
vegaTime: {
|
||||
value: { start: formatRFC3339(subDays(Date.now(), 7)) },
|
||||
},
|
||||
};
|
||||
export const LedgerManager = ({ partyId }: { partyId: string }) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [filter, setFilter] = useState<Filter | undefined>();
|
||||
const [filter, setFilter] = useState<Filter>(defaultFilter);
|
||||
const [dataCount, setDataCount] = useState(0);
|
||||
|
||||
const { data, error, loading, getRows, reload } =
|
||||
useLedgerEntriesDataProvider({
|
||||
partyId,
|
||||
filter,
|
||||
gridRef,
|
||||
});
|
||||
const { data, error, loading, reload } = useLedgerEntriesDataProvider({
|
||||
partyId,
|
||||
filter,
|
||||
gridRef,
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
const updatedFilter = event.api.getFilterModel();
|
||||
if (Object.keys(updatedFilter).length) {
|
||||
setFilter(updatedFilter);
|
||||
} else if (filter) {
|
||||
setFilter(undefined);
|
||||
}
|
||||
},
|
||||
[filter]
|
||||
);
|
||||
const getRowId = useCallback(
|
||||
({ data }: { data: Types.AggregatedLedgerEntry }) =>
|
||||
`${data.vegaTime}-${data.fromAccountPartyId}-${data.toAccountPartyId}`,
|
||||
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
|
||||
const updatedFilter = { ...defaultFilter, ...event.api.getFilterModel() };
|
||||
setFilter(updatedFilter);
|
||||
}, []);
|
||||
const extractNodesDecorator = useCallback(
|
||||
(data: AggregatedLedgerEntriesNode[] | null, loading: boolean) =>
|
||||
data && !loading ? data.map((item) => item.node) : null,
|
||||
[]
|
||||
);
|
||||
|
||||
const extractedData = extractNodesDecorator(data, loading);
|
||||
useEffect(() => {
|
||||
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
||||
}, [extractedData]);
|
||||
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<LedgerTable
|
||||
ref={gridRef}
|
||||
rowModelType="infinite"
|
||||
datasource={{ getRows }}
|
||||
rowData={extractedData}
|
||||
onFilterChanged={onFilterChanged}
|
||||
getRowId={getRowId}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
@@ -59,7 +61,7 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No entries')}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
noDataCondition={() => !dataCount}
|
||||
reload={reload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '@vegaprotocol/types';
|
||||
import type { LedgerEntry } from './ledger-entries-data-provider';
|
||||
import { forwardRef } from 'react';
|
||||
import { formatRFC3339, subDays } from 'date-fns';
|
||||
|
||||
export const TransferTooltipCellComponent = ({
|
||||
value,
|
||||
@@ -35,6 +36,11 @@ export const TransferTooltipCellComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const defaultRangeFilter = { start: formatRFC3339(subDays(Date.now(), 7)) };
|
||||
const dateRangeFilterParams = {
|
||||
maxNextDays: 0,
|
||||
defaultRangeFilter,
|
||||
};
|
||||
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
|
||||
|
||||
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
@@ -42,17 +48,20 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No entries')}
|
||||
ref={ref}
|
||||
getRowId={({ data }) => data.id}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
tooltipComponent: TransferTooltipCellComponent,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
filterParams: {
|
||||
...dateRangeFilterParams,
|
||||
buttons: ['reset'],
|
||||
},
|
||||
}}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
@@ -201,6 +210,7 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
|
||||
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-'
|
||||
}
|
||||
filterParams={dateRangeFilterParams}
|
||||
filter={DateRangeFilter}
|
||||
/>
|
||||
</AgGrid>
|
||||
|
||||
@@ -70,7 +70,6 @@ export const OrderbookRow = React.memo(
|
||||
relativeAsk={cumulativeRelativeAsk}
|
||||
relativeBid={cumulativeRelativeBid}
|
||||
indicativeVolume={indicativeVolume}
|
||||
className="pr-4 text-black dark:text-white"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -201,26 +201,8 @@ const OrderbookDebugInfo = ({
|
||||
midPrice?: string;
|
||||
}) => (
|
||||
<Fragment>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '0',
|
||||
borderTop: '1px solid rgba(255,0,0,0.5)',
|
||||
background: 'black',
|
||||
width: '100%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="absolute left-0 bottom-0 font-mono"
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: '#FFF',
|
||||
background: '#000',
|
||||
padding: '2px',
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-1/2 left-0 border-t border-t-black w-full" />
|
||||
<div className="text-xs p-2 bg-black/80 text-white absolute left-0 bottom-6 font-mono">
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
{
|
||||
@@ -493,12 +475,7 @@ export const Orderbook = ({
|
||||
|
||||
const tableBody =
|
||||
data && data.length !== 0 ? (
|
||||
<div
|
||||
className="grid grid-cols-4 gap-[0.3125rem] text-right"
|
||||
style={{
|
||||
gridAutoRows: '17px',
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-4 gap-1 text-right auto-rows-[17px]">
|
||||
{data.map((data, i) => (
|
||||
<OrderbookRow
|
||||
key={data.price}
|
||||
@@ -559,14 +536,13 @@ export const Orderbook = ({
|
||||
onDoubleClick={() => setDebug(!debug)}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 grid grid-cols-4 gap-2 text-right border-b pt-2 bg-white dark:bg-black z-10 border-default w-full"
|
||||
style={{ gridAutoRows: '17px' }}
|
||||
className="absolute top-0 grid grid-cols-4 auto-rows-[17px] gap-2 text-right border-b pt-2 bg-white dark:bg-black z-10 border-default w-full"
|
||||
ref={headerElement}
|
||||
>
|
||||
<div>{t('Bid vol')}</div>
|
||||
<div>{t('Ask vol')}</div>
|
||||
<div>{t('Price')}</div>
|
||||
<div className="pr-[2px] whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<div className="pr-1 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{t('Cumulative vol')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -605,8 +581,7 @@ export const Orderbook = ({
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="absolute bottom-0 grid grid-cols-4 gap-2 border-t-[1px] border-default mt-2 z-10 bg-white dark:bg-black w-full"
|
||||
style={{ gridAutoRows: '20px' }}
|
||||
className="absolute bottom-0 grid grid-cols-4 gap-2 border-t border-default mt-2 z-10 bg-white dark:bg-black w-full"
|
||||
ref={footerElement}
|
||||
>
|
||||
<div className="col-span-2">
|
||||
|
||||
@@ -152,6 +152,8 @@ export const OrderListManager = ({
|
||||
isReadOnly={isReadOnly}
|
||||
hasActiveOrder={hasActiveOrder}
|
||||
blockLoadDebounceMillis={100}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -323,6 +323,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
field="realisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
cellClass="text-right font-mono"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
@@ -347,6 +348,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
field="unrealisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
cellClass="text-right font-mono"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable */
|
||||
process.env.TZ = 'UTC';
|
||||
export default {
|
||||
displayName: 'react-helpers',
|
||||
preset: '../../jest.preset.js',
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { countryCodeToFlagEmoji, FALLBACK_FLAG } from './flag-emoji';
|
||||
|
||||
// REGIONAL INDICATOR SYMBOLS:
|
||||
// 🇦
|
||||
// 🇧
|
||||
// 🇨
|
||||
// 🇩
|
||||
// 🇪
|
||||
// 🇫
|
||||
// 🇬
|
||||
// 🇭
|
||||
// 🇮
|
||||
// 🇯
|
||||
// 🇰
|
||||
// 🇱
|
||||
// 🇲
|
||||
// 🇳
|
||||
// 🇴
|
||||
// 🇵
|
||||
// 🇶
|
||||
// 🇷
|
||||
// 🇸
|
||||
// 🇹
|
||||
// 🇺
|
||||
// 🇻
|
||||
// 🇼
|
||||
// 🇽
|
||||
// 🇾
|
||||
// 🇿
|
||||
|
||||
const cases = [
|
||||
['AC', '🇦🇨'],
|
||||
['AD', '🇦🇩'],
|
||||
['AE', '🇦🇪'],
|
||||
['AF', '🇦🇫'],
|
||||
['AG', '🇦🇬'],
|
||||
['AI', '🇦🇮'],
|
||||
['AL', '🇦🇱'],
|
||||
['AM', '🇦🇲'],
|
||||
['AO', '🇦🇴'],
|
||||
['AR', '🇦🇷'],
|
||||
['AS', '🇦🇸'],
|
||||
['AT', '🇦🇹'],
|
||||
['AQ', '🇦🇶'],
|
||||
['AW', '🇦🇼'],
|
||||
['AZ', '🇦🇿'],
|
||||
['BA', '🇧🇦'],
|
||||
['BB', '🇧🇧'],
|
||||
['BD', '🇧🇩'],
|
||||
['BE', '🇧🇪'],
|
||||
['BF', '🇧🇫'],
|
||||
['BG', '🇧🇬'],
|
||||
['BH', '🇧🇭'],
|
||||
['BI', '🇧🇮'],
|
||||
['BJ', '🇧🇯'],
|
||||
['BL', '🇧🇱'],
|
||||
['BM', '🇧🇲'],
|
||||
['BN', '🇧🇳'],
|
||||
['BO', '🇧🇴'],
|
||||
['BR', '🇧🇷'],
|
||||
['BS', '🇧🇸'],
|
||||
['BT', '🇧🇹'],
|
||||
['BQ', '🇧🇶'],
|
||||
['BW', '🇧🇼'],
|
||||
['BY', '🇧🇾'],
|
||||
['BZ', '🇧🇿'],
|
||||
['CA', '🇨🇦'],
|
||||
['CC', '🇨🇨'],
|
||||
['CD', '🇨🇩'],
|
||||
['CF', '🇨🇫'],
|
||||
['CG', '🇨🇬'],
|
||||
['CH', '🇨🇭'],
|
||||
['CI', '🇨🇮'],
|
||||
['CK', '🇨🇰'],
|
||||
['CL', '🇨🇱'],
|
||||
['CM', '🇨🇲'],
|
||||
['CN', '🇨🇳'],
|
||||
['CO', '🇨🇴'],
|
||||
['CP', '🇨🇵'],
|
||||
['CR', '🇨🇷'],
|
||||
['CW', '🇨🇼'],
|
||||
['CY', '🇨🇾'],
|
||||
['CZ', '🇨🇿'],
|
||||
['DE', '🇩🇪'],
|
||||
['DG', '🇩🇬'],
|
||||
['DJ', '🇩🇯'],
|
||||
['DK', '🇩🇰'],
|
||||
['DM', '🇩🇲'],
|
||||
['DO', '🇩🇴'],
|
||||
['DZ', '🇩🇿'],
|
||||
['EA', '🇪🇦'],
|
||||
['EC', '🇪🇨'],
|
||||
['EE', '🇪🇪'],
|
||||
['EG', '🇪🇬'],
|
||||
['EH', '🇪🇭'],
|
||||
['ER', '🇪🇷'],
|
||||
['ES', '🇪🇸'],
|
||||
['ET', '🇪🇹'],
|
||||
['FI', '🇫🇮'],
|
||||
['FJ', '🇫🇯'],
|
||||
['FK', '🇫🇰'],
|
||||
['FM', '🇫🇲'],
|
||||
['FO', '🇫🇴'],
|
||||
['FR', '🇫🇷'],
|
||||
['GA', '🇬🇦'],
|
||||
['GB', '🇬🇧'],
|
||||
['GD', '🇬🇩'],
|
||||
['GE', '🇬🇪'],
|
||||
['GF', '🇬🇫'],
|
||||
['GG', '🇬🇬'],
|
||||
['GH', '🇬🇭'],
|
||||
['GI', '🇬🇮'],
|
||||
['GL', '🇬🇱'],
|
||||
['GM', '🇬🇲'],
|
||||
['GN', '🇬🇳'],
|
||||
['GP', '🇬🇵'],
|
||||
['GR', '🇬🇷'],
|
||||
['GS', '🇬🇸'],
|
||||
['GT', '🇬🇹'],
|
||||
['GQ', '🇬🇶'],
|
||||
['GW', '🇬🇼'],
|
||||
['GY', '🇬🇾'],
|
||||
['HK', '🇭🇰'],
|
||||
['HM', '🇭🇲'],
|
||||
['HN', '🇭🇳'],
|
||||
['HR', '🇭🇷'],
|
||||
['HT', '🇭🇹'],
|
||||
['IC', '🇮🇨'],
|
||||
['ID', '🇮🇩'],
|
||||
['IE', '🇮🇪'],
|
||||
['IL', '🇮🇱'],
|
||||
['IM', '🇮🇲'],
|
||||
['IN', '🇮🇳'],
|
||||
['IO', '🇮🇴'],
|
||||
['IR', '🇮🇷'],
|
||||
['IS', '🇮🇸'],
|
||||
['IT', '🇮🇹'],
|
||||
['IQ', '🇮🇶'],
|
||||
['JE', '🇯🇪'],
|
||||
['JM', '🇯🇲'],
|
||||
['JO', '🇯🇴'],
|
||||
['JP', '🇯🇵'],
|
||||
['KE', '🇰🇪'],
|
||||
['KG', '🇰🇬'],
|
||||
['KH', '🇰🇭'],
|
||||
['KI', '🇰🇮'],
|
||||
['KM', '🇰🇲'],
|
||||
['KN', '🇰🇳'],
|
||||
['KP', '🇰🇵'],
|
||||
['KR', '🇰🇷'],
|
||||
['KW', '🇰🇼'],
|
||||
['KY', '🇰🇾'],
|
||||
['KZ', '🇰🇿'],
|
||||
['LA', '🇱🇦'],
|
||||
['LB', '🇱🇧'],
|
||||
['LC', '🇱🇨'],
|
||||
['LI', '🇱🇮'],
|
||||
['LK', '🇱🇰'],
|
||||
['LR', '🇱🇷'],
|
||||
['LS', '🇱🇸'],
|
||||
['LT', '🇱🇹'],
|
||||
['LY', '🇱🇾'],
|
||||
['MA', '🇲🇦'],
|
||||
['MC', '🇲🇨'],
|
||||
['MD', '🇲🇩'],
|
||||
['ME', '🇲🇪'],
|
||||
['MF', '🇲🇫'],
|
||||
['MG', '🇲🇬'],
|
||||
['MH', '🇲🇭'],
|
||||
['MK', '🇲🇰'],
|
||||
['ML', '🇲🇱'],
|
||||
['MM', '🇲🇲'],
|
||||
['MN', '🇲🇳'],
|
||||
['MO', '🇲🇴'],
|
||||
['MP', '🇲🇵'],
|
||||
['MR', '🇲🇷'],
|
||||
['MS', '🇲🇸'],
|
||||
['MT', '🇲🇹'],
|
||||
['MQ', '🇲🇶'],
|
||||
['MW', '🇲🇼'],
|
||||
['MY', '🇲🇾'],
|
||||
['MZ', '🇲🇿'],
|
||||
['NA', '🇳🇦'],
|
||||
['NC', '🇳🇨'],
|
||||
['NE', '🇳🇪'],
|
||||
['NF', '🇳🇫'],
|
||||
['NG', '🇳🇬'],
|
||||
['NI', '🇳🇮'],
|
||||
['NL', '🇳🇱'],
|
||||
['NO', '🇳🇴'],
|
||||
['NP', '🇳🇵'],
|
||||
['NR', '🇳🇷'],
|
||||
['NZ', '🇳🇿'],
|
||||
['OM', '🇴🇲'],
|
||||
['PA', '🇵🇦'],
|
||||
['PE', '🇵🇪'],
|
||||
['PF', '🇵🇫'],
|
||||
['PG', '🇵🇬'],
|
||||
['PH', '🇵🇭'],
|
||||
['PK', '🇵🇰'],
|
||||
['PL', '🇵🇱'],
|
||||
['PM', '🇵🇲'],
|
||||
['PN', '🇵🇳'],
|
||||
['PR', '🇵🇷'],
|
||||
['PS', '🇵🇸'],
|
||||
['PT', '🇵🇹'],
|
||||
['PW', '🇵🇼'],
|
||||
['PY', '🇵🇾'],
|
||||
['RE', '🇷🇪'],
|
||||
['RO', '🇷🇴'],
|
||||
['RS', '🇷🇸'],
|
||||
['RW', '🇷🇼'],
|
||||
['SA', '🇸🇦'],
|
||||
['SB', '🇸🇧'],
|
||||
['SC', '🇸🇨'],
|
||||
['SD', '🇸🇩'],
|
||||
['SE', '🇸🇪'],
|
||||
['SG', '🇸🇬'],
|
||||
['SH', '🇸🇭'],
|
||||
['SI', '🇸🇮'],
|
||||
['SJ', '🇸🇯'],
|
||||
['SK', '🇸🇰'],
|
||||
['SL', '🇸🇱'],
|
||||
['SM', '🇸🇲'],
|
||||
['SN', '🇸🇳'],
|
||||
['SO', '🇸🇴'],
|
||||
['SR', '🇸🇷'],
|
||||
['SS', '🇸🇸'],
|
||||
['ST', '🇸🇹'],
|
||||
['SY', '🇸🇾'],
|
||||
['SZ', '🇸🇿'],
|
||||
['TA', '🇹🇦'],
|
||||
['TC', '🇹🇨'],
|
||||
['TD', '🇹🇩'],
|
||||
['TF', '🇹🇫'],
|
||||
['TG', '🇹🇬'],
|
||||
['TH', '🇹🇭'],
|
||||
['TJ', '🇹🇯'],
|
||||
['TK', '🇹🇰'],
|
||||
['TL', '🇹🇱'],
|
||||
['TM', '🇹🇲'],
|
||||
['TN', '🇹🇳'],
|
||||
['TO', '🇹🇴'],
|
||||
['TR', '🇹🇷'],
|
||||
['TT', '🇹🇹'],
|
||||
['TW', '🇹🇼'],
|
||||
['TZ', '🇹🇿'],
|
||||
['QA', '🇶🇦'],
|
||||
['VG', '🇻🇬'],
|
||||
['WF', '🇼🇫'],
|
||||
['WS', '🇼🇸'],
|
||||
['YE', '🇾🇪'],
|
||||
['YT', '🇾🇹'],
|
||||
['ZA', '🇿🇦'],
|
||||
['ZM', '🇿🇲'],
|
||||
['ZW', '🇿🇼'],
|
||||
// unknown
|
||||
['AA', FALLBACK_FLAG],
|
||||
['XX', FALLBACK_FLAG],
|
||||
['AAA', FALLBACK_FLAG],
|
||||
];
|
||||
|
||||
describe('countryCodeToFlagEmoji', () => {
|
||||
it.each(cases)('converts %s to %s', (countryCode, flag) => {
|
||||
expect(countryCodeToFlagEmoji(countryCode)).toEqual(flag);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import compact from 'lodash/compact';
|
||||
|
||||
export const FALLBACK_FLAG = '🏳';
|
||||
|
||||
const KNOWN_CODES = `AC AD AE AF AG AI AL AM AO AR AS AT AQ AW AZ BA BB BD BE BF
|
||||
BG BH BI BJ BL BM BN BO BR BS BT BQ BW BY BZ CA CC CD CF CG CH CI CK CL CM CN
|
||||
CO CP CR CW CY CZ DE DG DJ DK DM DO DZ EA EC EE EG EH ER ES ET FI FJ FK FM FO
|
||||
FR GA GB GD GE GF GG GH GI GL GM GN GP GR GS GT GQ GW GY HK HM HN HR HT IC ID
|
||||
IE IL IM IN IO IR IS IT IQ JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB
|
||||
LC LI LK LR LS LT LY MA MC MD ME MF MG MH MK ML MM MN MO MP MR MS MT MQ MW MY
|
||||
MZ NA NC NE NF NG NI NL NO NP NR NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW
|
||||
PY RE RO RS RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SY SZ TA TC
|
||||
TD TF TG TH TJ TK TL TM TN TO TR TT TW TZ QA VG WF WS YE YT ZA ZM ZW`;
|
||||
|
||||
export const countryCodeToFlagEmoji = (countryCode: string) => {
|
||||
const code = countryCode.trim().toUpperCase();
|
||||
const known = compact(KNOWN_CODES.split(' ').map((ch) => ch.trim()));
|
||||
if (known.includes(code)) {
|
||||
return code.replace(/./g, (char) =>
|
||||
String.fromCodePoint(0x1f1a5 + char.toUpperCase().charCodeAt(0))
|
||||
);
|
||||
}
|
||||
return FALLBACK_FLAG;
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { ICellRendererParams } from 'ag-grid-community';
|
||||
import classNames from 'classnames';
|
||||
import { memo } from 'react';
|
||||
import { BID_COLOR, ASK_COLOR } from './vol-cell';
|
||||
import { NumericCell } from './numeric-cell';
|
||||
import { addDecimalsFormatNumber } from '../format';
|
||||
|
||||
export interface CumulativeVolProps {
|
||||
@@ -15,11 +14,7 @@ export interface CumulativeVolProps {
|
||||
positionDecimalPlaces: number;
|
||||
}
|
||||
|
||||
export interface ICumulativeVolCellProps extends ICellRendererParams {
|
||||
value: CumulativeVolProps;
|
||||
}
|
||||
|
||||
export const CumulativeVol = React.memo(
|
||||
export const CumulativeVol = memo(
|
||||
({
|
||||
relativeAsk,
|
||||
relativeBid,
|
||||
@@ -27,7 +22,6 @@ export const CumulativeVol = React.memo(
|
||||
bid,
|
||||
indicativeVolume,
|
||||
testId,
|
||||
className,
|
||||
positionDecimalPlaces,
|
||||
}: CumulativeVolProps) => {
|
||||
const askBar = relativeAsk ? (
|
||||
@@ -40,7 +34,7 @@ export const CumulativeVol = React.memo(
|
||||
backgroundColor: ASK_COLOR,
|
||||
opacity: 0.6,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
) : null;
|
||||
const bidBar = relativeBid ? (
|
||||
<div
|
||||
@@ -53,25 +47,48 @@ export const CumulativeVol = React.memo(
|
||||
backgroundColor: BID_COLOR,
|
||||
opacity: 0.6,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const volume = indicativeVolume ? (
|
||||
<span className="relative">
|
||||
({addDecimalsFormatNumber(indicativeVolume, positionDecimalPlaces ?? 0)}
|
||||
<span>
|
||||
(
|
||||
<NumericCell
|
||||
value={Number(indicativeVolume)}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
indicativeVolume,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
/>
|
||||
)
|
||||
</span>
|
||||
) : (
|
||||
<span className="relative">
|
||||
{ask ? addDecimalsFormatNumber(ask, positionDecimalPlaces ?? 0) : null}
|
||||
<span>
|
||||
{ask ? (
|
||||
<NumericCell
|
||||
value={ask}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
ask,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{ask && bid ? '/' : null}
|
||||
{bid ? addDecimalsFormatNumber(bid, positionDecimalPlaces ?? 0) : null}
|
||||
{bid ? (
|
||||
<NumericCell
|
||||
value={ask}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
bid,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('h-full relative', className)}
|
||||
className="relative font-mono pr-1"
|
||||
data-testid={testId || 'cumulative-vol'}
|
||||
>
|
||||
{askBar}
|
||||
@@ -83,9 +100,3 @@ export const CumulativeVol = React.memo(
|
||||
);
|
||||
|
||||
CumulativeVol.displayName = 'CumulativeVol';
|
||||
|
||||
export const CumulativeVolCell = ({ value }: ICumulativeVolCellProps) => (
|
||||
<CumulativeVol {...value} />
|
||||
);
|
||||
|
||||
CumulativeVolCell.displayName = 'CumulativeVolCell';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { DateRangeFilterProps } from './date-range-filter';
|
||||
import { DateRangeFilter } from './date-range-filter';
|
||||
|
||||
const commonProps = {
|
||||
filterChangedCallback: jest.fn(),
|
||||
};
|
||||
|
||||
describe('DateRangeFilter', () => {
|
||||
it('should be properly rendered', async () => {
|
||||
const defaultRangeFilter = {
|
||||
start: '2023-02-14T13:53:01+01:00',
|
||||
end: '2023-02-21T13:53:01+01:00',
|
||||
};
|
||||
const displayStartValue = '2023-02-14T12:53:01.000';
|
||||
const displayEndValue = '2023-02-21T12:53:01.000';
|
||||
render(
|
||||
<DateRangeFilter
|
||||
{...(commonProps as unknown as DateRangeFilterProps)}
|
||||
defaultRangeFilter={defaultRangeFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('Start')).toHaveValue(displayStartValue);
|
||||
expect(screen.getByLabelText('End')).toHaveValue(displayEndValue);
|
||||
|
||||
expect(commonProps.filterChangedCallback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,113 +1,259 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
||||
import { formatForInput } from '../format/date';
|
||||
import {
|
||||
isBefore,
|
||||
subDays,
|
||||
addDays,
|
||||
differenceInDays,
|
||||
formatRFC3339,
|
||||
min,
|
||||
max,
|
||||
isValid,
|
||||
} from 'date-fns';
|
||||
import { t } from '../i18n';
|
||||
import { formatForInput } from '../format/date';
|
||||
|
||||
const defaultFilterValue: Schema.DateRange = {};
|
||||
export interface DateRangeFilterProps extends IFilterParams {
|
||||
defaultRangeFilter?: Schema.DateRange;
|
||||
maxSubDays?: number;
|
||||
maxNextDays?: number;
|
||||
maxDaysRange?: number;
|
||||
}
|
||||
|
||||
export const DateRangeFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
const [value, setValue] = useState<Schema.DateRange>(defaultFilterValue);
|
||||
export const DateRangeFilter = forwardRef(
|
||||
(props: DateRangeFilterProps, ref) => {
|
||||
const defaultDates = props?.defaultRangeFilter || defaultFilterValue;
|
||||
const [value, setValue] = useState<Schema.DateRange>(defaultDates);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [minStartDate, maxStartDate, minEndDate, maxEndDate] = useMemo(() => {
|
||||
const minStartDate =
|
||||
props?.maxSubDays !== undefined
|
||||
? formatForInput(subDays(Date.now(), props.maxSubDays))
|
||||
: '';
|
||||
const maxStartDate =
|
||||
props?.maxNextDays !== undefined
|
||||
? formatForInput(addDays(Date.now(), props.maxNextDays))
|
||||
: '';
|
||||
const minEndDate =
|
||||
value.start && props?.maxDaysRange !== undefined
|
||||
? formatForInput(new Date(value.start))
|
||||
: minStartDate || value.start
|
||||
? formatForInput(new Date(value.start))
|
||||
: '';
|
||||
const endDateCandidates = [];
|
||||
if (props.maxNextDays !== undefined) {
|
||||
endDateCandidates.push(addDays(new Date(), props.maxNextDays));
|
||||
}
|
||||
if (props.maxDaysRange !== undefined && value.start) {
|
||||
endDateCandidates.push(
|
||||
addDays(new Date(value.start), props.maxDaysRange)
|
||||
);
|
||||
}
|
||||
const maxEndDate = endDateCandidates.length
|
||||
? formatForInput(min(endDateCandidates))
|
||||
: maxStartDate;
|
||||
return [minStartDate, maxStartDate, minEndDate, maxEndDate];
|
||||
}, [props.maxSubDays, props.maxDaysRange, props.maxNextDays, value.start]);
|
||||
// expose AG Grid Filter Lifecycle callbacks
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
doesFilterPass(params: IDoesFilterPassParams) {
|
||||
const { api, colDef, column, columnApi, context } = props;
|
||||
const { node } = params;
|
||||
const rowValue = props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
});
|
||||
if (
|
||||
value.start &&
|
||||
rowValue &&
|
||||
new Date(rowValue) <= new Date(value.start)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
value.end &&
|
||||
rowValue &&
|
||||
new Date(rowValue) >= new Date(value.end)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// expose AG Grid Filter Lifecycle callbacks
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
doesFilterPass(params: IDoesFilterPassParams) {
|
||||
const { api, colDef, column, columnApi, context } = props;
|
||||
const { node } = params;
|
||||
const rowValue = props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
});
|
||||
if (
|
||||
value.start &&
|
||||
rowValue &&
|
||||
new Date(rowValue) <= new Date(value.start)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
value.end &&
|
||||
rowValue &&
|
||||
new Date(rowValue) >= new Date(value.end)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
isFilterActive() {
|
||||
return value.start || value.end;
|
||||
},
|
||||
|
||||
isFilterActive() {
|
||||
return value.start || value.end;
|
||||
},
|
||||
getModel() {
|
||||
if (!this.isFilterActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getModel() {
|
||||
if (!this.isFilterActive()) {
|
||||
return null;
|
||||
}
|
||||
return { value };
|
||||
},
|
||||
|
||||
return { value };
|
||||
},
|
||||
|
||||
setModel(model?: { value: Schema.DateRange } | null) {
|
||||
setValue(model?.value || defaultFilterValue);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue({
|
||||
...value,
|
||||
[event.target.name]:
|
||||
event.target.value &&
|
||||
new Date(event.target.value).toISOString().replace('Z', '000000Z'),
|
||||
setModel(model?: { value: Schema.DateRange } | null) {
|
||||
setValue(
|
||||
model?.value || props?.defaultRangeFilter || defaultFilterValue
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
const validate = (
|
||||
name: string,
|
||||
timeValue: Date,
|
||||
update?: Schema.DateRange
|
||||
) => {
|
||||
if (
|
||||
props.maxSubDays !== undefined &&
|
||||
isBefore(new Date(timeValue), subDays(Date.now(), props.maxSubDays + 1))
|
||||
) {
|
||||
setError(
|
||||
t(
|
||||
'The earliest data that can be queried is %s days ago.',
|
||||
String(props.maxSubDays)
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (props?.maxDaysRange !== undefined) {
|
||||
const contrvalue =
|
||||
name === 'start'
|
||||
? update?.end || value.end
|
||||
: update?.start || value.start;
|
||||
if (
|
||||
Math.abs(
|
||||
differenceInDays(new Date(timeValue), new Date(contrvalue))
|
||||
) > props.maxDaysRange
|
||||
) {
|
||||
setError(
|
||||
t(
|
||||
'The maximum time range that can be queried is %s days.',
|
||||
String(props.maxDaysRange)
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props?.filterChangedCallback();
|
||||
}, [value, props]);
|
||||
const checkForEndDate = (
|
||||
endDate: Date | undefined,
|
||||
startDate: Date | undefined
|
||||
) => {
|
||||
const endDateCandidates: Date[] = [];
|
||||
if (props.maxDaysRange !== undefined && isValid(startDate)) {
|
||||
endDateCandidates.push(addDays(startDate as Date, props.maxDaysRange));
|
||||
}
|
||||
if (props.maxNextDays !== undefined) {
|
||||
endDateCandidates.push(addDays(Date.now(), props.maxNextDays));
|
||||
}
|
||||
if (isValid(endDate)) {
|
||||
endDateCandidates.push(endDate as Date);
|
||||
}
|
||||
return endDate && startDate
|
||||
? formatRFC3339(max([startDate, min(endDateCandidates)]))
|
||||
: undefined;
|
||||
};
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value: dateValue, name } = event.target;
|
||||
const date = new Date(dateValue || defaultDates[name as 'start' | 'end']);
|
||||
let update = { [name]: isValid(date) ? formatRFC3339(date) : undefined };
|
||||
const startCheckDate = name === 'start' ? date : new Date(value.start);
|
||||
const endCheckDate =
|
||||
name === 'start'
|
||||
? new Date(value.end)
|
||||
: isValid(date)
|
||||
? date
|
||||
: new Date(maxEndDate);
|
||||
const endDate = isValid(endCheckDate) ? endCheckDate : undefined;
|
||||
const startDate = isValid(startCheckDate) ? startCheckDate : undefined;
|
||||
update = { ...update, end: checkForEndDate(endDate, startDate) };
|
||||
|
||||
const start = (value.start && formatForInput(new Date(value.start))) || '';
|
||||
const end = (value.end && formatForInput(new Date(value.end))) || '';
|
||||
return (
|
||||
<div className="ag-filter-body-wrapper">
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
<label className="block" key="start">
|
||||
<span className="block">{t('Start')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="start"
|
||||
value={start}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="block" key="end">
|
||||
<span className="block">{t('End')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="end"
|
||||
value={end}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div className="ag-filter-apply-panel">
|
||||
<button
|
||||
type="button"
|
||||
className="ag-standard-button ag-filter-apply-panel-button"
|
||||
onClick={() => setValue(defaultFilterValue)}
|
||||
if (validate(name, date, update)) {
|
||||
setValue((curr) => ({
|
||||
...curr,
|
||||
...update,
|
||||
}));
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
props?.filterChangedCallback();
|
||||
}, [value, props]);
|
||||
|
||||
const notification = useMemo(() => {
|
||||
const not = error ? (
|
||||
<div
|
||||
className="text-sm flex items-center first-letter:uppercase mt-2 border-danger text-danger"
|
||||
role="alert"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
{error}
|
||||
</div>
|
||||
) : null;
|
||||
return (
|
||||
<div className="ag-filter-apply-panel flex min-h-[2rem]">{not}</div>
|
||||
);
|
||||
}, [error]);
|
||||
|
||||
const start = (value.start && formatForInput(new Date(value.start))) || '';
|
||||
const end = (value.end && formatForInput(new Date(value.end))) || '';
|
||||
return (
|
||||
<div className="ag-filter-body-wrapper inline-block min-w-fit">
|
||||
{notification}
|
||||
<div className="ag-filter-apply-panel">
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
<label className="block" key="start">
|
||||
<span className="block mb-1">{t('Start')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="start"
|
||||
value={start || ''}
|
||||
onChange={onChange}
|
||||
min={minStartDate}
|
||||
max={maxStartDate}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
<label className="block" key="end">
|
||||
<span className="block mb-1">{t('End')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="end"
|
||||
value={end || ''}
|
||||
onChange={onChange}
|
||||
min={minEndDate}
|
||||
max={maxEndDate}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="ag-filter-apply-panel">
|
||||
<button
|
||||
type="button"
|
||||
className="ag-standard-button ag-filter-apply-panel-button"
|
||||
onClick={() => {
|
||||
setError('');
|
||||
setValue(defaultDates);
|
||||
}}
|
||||
>
|
||||
{t('Reset')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './summary-rows';
|
||||
export * from './vol-cell';
|
||||
export * from './set-filter';
|
||||
export * from './date-range-filter';
|
||||
export * from './numeric-cell';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
import { NumericCell } from './numeric-cell';
|
||||
|
||||
describe('NumericCell', () => {
|
||||
const testId = 'cell';
|
||||
it('Displays formatted value', () => {
|
||||
const valueFormatted = '111.00';
|
||||
render(
|
||||
<NumericCell
|
||||
value={111}
|
||||
valueFormatted={valueFormatted}
|
||||
testId={testId}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId(testId)).toHaveTextContent(valueFormatted);
|
||||
expect(screen.getByText('00')).toBeInTheDocument();
|
||||
expect(screen.getByText('00')).toHaveClass('opacity-60');
|
||||
});
|
||||
|
||||
it('Displays 0', () => {
|
||||
render(<NumericCell value={0} valueFormatted="0.00" testId={testId} />);
|
||||
expect(screen.getByTestId(testId)).toHaveTextContent('0.00');
|
||||
});
|
||||
|
||||
it('Displays - if value is not a number', () => {
|
||||
render(<NumericCell value={null} valueFormatted="" testId={testId} />);
|
||||
expect(screen.getByTestId(testId)).toHaveTextContent('-');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { getDecimalSeparator, isNumeric } from '../format';
|
||||
|
||||
interface NumericCellProps {
|
||||
value: number | bigint | null | undefined;
|
||||
valueFormatted: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a numeric value in a consistent way for data grid
|
||||
* use, right aligned, monospace and decimals deemphasised
|
||||
*/
|
||||
export const NumericCell = forwardRef<HTMLSpanElement, NumericCellProps>(
|
||||
({ value, valueFormatted, testId }, ref) => {
|
||||
if (!isNumeric(value)) {
|
||||
return (
|
||||
<span ref={ref} data-testid={testId}>
|
||||
-
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const decimalSeparator = getDecimalSeparator();
|
||||
const valueSplit: string[] = decimalSeparator
|
||||
? valueFormatted.split(decimalSeparator).map((v) => `${v}`)
|
||||
: [`${value}`];
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
|
||||
data-testid={testId}
|
||||
title={valueFormatted}
|
||||
>
|
||||
{valueSplit[0]}
|
||||
{valueSplit[1] ? decimalSeparator : null}
|
||||
{valueSplit[1] ? (
|
||||
<span className="opacity-60">{valueSplit[1]}</span>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,15 +1,15 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import * as React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { PriceCell } from './price-cell';
|
||||
|
||||
describe('<PriceCell />', () => {
|
||||
describe('PriceCell', () => {
|
||||
it('Displays formatted value', () => {
|
||||
render(<PriceCell value={100} valueFormatted="100.00" />);
|
||||
expect(screen.getByTestId('price')).toHaveTextContent('100.00');
|
||||
expect(screen.getByTestId('price')).toHaveAttribute('title', '100.00');
|
||||
});
|
||||
|
||||
it('Displays 0', () => {
|
||||
render(<PriceCell value={0} valueFormatted="0.00" />);
|
||||
expect(screen.getByTestId('price')).toHaveTextContent('0.00');
|
||||
@@ -19,4 +19,14 @@ describe('<PriceCell />', () => {
|
||||
render(<PriceCell value={null} valueFormatted="" />);
|
||||
expect(screen.getByTestId('price')).toHaveTextContent('-');
|
||||
});
|
||||
|
||||
it('can be clicked if given an onClick handler', async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
const value = 100;
|
||||
render(
|
||||
<PriceCell value={value} valueFormatted="100.00" onClick={mockOnClick} />
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('price'));
|
||||
expect(mockOnClick).toHaveBeenCalledWith(value);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { memo, forwardRef } from 'react';
|
||||
import { getDecimalSeparator, isNumeric } from '../format';
|
||||
import { isNumeric } from '../format';
|
||||
import { NumericCell } from './numeric-cell';
|
||||
export interface IPriceCellProps {
|
||||
value: number | bigint | null | undefined;
|
||||
valueFormatted: string;
|
||||
@@ -17,41 +18,23 @@ export const PriceCell = memo(
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const decimalSeparator = getDecimalSeparator();
|
||||
const valueSplit: string[] = decimalSeparator
|
||||
? valueFormatted.split(decimalSeparator).map((v) => `${v}`)
|
||||
: [`${value}`];
|
||||
return onClick ? (
|
||||
<button
|
||||
onClick={() => onClick(value)}
|
||||
className="hover:dark:bg-neutral-800 hover:bg-neutral-200"
|
||||
className="hover:dark:bg-neutral-800 hover:bg-neutral-200 text-right"
|
||||
>
|
||||
<span
|
||||
ref={ref}
|
||||
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
|
||||
data-testid={testId || 'price'}
|
||||
title={valueFormatted}
|
||||
>
|
||||
{valueSplit[0]}
|
||||
{valueSplit[1] ? decimalSeparator : null}
|
||||
{valueSplit[1] ? (
|
||||
<span className="opacity-60">{valueSplit[1]}</span>
|
||||
) : null}
|
||||
</span>
|
||||
<NumericCell
|
||||
value={value}
|
||||
valueFormatted={valueFormatted}
|
||||
testId={testId || 'price'}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
ref={ref}
|
||||
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
|
||||
data-testid={testId || 'price'}
|
||||
title={valueFormatted}
|
||||
>
|
||||
{valueSplit[0]}
|
||||
{valueSplit[1] ? decimalSeparator : null}
|
||||
{valueSplit[1] ? (
|
||||
<span className="opacity-60">{valueSplit[1]}</span>
|
||||
) : null}
|
||||
</span>
|
||||
<NumericCell
|
||||
value={value}
|
||||
valueFormatted={valueFormatted}
|
||||
testId={testId || 'price'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
||||
import { t } from '../i18n';
|
||||
|
||||
export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
const [value, setValue] = useState<string[]>([]);
|
||||
@@ -16,18 +17,19 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
doesFilterPass(params: IDoesFilterPassParams) {
|
||||
const { api, colDef, column, columnApi, context } = props;
|
||||
const { node } = params;
|
||||
return (
|
||||
props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
}) === value
|
||||
);
|
||||
const getValue = props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
});
|
||||
return Array.isArray(value)
|
||||
? value.includes(getValue)
|
||||
: getValue === value;
|
||||
},
|
||||
|
||||
isFilterActive() {
|
||||
@@ -83,7 +85,7 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
className="ag-standard-button ag-filter-apply-panel-button"
|
||||
onClick={() => setValue([])}
|
||||
>
|
||||
Reset
|
||||
{t('Reset')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import type { ICellRendererParams } from 'ag-grid-community';
|
||||
import { PriceCell } from './price-cell';
|
||||
import classNames from 'classnames';
|
||||
import * as tailwind from '@vegaprotocol/tailwindcss-config';
|
||||
import { NumericCell } from './numeric-cell';
|
||||
|
||||
export enum VolumeType {
|
||||
bid,
|
||||
@@ -43,17 +43,11 @@ export const Vol = React.memo(
|
||||
backgroundColor: type === VolumeType.bid ? BID_COLOR : ASK_COLOR,
|
||||
opacity: type === VolumeType.bid ? 0.6 : 0.6,
|
||||
}}
|
||||
></div>
|
||||
<PriceCell value={value} valueFormatted={valueFormatted} />
|
||||
/>
|
||||
<NumericCell value={value} valueFormatted={valueFormatted} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Vol.displayName = 'Vol';
|
||||
|
||||
export const VolCell = ({ value, valueFormatted }: IVolCellProps) => (
|
||||
<Vol value={value} {...valueFormatted} />
|
||||
);
|
||||
|
||||
VolCell.displayName = 'VolCell';
|
||||
|
||||
@@ -19,3 +19,4 @@ export * from './remove-pagination-wrapper';
|
||||
export * from './storage';
|
||||
export * from './time';
|
||||
export * from './validate';
|
||||
export * from './flag-emoji';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user