Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e76e05f16f | ||
|
|
4bb78e9891 | ||
|
|
79f360dc24 | ||
|
|
a3fcd6b7dc | ||
|
|
23ce480daa | ||
|
|
da209d96b0 | ||
|
|
9166a85a55 | ||
|
|
a5ec1f708f | ||
|
|
3afdc0c8da | ||
|
|
757d545d97 | ||
|
|
fc4d40f3cf | ||
|
|
ff47eda692 | ||
|
|
7483d9ce1d | ||
|
|
f9209cf327 | ||
|
|
420a29680d | ||
|
|
19c9e79647 | ||
|
|
4cd856fcb8 | ||
|
|
649cc61280 | ||
|
|
35f47b75ac | ||
|
|
0697acdac9 | ||
|
|
92ec7166bc | ||
|
|
56b5214dbf | ||
|
|
94509a29c5 | ||
|
|
4e2b6b2d04 | ||
|
|
c29087cc96 | ||
|
|
5bde096977 | ||
|
|
923dc09eb6 | ||
|
|
4fbdb337dd | ||
|
|
6f4a5b9097 | ||
|
|
0c26d99ce2 | ||
|
|
5aedeba4ff | ||
|
|
5789d3496b | ||
|
|
d95fdca0d4 | ||
|
|
e4bf61c2e2 | ||
|
|
0ba54cd8a4 | ||
|
|
eee85c231b | ||
|
|
afdb387742 | ||
|
|
63698fbb80 | ||
|
|
58d8f7857e | ||
|
|
0931730582 | ||
|
|
3aefa10a4f |
@@ -18,9 +18,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 16.15.1
|
||||
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v4
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,12 +19,12 @@ const EmptyList = ({ heading, label }: EmptyListProps) => {
|
||||
|
||||
<div className="mt-4">
|
||||
{heading ? (
|
||||
<h1 className="font-alpha text-xl uppercase text-center leading-relaxed">
|
||||
<h1 className="font-alpha calt text-xl uppercase text-center leading-relaxed">
|
||||
{heading}
|
||||
</h1>
|
||||
) : null}
|
||||
{label ? (
|
||||
<p className="font-alpha text-gray-500 text-center">{label}</p>
|
||||
<p className="font-alpha calt text-gray-500 text-center">{label}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@ const MarketLink = ({
|
||||
<Link
|
||||
className="underline"
|
||||
{...props}
|
||||
to={`/${Routes.MARKETS}#${id}`}
|
||||
to={`/${Routes.MARKETS}/${id}`}
|
||||
title={id}
|
||||
>
|
||||
{label}
|
||||
@@ -56,7 +56,7 @@ const MarketLink = ({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
|
||||
<Link className="underline" {...props} to={`/${Routes.MARKETS}/${id}`}>
|
||||
<Hash text={id} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -22,8 +22,7 @@ export const MarketDetails = ({
|
||||
}: {
|
||||
market: MarketInfoNoCandlesQuery['market'];
|
||||
}) => {
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol;
|
||||
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
@@ -39,6 +38,10 @@ export const MarketDetails = ({
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
|
||||
const panels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
@@ -179,32 +182,41 @@ export const MarketDetails = ({
|
||||
{
|
||||
title: t('Liquidity price range'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
liquidityPriceRange: formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
),
|
||||
LPVolumeMin:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
LPVolumeMax:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
<>
|
||||
<p className="text-xs mb-4">
|
||||
{`For liquidity orders count towards a commitment they have to be
|
||||
within either the liquidity or price monitoring bounds (whichever is
|
||||
tighter).`}
|
||||
</p>
|
||||
<p className="text-xs mb-4">
|
||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
||||
price.`}
|
||||
</p>
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -237,7 +249,7 @@ export const MarketDetails = ({
|
||||
<>
|
||||
{panels.map((p) => (
|
||||
<div className="mb-3">
|
||||
<h2 className="font-alpha text-xl">{p.title}</h2>
|
||||
<h2 className="font-alpha calt text-xl">{p.title}</h2>
|
||||
{p.content}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -83,7 +83,7 @@ const NestedDataListItem = ({
|
||||
});
|
||||
|
||||
const titleClasses = classNames({
|
||||
'text-xl pl-4 border-l-4 font-alpha': hasChildren,
|
||||
'text-xl pl-4 border-l-4 font-alpha calt': hasChildren,
|
||||
'text-base font-medium whitespace-nowrap': !hasChildren,
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export const PageHeader = ({
|
||||
copy = false,
|
||||
className,
|
||||
}: PageHeaderProps) => {
|
||||
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha';
|
||||
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha calt';
|
||||
return (
|
||||
<header className={className}>
|
||||
<span className={`${titleClasses} block`}>{prefix}</span>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -13,7 +13,7 @@ export const RouteTitle = ({
|
||||
...props
|
||||
}: RouteTitleProps) => {
|
||||
const classes = classnames(
|
||||
'font-alpha',
|
||||
'font-alpha calt',
|
||||
'text-4xl',
|
||||
'uppercase',
|
||||
'mb-8',
|
||||
|
||||
@@ -11,7 +11,7 @@ export const StatusMessage = ({
|
||||
className,
|
||||
...props
|
||||
}: StatusMessageProps) => {
|
||||
const classes = classnames('font-alpha text-2xl mb-28', className);
|
||||
const classes = classnames('font-alpha calt text-2xl mb-28', className);
|
||||
return (
|
||||
<h3 className={classes} {...props}>
|
||||
{children}
|
||||
|
||||
@@ -12,7 +12,7 @@ export const SubHeading = ({
|
||||
...props
|
||||
}: SubHeadingProps) => {
|
||||
const classes = classnames(
|
||||
'font-alpha',
|
||||
'font-alpha calt',
|
||||
'text-2xl',
|
||||
'uppercase',
|
||||
'mt-8 mb-2',
|
||||
|
||||
@@ -9,7 +9,7 @@ export type Metric = components['schemas']['vegaDispatchMetric'];
|
||||
export const wrapperClasses =
|
||||
'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
|
||||
export const headerClasses =
|
||||
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha';
|
||||
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha calt';
|
||||
|
||||
export type Transfer = components['schemas']['commandsv1Transfer'];
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -68,7 +68,7 @@ const Party = () => {
|
||||
return (
|
||||
<section>
|
||||
<h1
|
||||
className="font-alpha uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
|
||||
className="font-alpha calt uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
|
||||
data-testid="parties-header"
|
||||
>
|
||||
{t('Public key')}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ export function Dashboard() {
|
||||
<>
|
||||
<div className="px-16 pt-20 pb-12 bg-greys-light-100">
|
||||
<div className="max-w-screen-xl mx-auto">
|
||||
<h1 className="font-alpha uppercase text-5xl mb-8">
|
||||
<h1 className="font-alpha calt uppercase text-5xl mb-8">
|
||||
{t('Top liquidity opportunities')}
|
||||
</h1>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ type Network = 'testnet' | 'mainnet';
|
||||
export const Intro = ({ network = 'testnet' }: { network?: Network }) => {
|
||||
return (
|
||||
<div>
|
||||
<p className="font-alpha text-2xl font-medium mb-2">
|
||||
<p className="font-alpha calt text-2xl font-medium mb-2">
|
||||
{t(
|
||||
'Become a liquidity provider and earn a cut of the fees paid during trading.'
|
||||
)}
|
||||
|
||||
@@ -92,7 +92,7 @@ export const Detail = () => {
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-alpha text-2xl mb-4">
|
||||
<h2 className="font-alpha calt text-2xl mb-4">
|
||||
{t('Current Liquidity Provision')}
|
||||
</h2>
|
||||
<LPProvidersGrid
|
||||
|
||||
@@ -14,13 +14,13 @@ export const Header = ({
|
||||
<div className="mb-6">
|
||||
<Link to="/">
|
||||
<Icon name="chevron-left" className="mr-2" />
|
||||
<span className="underline font-alpha text-lg font-medium">
|
||||
<span className="underline font-alpha calt text-lg font-medium">
|
||||
{t('Liquidity opportunities')}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<h1 className="font-alpha text-5xl mb-6">{name}</h1>
|
||||
<p className="font-alpha text-4xl">{symbol}</p>
|
||||
<h1 className="font-alpha calt text-5xl mb-6">{name}</h1>
|
||||
<p className="font-alpha calt text-4xl">{symbol}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ export const Market = ({
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr
|
||||
className="text-sm text-greys-light-400 text-left font-alpha"
|
||||
className="text-sm text-greys-light-400 text-left font-alpha calt"
|
||||
style={{ fontFeatureSettings: "'liga' off, 'calt' off" }}
|
||||
>
|
||||
<th className="font-medium px-4">{t('Volume (24h)')}</th>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const Grid = ({ isRowClickable, children, ...props }: Props) => {
|
||||
|
||||
return (
|
||||
<AgGridReact
|
||||
className={classNames('ag-theme-alpine h-full font-alpha', {
|
||||
className={classNames('ag-theme-alpine h-full font-alpha calt', {
|
||||
'row-hover': isRowClickable,
|
||||
})}
|
||||
rowHeight={92}
|
||||
|
||||
@@ -13,7 +13,7 @@ const Remainder = () => (
|
||||
);
|
||||
|
||||
const COPY_CLASS =
|
||||
'text-sm font-medium whitespace-nowrap text-white font-alpha';
|
||||
'text-sm font-medium whitespace-nowrap text-white font-alpha calt';
|
||||
|
||||
const Tooltip = ({
|
||||
children,
|
||||
|
||||
+6
-8
@@ -59,10 +59,10 @@ const ROWS = [
|
||||
export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
|
||||
return (
|
||||
<Dialog size="medium" open={isOpen} onChange={onChange}>
|
||||
<h1 className="text-2xl mb-5 pr-2 font-medium font-alpha uppercase liga-0-calt-0">
|
||||
<h1 className="text-2xl mb-5 pr-2 font-medium font-alpha uppercase">
|
||||
{t('Health')}
|
||||
</h1>
|
||||
<p className="text-lg font-medium font-alpha mb-8 liga-0-calt-0">
|
||||
<p className="text-lg font-medium font-alpha mb-8">
|
||||
{t(
|
||||
'Market health is a representation of market and liquidity status and how close that market is to moving from one fee level to another.'
|
||||
)}
|
||||
@@ -70,10 +70,10 @@ export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
|
||||
|
||||
<table className="table-fixed">
|
||||
<thead className="border-b border-greys-light-300">
|
||||
<th className="w-1/2 text-left font-medium font-alpha text-base pb-4 uppercase liga-0-calt-0">
|
||||
<th className="w-1/2 text-left font-medium font-alpha text-base pb-4 uppercase">
|
||||
{t('Market status')}
|
||||
</th>
|
||||
<th className="w-1/2 text-lef font-medium font-alpha text-base pb-4 uppercase liga-0-calt-0">
|
||||
<th className="w-1/2 text-lef font-medium font-alpha text-base pb-4 uppercase">
|
||||
{t('Liquidity status')}
|
||||
</th>
|
||||
</thead>
|
||||
@@ -85,12 +85,10 @@ export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
|
||||
<td
|
||||
className={classNames('pr-4 pb-10', { 'pt-8': isFirstRow })}
|
||||
>
|
||||
<h2 className="font-medium font-alpha uppercase text-base liga-0-calt-0">
|
||||
<h2 className="font-medium font-alpha uppercase text-base">
|
||||
{t(r.title)}
|
||||
</h2>
|
||||
<p className="font-medium font-alpha text-lg liga-0-calt-0">
|
||||
{t(r.copy)}
|
||||
</p>
|
||||
<p className="font-medium font-alpha text-lg">{t(r.copy)}</p>
|
||||
</td>
|
||||
<td
|
||||
className={classNames('pl-4 pb-10', { 'pt-8': isFirstRow })}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
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>
|
||||
|
||||
@@ -13,7 +13,7 @@ export const NoRewards = () => {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<SubHeading title={t('noRewardsHaveBeenDistributedYet')} />
|
||||
<p className="font-alpha text-xl">{t('checkBackSoon')}</p>
|
||||
<p className="font-alpha calt text-xl">{t('checkBackSoon')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -22,6 +22,7 @@ module.exports = defineConfig({
|
||||
viewportHeight: 900,
|
||||
responseTimeout: 50000,
|
||||
requestTimeout: 20000,
|
||||
retries: 2,
|
||||
},
|
||||
env: {
|
||||
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
|
||||
|
||||
@@ -72,7 +72,11 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
||||
cy.getByTestId('deposit-approve-submit').click();
|
||||
cy.getByTestId('approve-warning').should(
|
||||
'contain.text',
|
||||
`Deposits of ${btcSymbol} not approved`
|
||||
);
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
||||
cy.get('[data-testid="Return to deposit"]').click();
|
||||
cy.get(amountField).clear().type('10');
|
||||
@@ -186,6 +190,24 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
|
||||
it('shows node health', function () {
|
||||
const market = this.market;
|
||||
cy.visit(`/#/markets/${market.id}`);
|
||||
cy.getByTestId('node-health')
|
||||
.children()
|
||||
.first()
|
||||
.should('contain.text', 'Operational')
|
||||
.next()
|
||||
.should('contain.text', new URL(Cypress.env('VEGA_URL')).origin)
|
||||
.next()
|
||||
.then(($el) => {
|
||||
const blockHeight = parseInt($el.text());
|
||||
// block height will increase over the course of the test run so best
|
||||
// we can do here is check that its showing something sensible
|
||||
expect(blockHeight).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('can place and receive an order', function () {
|
||||
const market = this.market;
|
||||
cy.visit(`/#/markets/${market.id}`);
|
||||
@@ -407,7 +429,7 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField, txTimeout).select(vegaName, { force: true });
|
||||
cy.getByTestId('deposit-approve-submit').click();
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
||||
cy.get('[data-testid="Return to deposit"]').click();
|
||||
cy.get(amountField).clear().type('10000');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { removeDecimal } from '@vegaprotocol/cypress';
|
||||
import { ethers } from 'ethers';
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
@@ -9,7 +11,7 @@ const formFieldError = 'input-error-text';
|
||||
const ASSET_EURO = 1;
|
||||
|
||||
describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
function openDepositForm() {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockSubscription();
|
||||
cy.mockTradingPage();
|
||||
@@ -20,10 +22,14 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.wait('@Assets');
|
||||
connectEthereumWallet('MetaMask');
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
}
|
||||
|
||||
before(() => {
|
||||
openDepositForm();
|
||||
});
|
||||
|
||||
it('handles empty fields', () => {
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||
cy.getByTestId(formFieldError).should('have.length', 2);
|
||||
});
|
||||
@@ -44,6 +50,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('invalid amount', () => {
|
||||
mockWeb3DepositCalls({
|
||||
allowance: '1000',
|
||||
depositLifetimeLimit: '1000',
|
||||
balance: '800',
|
||||
deposited: '0',
|
||||
dps: 5,
|
||||
});
|
||||
// Deposit amount smaller than minimum viable for selected asset
|
||||
// Select an amount so that we have a known decimal places value to work with
|
||||
selectAsset(ASSET_EURO);
|
||||
@@ -56,12 +69,16 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('insufficient funds', () => {
|
||||
// 1001-DEPO-005
|
||||
// Deposit amount is valid, but less than approved. This will always be the case because our
|
||||
// CI wallet wont have approved any assets
|
||||
mockWeb3DepositCalls({
|
||||
allowance: '1000',
|
||||
depositLifetimeLimit: '1000',
|
||||
balance: '800',
|
||||
deposited: '0',
|
||||
dps: 5,
|
||||
});
|
||||
cy.get(amountField)
|
||||
.clear()
|
||||
.type('100')
|
||||
.type('850')
|
||||
.next(`[data-testid="${formFieldError}"]`)
|
||||
.should('have.text', 'Insufficient amount in Ethereum wallet');
|
||||
});
|
||||
@@ -88,3 +105,90 @@ describe('deposit actions', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('deposit-submit').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
function mockWeb3DepositCalls({
|
||||
allowance,
|
||||
depositLifetimeLimit,
|
||||
balance,
|
||||
deposited,
|
||||
dps,
|
||||
}: {
|
||||
allowance: string;
|
||||
depositLifetimeLimit: string;
|
||||
balance: string;
|
||||
deposited: string;
|
||||
dps: number;
|
||||
}) {
|
||||
const assetContractAddress = '0x0158031158bb4df2ad02eaa31e8963e84ea978a4';
|
||||
const collateralBridgeAddress = '0x7fe27d970bc8afc3b11cc8d9737bfb66b1efd799';
|
||||
const toResult = (value: string, dps: number) => {
|
||||
const rawValue = removeDecimal(value, dps);
|
||||
return ethers.utils.hexZeroPad(
|
||||
ethers.utils.hexlify(parseInt(rawValue)),
|
||||
32
|
||||
);
|
||||
};
|
||||
cy.intercept('POST', 'http://localhost:8545', (req) => {
|
||||
// Mock chainId call
|
||||
if (req.body.method === 'eth_chainId') {
|
||||
req.alias = 'eth_chainId';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: '0xaa36a7', // 11155111 for sepolia chain id
|
||||
});
|
||||
}
|
||||
|
||||
// Mock deposited amount
|
||||
if (req.body.method === 'eth_getStorageAt') {
|
||||
req.alias = 'eth_getStorageAt';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(deposited, dps),
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.method === 'eth_call') {
|
||||
// Mock approved amount for asset on collateral bridge
|
||||
if (
|
||||
req.body.params[0].to === assetContractAddress &&
|
||||
req.body.params[0].data ===
|
||||
'0xdd62ed3e000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f0000000000000000000000007fe27d970bc8afc3b11cc8d9737bfb66b1efd799'
|
||||
) {
|
||||
req.alias = 'eth_call_allowance';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(allowance, dps),
|
||||
});
|
||||
}
|
||||
// Mock balance of asset in Ethereum wallet
|
||||
else if (
|
||||
req.body.params[0].to === assetContractAddress &&
|
||||
req.body.params[0].data ===
|
||||
'0x70a08231000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f'
|
||||
) {
|
||||
req.alias = 'eth_call_balanceOf';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(balance, dps),
|
||||
});
|
||||
}
|
||||
// Mock deposit lifetime limit
|
||||
else if (
|
||||
req.body.params[0].to === collateralBridgeAddress &&
|
||||
req.body.params[0].data ===
|
||||
'0x354a897a0000000000000000000000000158031158bb4df2ad02eaa31e8963e84ea978a4'
|
||||
) {
|
||||
req.alias = 'eth_call_get_deposit_maximum'; // deposit lifetime limit
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(depositLifetimeLimit, dps),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -304,4 +304,18 @@ describe('home', { tags: '@regression' }, () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('footer', () => {
|
||||
it('shows current block height', () => {
|
||||
cy.visit('/');
|
||||
cy.getByTestId('node-health')
|
||||
.children()
|
||||
.first()
|
||||
.should('contain.text', 'Operational')
|
||||
.next()
|
||||
.should('contain.text', new URL(Cypress.env('VEGA_URL')).origin)
|
||||
.next()
|
||||
.should('contain.text', '100'); // all mocked queries have x-block-height header set to 100
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
validateMarketDataRow(0, 'Mark Price', '0.05749');
|
||||
validateMarketDataRow(1, 'Best Bid Price', '6.81765 ');
|
||||
validateMarketDataRow(2, 'Best Offer Price', '6.81769 ');
|
||||
validateMarketDataRow(3, 'Quote Unit', 'BTC');
|
||||
});
|
||||
|
||||
it('market volume displayed', () => {
|
||||
@@ -168,9 +169,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
it('liquidity price range displayed', () => {
|
||||
cy.getByTestId(marketTitle).contains('Liquidity price range').click();
|
||||
|
||||
validateMarketDataRow(0, 'Liquidity Price Range', '2.00%');
|
||||
validateMarketDataRow(1, 'LP Volume Min', '0.05634 tBTC');
|
||||
validateMarketDataRow(2, 'LP Volume Max', '0.05864 tBTC');
|
||||
validateMarketDataRow(0, 'Liquidity Price Range', '2.00% of mid price');
|
||||
validateMarketDataRow(1, 'Lowest Price', '0.05634 BTC');
|
||||
validateMarketDataRow(2, 'Highest Price', '0.05864 BTC');
|
||||
});
|
||||
|
||||
it('oracle displayed', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { marketsQuery } from '@vegaprotocol/mock';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const dialogCloseBtn = 'dialog-close';
|
||||
const popoverTrigger = 'popover-trigger';
|
||||
|
||||
describe('markets table', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
@@ -173,8 +174,13 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
function openMarketDropDown() {
|
||||
cy.getByTestId(dialogCloseBtn).should('be.visible');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
cy.getByTestId('popover-trigger').click();
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
cy.getByTestId(dialogCloseBtn).then((button) => {
|
||||
if (button.is(':visible')) {
|
||||
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
}
|
||||
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
|
||||
cy.getByTestId(popoverTrigger).click({ force: true });
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,19 +17,50 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('can connect', () => {
|
||||
// Mock authentication
|
||||
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
|
||||
body: {
|
||||
token: 'test-token',
|
||||
},
|
||||
});
|
||||
// Mock getting keys from wallet
|
||||
cy.intercept('GET', 'https://wallet.testnet.vega.xyz/api/v1/keys', {
|
||||
body: {
|
||||
keys: [
|
||||
{
|
||||
algorithm: {
|
||||
name: 'algo',
|
||||
version: 1,
|
||||
},
|
||||
index: 0,
|
||||
meta: [],
|
||||
pub: 'HOSTED_PUBKEY',
|
||||
tainted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
mockConnectWallet();
|
||||
cy.contains('Connect Vega wallet');
|
||||
cy.contains('Hosted Fairground wallet');
|
||||
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
.click();
|
||||
cy.wait('@walletReq');
|
||||
cy.getByTestId(form).find('#wallet').click().type('user');
|
||||
cy.getByTestId(form).find('#passphrase').click().type('pass');
|
||||
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
|
||||
cy.getByTestId(manageVegaBtn).should('exist');
|
||||
});
|
||||
|
||||
it('doesnt connect with invalid credentials', () => {
|
||||
// Mock incorrect username/password
|
||||
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
|
||||
body: {
|
||||
error: 'No wallet',
|
||||
},
|
||||
statusCode: 403, // 403 forbidden invalid crednetials
|
||||
});
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
@@ -40,7 +71,7 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('form-error').should('have.text', 'Invalid credentials');
|
||||
});
|
||||
|
||||
it('doesnt connect with invalid fields', () => {
|
||||
it('doesnt connect with empty fields', () => {
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
|
||||
@@ -84,7 +84,7 @@ export const TradeMarketHeader = ({
|
||||
heading={t('Volume (24h)')}
|
||||
testId="market-volume"
|
||||
description={t(
|
||||
'The total amount of assets traded in the last 24 hours.'
|
||||
'The total number of contracts traded in the last 24 hours.'
|
||||
)}
|
||||
>
|
||||
<Last24hVolume
|
||||
|
||||
@@ -3,12 +3,14 @@ query AccountHistory(
|
||||
$assetId: ID!
|
||||
$accountTypes: [AccountType!]
|
||||
$dateRange: DateRange
|
||||
$marketIds: [ID!]
|
||||
) {
|
||||
balanceChanges(
|
||||
filter: {
|
||||
partyIds: [$partyId]
|
||||
accountTypes: $accountTypes
|
||||
assetId: $assetId
|
||||
marketIds: $marketIds
|
||||
}
|
||||
dateRange: $dateRange
|
||||
) {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type AccountHistoryQueryVariables = Types.Exact<{
|
||||
assetId: Types.Scalars['ID'];
|
||||
accountTypes?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
|
||||
dateRange?: Types.InputMaybe<Types.DateRange>;
|
||||
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -23,9 +24,9 @@ export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: {
|
||||
|
||||
|
||||
export const AccountHistoryDocument = gql`
|
||||
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange) {
|
||||
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) {
|
||||
balanceChanges(
|
||||
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId}
|
||||
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds}
|
||||
dateRange: $dateRange
|
||||
) {
|
||||
edges {
|
||||
@@ -58,6 +59,7 @@ export const AccountHistoryDocument = gql`
|
||||
* assetId: // value for 'assetId'
|
||||
* accountTypes: // value for 'accountTypes'
|
||||
* dateRange: // value for 'dateRange'
|
||||
* marketIds: // value for 'marketIds'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -6,8 +6,9 @@ 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 { useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
|
||||
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -25,8 +26,10 @@ import {
|
||||
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { PriceChart } from 'pennant';
|
||||
import 'pennant/dist/style.css';
|
||||
import { accountsOnlyDataProvider } from '@vegaprotocol/accounts';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { accountsDataProvider } from '@vegaprotocol/accounts';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
|
||||
const DateRange = {
|
||||
RANGE_1D: '1D',
|
||||
@@ -97,7 +100,7 @@ const AccountHistoryManager = ({
|
||||
);
|
||||
|
||||
const { data: accounts } = useDataProvider({
|
||||
dataProvider: accountsOnlyDataProvider,
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: variablesForOneTimeQuery,
|
||||
skip: !pubKey,
|
||||
});
|
||||
@@ -118,6 +121,39 @@ const AccountHistoryManager = ({
|
||||
const [range, setRange] = useState<typeof DateRange[keyof typeof DateRange]>(
|
||||
DateRange.RANGE_1M
|
||||
);
|
||||
const [market, setMarket] = useState<Market | null>(null);
|
||||
const marketFilterCb = useCallback(
|
||||
(item: Market) =>
|
||||
!asset?.id ||
|
||||
item.tradableInstrument.instrument.product.settlementAsset.id ===
|
||||
asset?.id,
|
||||
[asset?.id]
|
||||
);
|
||||
const markets = useMemo<Market[] | null>(() => {
|
||||
const arr =
|
||||
accounts
|
||||
?.filter((item: Account) => Boolean(item && item.market))
|
||||
.map<Market>((item) => item.market as Market) ?? null;
|
||||
return arr
|
||||
? uniqBy(arr.filter(marketFilterCb), 'id').sort((a, b) =>
|
||||
a.tradableInstrument.instrument.code.localeCompare(
|
||||
b.tradableInstrument.instrument.code
|
||||
)
|
||||
)
|
||||
: null;
|
||||
}, [accounts, marketFilterCb]);
|
||||
const resolveMarket = useCallback(
|
||||
(m: Market) => {
|
||||
setMarket(m);
|
||||
const newAssetId =
|
||||
m.tradableInstrument.instrument.product.settlementAsset.id;
|
||||
const newAsset = assets.find((item) => item.id === newAssetId);
|
||||
if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) {
|
||||
setAsset(newAsset);
|
||||
}
|
||||
},
|
||||
[asset, assets]
|
||||
);
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
@@ -126,62 +162,113 @@ const AccountHistoryManager = ({
|
||||
accountTypes: accountType ? [accountType] : undefined,
|
||||
dateRange:
|
||||
range === 'All' ? undefined : { start: calculateStartDate(range) },
|
||||
marketIds: market?.id ? [market.id] : undefined,
|
||||
}),
|
||||
[pubKey, asset, accountType, range]
|
||||
[pubKey, asset, accountType, range, market?.id]
|
||||
);
|
||||
|
||||
const { data } = useAccountHistoryQuery({
|
||||
variables,
|
||||
skip: !asset || !pubKey,
|
||||
});
|
||||
|
||||
const accountTypeMenu = useMemo(() => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{accountType
|
||||
? `${
|
||||
AccountTypeMapping[
|
||||
accountType as keyof typeof Schema.AccountType
|
||||
]
|
||||
} Account`
|
||||
: t('Select account type')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{[
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
Schema.AccountType.ACCOUNT_TYPE_BOND,
|
||||
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
].map((type) => (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
onClick={() => setAccountType(type as Schema.AccountType)}
|
||||
>
|
||||
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}, [accountType]);
|
||||
const assetsMenu = useMemo(() => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{asset ? asset.symbol : t('Select asset')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{assets.map((a) => (
|
||||
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
|
||||
{a.symbol}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}, [assets, asset]);
|
||||
const marketsMenu = useMemo(() => {
|
||||
return accountType === Schema.AccountType.ACCOUNT_TYPE_MARGIN &&
|
||||
markets?.length ? (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{market
|
||||
? market.tradableInstrument.instrument.code
|
||||
: t('Select market')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{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}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null;
|
||||
}, [markets, market, accountType, resolveMarket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN ||
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.id !==
|
||||
asset?.id
|
||||
) {
|
||||
setMarket(null);
|
||||
}
|
||||
}, [accountType, asset?.id, market]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col gap-8">
|
||||
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
|
||||
<div className="flex items-center gap-4 shrink-0">
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{accountType
|
||||
? `${
|
||||
AccountTypeMapping[
|
||||
accountType as keyof typeof Schema.AccountType
|
||||
]
|
||||
} Account`
|
||||
: t('Select account type')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{[
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
Schema.AccountType.ACCOUNT_TYPE_BOND,
|
||||
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
].map((type) => (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
onClick={() => setAccountType(type as Schema.AccountType)}
|
||||
>
|
||||
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{asset ? asset.symbol : t('Select asset')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{assets.map((a) => (
|
||||
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
|
||||
{a.symbol}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<>
|
||||
{accountTypeMenu}
|
||||
{assetsMenu}
|
||||
{marketsMenu}
|
||||
</>
|
||||
</div>
|
||||
<div className="pt-1 justify-items-end">
|
||||
<Toggle
|
||||
|
||||
@@ -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...</>,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { NodeUrl, NodeHealth } from './footer';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
|
||||
|
||||
describe('NodeUrl', () => {
|
||||
it('can open node switcher by clicking the node url', () => {
|
||||
const mockOpenNodeSwitcher = jest.fn();
|
||||
const node = 'https://api.n99.somenetwork.vega.xyz';
|
||||
|
||||
render(<NodeUrl url={node} openNodeSwitcher={mockOpenNodeSwitcher} />);
|
||||
|
||||
fireEvent.click(screen.getByText(/n99/));
|
||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||
describe('NodeHealth', () => {
|
||||
it('controls the node switcher dialog', async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
render(
|
||||
<NodeHealth
|
||||
onClick={mockOnClick}
|
||||
url={'https://api.n99.somenetwork.vega.xyz'}
|
||||
blockHeight={100}
|
||||
blockDiff={0}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(mockOnClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NodeHealth', () => {
|
||||
const mockOpenNodeSwitcher = jest.fn();
|
||||
describe('NodeUrl', () => {
|
||||
it('renders correct part of node url', () => {
|
||||
const node = 'https://api.n99.somenetwork.vega.xyz';
|
||||
const expectedText = node.split('.').slice(1).join('.');
|
||||
|
||||
render(<NodeUrl url={node} />);
|
||||
|
||||
expect(screen.getByText(expectedText)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('HealthIndicator', () => {
|
||||
const cases = [
|
||||
{ diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
|
||||
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
||||
@@ -23,16 +38,9 @@ describe('NodeHealth', () => {
|
||||
it.each(cases)(
|
||||
'renders correct text and indicator color for $diff block difference',
|
||||
(elem) => {
|
||||
render(
|
||||
<NodeHealth
|
||||
blockDiff={elem.diff}
|
||||
openNodeSwitcher={mockOpenNodeSwitcher}
|
||||
/>
|
||||
);
|
||||
render(<HealthIndicator blockDiff={elem.diff} />);
|
||||
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
||||
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText(elem.text));
|
||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
||||
import { t, useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||
import { ButtonLink, Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
|
||||
export const Footer = () => {
|
||||
@@ -8,45 +10,64 @@ export const Footer = () => {
|
||||
const setNodeSwitcher = useGlobalStore(
|
||||
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||
);
|
||||
const { blockDiff } = useNodeHealth();
|
||||
const { blockDiff, datanodeBlockHeight } = useNodeHealth();
|
||||
|
||||
return (
|
||||
<footer className="px-4 py-1 text-xs border-t border-default">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
{VEGA_URL && (
|
||||
<>
|
||||
<NodeHealth
|
||||
blockDiff={blockDiff}
|
||||
openNodeSwitcher={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
{' | '}
|
||||
<NodeUrl
|
||||
url={VEGA_URL}
|
||||
openNodeSwitcher={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300">
|
||||
{/* Pull left to align with top nav, due to button padding */}
|
||||
<div className="-ml-2">
|
||||
{VEGA_URL && (
|
||||
<NodeHealth
|
||||
url={VEGA_URL}
|
||||
blockHeight={datanodeBlockHeight}
|
||||
blockDiff={blockDiff}
|
||||
onClick={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
interface NodeHealthProps {
|
||||
url: string;
|
||||
blockHeight: number | undefined;
|
||||
blockDiff: number | null;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const NodeHealth = ({
|
||||
url,
|
||||
blockHeight,
|
||||
blockDiff,
|
||||
onClick,
|
||||
}: NodeHealthProps) => {
|
||||
return (
|
||||
<FooterButton onClick={onClick} data-testid="node-health">
|
||||
<FooterButtonPart>
|
||||
<HealthIndicator blockDiff={blockDiff} />
|
||||
</FooterButtonPart>
|
||||
<FooterButtonPart>
|
||||
<NodeUrl url={url} />
|
||||
</FooterButtonPart>
|
||||
<FooterButtonPart>
|
||||
<span title={t('Block height')}>{blockHeight}</span>
|
||||
</FooterButtonPart>
|
||||
</FooterButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface NodeUrlProps {
|
||||
url: string;
|
||||
openNodeSwitcher: () => void;
|
||||
}
|
||||
|
||||
export const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
|
||||
export const NodeUrl = ({ url }: NodeUrlProps) => {
|
||||
// get base url from api url, api sub domain
|
||||
const urlObj = new URL(url);
|
||||
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
|
||||
return <ButtonLink onClick={openNodeSwitcher}>{nodeUrl}</ButtonLink>;
|
||||
return <span title={t('Connected node')}>{nodeUrl}</span>;
|
||||
};
|
||||
|
||||
interface NodeHealthProps {
|
||||
openNodeSwitcher: () => void;
|
||||
interface HealthIndicatorProps {
|
||||
blockDiff: number | null;
|
||||
}
|
||||
|
||||
@@ -54,10 +75,7 @@ interface NodeHealthProps {
|
||||
// deemed acceptable for "Good" status
|
||||
const BLOCK_THRESHOLD = 3;
|
||||
|
||||
export const NodeHealth = ({
|
||||
blockDiff,
|
||||
openNodeSwitcher,
|
||||
}: NodeHealthProps) => {
|
||||
export const HealthIndicator = ({ blockDiff }: HealthIndicatorProps) => {
|
||||
const online = useNavigatorOnline();
|
||||
|
||||
let intent = Intent.Success;
|
||||
@@ -76,9 +94,36 @@ export const NodeHealth = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span title={t('Node health')}>
|
||||
<Indicator variant={intent} />
|
||||
<ButtonLink onClick={openNodeSwitcher}>{text}</ButtonLink>
|
||||
</>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
type FooterButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
const FooterButton = (props: FooterButtonProps) => {
|
||||
const buttonClasses = classNames(
|
||||
'px-2 py-0.5 rounded-md',
|
||||
'enabled:hover:bg-vega-light-150',
|
||||
'dark:enabled:hover:bg-vega-dark-150'
|
||||
);
|
||||
return <button {...props} className={buttonClasses} />;
|
||||
};
|
||||
|
||||
const FooterButtonPart = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
'relative inline-block mr-2 last:mr-0 pr-2 last:pr-0',
|
||||
'last:after:hidden',
|
||||
'after:content after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2',
|
||||
'after:h-3 after:w-1 after:border-r',
|
||||
'after:border-vega-light-300 dark:after:border-vega-dark-300'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ export const MarketLiquiditySupplied = ({
|
||||
{showMessage && (
|
||||
<p className="mt-4">
|
||||
{t(
|
||||
'The market is in an auction because there are no priced limit orders, which are required to deploy liquidity commitment pegged orders. This means the order book is empty on one or both sides.'
|
||||
'The market has sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user