Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17bc4541e9 | ||
|
|
2bd8044a6c | ||
|
|
b3a82fd6f0 | ||
|
|
f22fb56afd | ||
|
|
f2627e6ebf | ||
|
|
24e6a13d93 | ||
|
|
b56050ad1c | ||
|
|
cff83ed062 | ||
|
|
8ecce874b0 | ||
|
|
00e319b3c6 | ||
|
|
05559f3ea0 | ||
|
|
4b3b5c322a | ||
|
|
b1280c8285 | ||
|
|
1e0e1c7859 | ||
|
|
ec2bb81ec8 | ||
|
|
fe95c6fcbc | ||
|
|
853ec8f69c | ||
|
|
b68c090ee5 | ||
|
|
cb9b811730 | ||
|
|
98b5260d93 | ||
|
|
613262f7a5 | ||
|
|
9ae6f5201c | ||
|
|
cfb75fcf8f |
@@ -4,6 +4,7 @@ import { useExplorerAssetQuery } from './__generated__/Asset';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
|
||||
export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
id: string;
|
||||
@@ -27,7 +28,7 @@ const AssetLink = ({ id, ...props }: AssetLinkProps) => {
|
||||
|
||||
return (
|
||||
<Link className="underline" {...props} to={`/${Routes.ASSETS}#${id}`}>
|
||||
{label}
|
||||
<Hash text={label} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Routes } from '../../../routes/route-names';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
|
||||
export type BlockLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
height: string;
|
||||
@@ -11,7 +12,7 @@ export type BlockLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
const BlockLink = ({ height, ...props }: BlockLinkProps) => {
|
||||
return (
|
||||
<Link className="underline" {...props} to={`/${Routes.BLOCKS}/${height}`}>
|
||||
{height}
|
||||
<Hash text={height} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import Hash from '../hash';
|
||||
|
||||
export enum EthExplorerLinkTypes {
|
||||
block = 'block',
|
||||
@@ -27,7 +28,7 @@ export const EthExplorerLink = ({
|
||||
{...props}
|
||||
href={link}
|
||||
>
|
||||
{id}
|
||||
<Hash text={id} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export type HashProps = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A simple component that ensures long text things like hashes
|
||||
* are broken when they need to wrap. This will remove the need
|
||||
* for a lot of the overflow scrolling that currently exists.
|
||||
*/
|
||||
const Hash = ({ text }: HashProps) => {
|
||||
return (
|
||||
<code className="break-all font-mono" style={{ wordWrap: 'break-word' }}>
|
||||
{text}
|
||||
</code>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hash;
|
||||
@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import Hash from '../hash';
|
||||
|
||||
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
id: string;
|
||||
@@ -35,7 +36,8 @@ const MarketLink = ({
|
||||
<span role="img" aria-label="Unknown market" className="img">
|
||||
⚠️ {t('Invalid market')}
|
||||
</span>
|
||||
{id}
|
||||
|
||||
<Hash text={id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -55,7 +57,7 @@ const MarketLink = ({
|
||||
} else {
|
||||
return (
|
||||
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
|
||||
{id}
|
||||
<Hash text={id} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useExplorerNodeQuery } from './__generated__/Node';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
|
||||
export type NodeLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
id: string;
|
||||
@@ -22,7 +23,7 @@ const NodeLink = ({ id, ...props }: NodeLinkProps) => {
|
||||
|
||||
return (
|
||||
<Link className="underline" {...props} to={`/${Routes.VALIDATORS}#${id}`}>
|
||||
<code>{label}</code>
|
||||
<Hash text={label} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Routes } from '../../../routes/route-names';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
|
||||
export type OracleLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
id: string;
|
||||
@@ -14,7 +15,7 @@ const OracleLink = ({ id, ...props }: OracleLinkProps) => {
|
||||
{...props}
|
||||
to={`/${Routes.ORACLES}/${id}`}
|
||||
>
|
||||
{id}
|
||||
<Hash text={id} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Routes } from '../../../routes/route-names';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
|
||||
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
id: string;
|
||||
@@ -14,7 +15,7 @@ const PartyLink = ({ id, ...props }: PartyLinkProps) => {
|
||||
{...props}
|
||||
to={`/${Routes.PARTIES}/${id}`}
|
||||
>
|
||||
{id}
|
||||
<Hash text={id} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('Proposal link component', () => {
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the ID with an emoji on error', async () => {
|
||||
it('Renders the ID on error', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerProposalDocument,
|
||||
@@ -37,9 +37,6 @@ describe('Proposal link component', () => {
|
||||
const res = render(renderComponent('456', [mock]));
|
||||
// The ID
|
||||
expect(res.getByText('456')).toBeInTheDocument();
|
||||
|
||||
// The emoji
|
||||
expect(await res.findByRole('img')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the proposal title when the query returns a result', async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useExplorerProposalQuery } from './__generated__/Proposal';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { ENV } from '../../../config/env';
|
||||
import Hash from '../hash';
|
||||
export type ProposalLinkProps = {
|
||||
id: string;
|
||||
};
|
||||
@@ -17,7 +18,11 @@ const ProposalLink = ({ id }: ProposalLinkProps) => {
|
||||
const base = ENV.dataSources.governanceUrl;
|
||||
const label = data?.proposal?.rationale.title || id;
|
||||
|
||||
return <ExternalLink href={`${base}/proposals/${id}`}>{label}</ExternalLink>;
|
||||
return (
|
||||
<ExternalLink href={`${base}/proposals/${id}`}>
|
||||
<Hash text={label} />
|
||||
</ExternalLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProposalLink;
|
||||
|
||||
+10
-4
@@ -63,15 +63,21 @@ describe('Chain Event: Builtin asset deposit', () => {
|
||||
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
|
||||
const partyLink = screen.getByText(`${fullMock.partyId}`);
|
||||
expect(partyLink).toBeInTheDocument();
|
||||
expect(partyLink.tagName).toEqual('A');
|
||||
expect(partyLink.getAttribute('href')).toEqual(
|
||||
if (!partyLink.parentElement) {
|
||||
throw new Error('Party link does not exist');
|
||||
}
|
||||
expect(partyLink.parentElement.tagName).toEqual('A');
|
||||
expect(partyLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/parties/${fullMock.partyId}`
|
||||
);
|
||||
|
||||
const assetLink = screen.getByText(`${fullMock.vegaAssetId}`);
|
||||
expect(assetLink).toBeInTheDocument();
|
||||
expect(assetLink.tagName).toEqual('A');
|
||||
expect(assetLink.getAttribute('href')).toEqual(
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
});
|
||||
|
||||
+10
-4
@@ -69,15 +69,21 @@ describe('Chain Event: Builtin asset withdrawal', () => {
|
||||
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
|
||||
const partyLink = screen.getByText(`${fullMock.partyId}`);
|
||||
expect(partyLink).toBeInTheDocument();
|
||||
expect(partyLink.tagName).toEqual('A');
|
||||
expect(partyLink.getAttribute('href')).toEqual(
|
||||
if (!partyLink.parentElement) {
|
||||
throw new Error('Party link does not exist');
|
||||
}
|
||||
expect(partyLink.parentElement.tagName).toEqual('A');
|
||||
expect(partyLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/parties/${fullMock.partyId}`
|
||||
);
|
||||
|
||||
const assetLink = screen.getByText(`${fullMock.vegaAssetId}`);
|
||||
expect(assetLink).toBeInTheDocument();
|
||||
expect(assetLink.tagName).toEqual('A');
|
||||
expect(assetLink.getAttribute('href')).toEqual(
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
});
|
||||
|
||||
+5
-2
@@ -60,8 +60,11 @@ describe('Chain Event: ERC20 Asset Delist', () => {
|
||||
|
||||
const assetLink = screen.getByText(`${fullMock.vegaAssetId}`);
|
||||
expect(assetLink).toBeInTheDocument();
|
||||
expect(assetLink.tagName).toEqual('A');
|
||||
expect(assetLink.getAttribute('href')).toEqual(
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
});
|
||||
|
||||
+9
-3
@@ -76,14 +76,20 @@ describe('Chain Event: ERC20 Asset limits updated', () => {
|
||||
expect(screen.getByText(t('Vega asset'))).toBeInTheDocument();
|
||||
const assetLink = screen.getByText(`${fullMock.vegaAssetId}`);
|
||||
expect(assetLink).toBeInTheDocument();
|
||||
expect(assetLink.tagName).toEqual('A');
|
||||
expect(assetLink.getAttribute('href')).toEqual(
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('ERC20 asset'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.sourceEthereumAddress}`);
|
||||
expect(ethLink.getAttribute('href')).toContain(
|
||||
if (!ethLink.parentElement) {
|
||||
throw new Error('ETH link does not exist');
|
||||
}
|
||||
expect(ethLink.parentElement.getAttribute('href')).toContain(
|
||||
`/address/${fullMock.sourceEthereumAddress}`
|
||||
);
|
||||
});
|
||||
|
||||
+9
-3
@@ -62,14 +62,20 @@ describe('Chain Event: ERC20 Asset List', () => {
|
||||
expect(screen.getByText(t('Added Vega asset'))).toBeInTheDocument();
|
||||
const assetLink = screen.getByText(`${fullMock.vegaAssetId}`);
|
||||
expect(assetLink).toBeInTheDocument();
|
||||
expect(assetLink.tagName).toEqual('A');
|
||||
expect(assetLink.getAttribute('href')).toEqual(
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('Source'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.assetSource}`);
|
||||
expect(ethLink.getAttribute('href')).toContain(
|
||||
if (!ethLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(ethLink.parentElement.getAttribute('href')).toContain(
|
||||
`/address/${fullMock.assetSource}`
|
||||
);
|
||||
});
|
||||
|
||||
+14
-5
@@ -62,21 +62,30 @@ describe('Chain Event: ERC20 asset deposit', () => {
|
||||
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
|
||||
const partyLink = screen.getByText(`${fullMock.targetPartyId}`);
|
||||
expect(partyLink).toBeInTheDocument();
|
||||
expect(partyLink.tagName).toEqual('A');
|
||||
expect(partyLink.getAttribute('href')).toEqual(
|
||||
if (!partyLink.parentElement) {
|
||||
throw new Error('Party link does not exist');
|
||||
}
|
||||
expect(partyLink.parentElement.tagName).toEqual('A');
|
||||
expect(partyLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/parties/${fullMock.targetPartyId}`
|
||||
);
|
||||
|
||||
const assetLink = screen.getByText(`${fullMock.vegaAssetId}`);
|
||||
expect(assetLink).toBeInTheDocument();
|
||||
expect(assetLink.tagName).toEqual('A');
|
||||
expect(assetLink.getAttribute('href')).toEqual(
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('Source'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.sourceEthereumAddress}`);
|
||||
expect(ethLink.getAttribute('href')).toContain(
|
||||
if (!ethLink.parentElement) {
|
||||
throw new Error('ETH link does not exist');
|
||||
}
|
||||
expect(ethLink.parentElement.getAttribute('href')).toContain(
|
||||
`/address/${fullMock.sourceEthereumAddress}`
|
||||
);
|
||||
});
|
||||
|
||||
+9
-3
@@ -57,14 +57,20 @@ describe('Chain Event: ERC20 asset deposit', () => {
|
||||
expect(screen.getByText(t('Asset'))).toBeInTheDocument();
|
||||
const assetLink = screen.getByText(`${fullMock.vegaAssetId}`);
|
||||
expect(assetLink).toBeInTheDocument();
|
||||
expect(assetLink.tagName).toEqual('A');
|
||||
expect(assetLink.getAttribute('href')).toEqual(
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.targetEthereumAddress}`);
|
||||
expect(ethLink.getAttribute('href')).toContain(
|
||||
if (!ethLink.parentElement) {
|
||||
throw new Error('ETH link does not exist');
|
||||
}
|
||||
expect(ethLink.parentElement.getAttribute('href')).toContain(
|
||||
`/address/${fullMock.targetEthereumAddress}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,14 +64,20 @@ describe('Chain Event: Stake deposit', () => {
|
||||
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
|
||||
const partyLink = screen.getByText(`${fullMock.vegaPublicKey}`);
|
||||
expect(partyLink).toBeInTheDocument();
|
||||
expect(partyLink.tagName).toEqual('A');
|
||||
expect(partyLink.getAttribute('href')).toEqual(
|
||||
if (!partyLink.parentElement) {
|
||||
throw new Error('Party link does not exist');
|
||||
}
|
||||
expect(partyLink.parentElement.tagName).toEqual('A');
|
||||
expect(partyLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/parties/${fullMock.vegaPublicKey}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('Source'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.ethereumAddress}`);
|
||||
expect(ethLink.getAttribute('href')).toContain(
|
||||
if (!ethLink.parentElement) {
|
||||
throw new Error('ETH link does not exist');
|
||||
}
|
||||
expect(ethLink.parentElement.getAttribute('href')).toContain(
|
||||
`/address/${fullMock.ethereumAddress}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,14 +64,20 @@ describe('Chain Event: Stake remove', () => {
|
||||
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
|
||||
const partyLink = screen.getByText(`${fullMock.vegaPublicKey}`);
|
||||
expect(partyLink).toBeInTheDocument();
|
||||
expect(partyLink.tagName).toEqual('A');
|
||||
expect(partyLink.getAttribute('href')).toEqual(
|
||||
if (!partyLink.parentElement) {
|
||||
throw new Error('Party link does not exist');
|
||||
}
|
||||
expect(partyLink.parentElement.tagName).toEqual('A');
|
||||
expect(partyLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/parties/${fullMock.vegaPublicKey}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('Source'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.ethereumAddress}`);
|
||||
expect(ethLink.getAttribute('href')).toContain(
|
||||
if (!ethLink.parentElement) {
|
||||
throw new Error('ETH link does not exist');
|
||||
}
|
||||
expect(ethLink.parentElement.getAttribute('href')).toContain(
|
||||
`/address/${fullMock.ethereumAddress}`
|
||||
);
|
||||
});
|
||||
|
||||
+4
-1
@@ -66,7 +66,10 @@ describe('Chain Event: Stake total supply change', () => {
|
||||
|
||||
expect(screen.getByText(t('Source'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.tokenAddress}`);
|
||||
expect(ethLink.getAttribute('href')).toContain(
|
||||
if (!ethLink.parentElement) {
|
||||
throw new Error('ETH link does not exist');
|
||||
}
|
||||
expect(ethLink.parentElement.getAttribute('href')).toContain(
|
||||
`/address/${fullMock.tokenAddress}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ import type { BlockExplorerTransactionResult } from '../../../../routes/types/bl
|
||||
import type { TendermintBlocksResponse } from '../../../../routes/blocks/tendermint-blocks-response';
|
||||
import { Time } from '../../../time';
|
||||
import { ChainResponseCode } from '../chain-response-code/chain-reponse.code';
|
||||
import { TxDataView } from '../../tx-data-view';
|
||||
import Hash from '../../../links/hash';
|
||||
|
||||
interface TxDetailsSharedProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -46,7 +48,7 @@ export const TxDetailsShared = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Hash')}</TableCell>
|
||||
<TableCell>
|
||||
<code>{txData.hash}</code>
|
||||
<Hash text={txData.hash} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
@@ -82,6 +84,12 @@ export const TxDetailsShared = ({
|
||||
<ChainResponseCode code={txData.code} error={txData.error} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Transaction')}</TableCell>
|
||||
<TableCell>
|
||||
<TxDataView blockData={blockData} txData={txData} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,10 +8,8 @@ import { TxDetailsHeartbeat } from './tx-hearbeat';
|
||||
import { TxDetailsGeneric } from './tx-generic';
|
||||
import { TxDetailsBatch } from './tx-batch';
|
||||
import { TxDetailsChainEvent } from './tx-chain-event';
|
||||
import { TxContent } from '../../../routes/txs/id/tx-content';
|
||||
import { TxDetailsNodeVote } from './tx-node-vote';
|
||||
import { TxDetailsOrderCancel } from './tx-order-cancel';
|
||||
import get from 'lodash/get';
|
||||
import { TxDetailsOrderAmend } from './tx-order-amend';
|
||||
import { TxDetailsWithdrawSubmission } from './tx-withdraw-submission';
|
||||
import { TxDetailsDelegate } from './tx-delegation';
|
||||
@@ -46,23 +44,9 @@ export const TxDetailsWrapper = ({
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const raw = get(blockData, `result.block.data.txs[${txData.index}]`);
|
||||
|
||||
return (
|
||||
<div key={`txd-${txData.hash}`}>
|
||||
<section>{child({ txData, pubKey, blockData })}</section>
|
||||
|
||||
<details title={t('Decoded transaction')} className="mt-3">
|
||||
<summary className="cursor-pointer">{t('Decoded transaction')}</summary>
|
||||
<TxContent data={txData} />
|
||||
</details>
|
||||
|
||||
{raw ? (
|
||||
<details title={t('Raw transaction')} className="mt-3">
|
||||
<summary className="cursor-pointer">{t('Raw transaction')}</summary>
|
||||
<code className="break-all font-mono text-xs">{raw}</code>
|
||||
</details>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
||||
import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
|
||||
import Hash from '../../links/hash';
|
||||
|
||||
interface TxDetailsOrderProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -47,7 +48,7 @@ export const TxDetailsOrder = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Order')}</TableCell>
|
||||
<TableCell>
|
||||
<code>{deterministicId}</code>
|
||||
<Hash text={deterministicId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useState } from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import get from 'lodash/get';
|
||||
import { Select } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../routes/blocks/tendermint-blocks-response';
|
||||
|
||||
export function getClassName(showTxData: ShowTxDataType) {
|
||||
const baseClasses =
|
||||
'font-mono bg-neutral-300 text-[11px] leading-3 text-gray-900 w-full p-2 max-w-[615px]';
|
||||
if (showTxData === 'JSON') {
|
||||
return `${baseClasses} whitespace-pre overflow-x-scroll`;
|
||||
} else {
|
||||
return baseClasses;
|
||||
}
|
||||
}
|
||||
|
||||
export function getContents(
|
||||
showTxData: ShowTxDataType,
|
||||
txData: BlockExplorerTransactionResult | null,
|
||||
blockData: TendermintBlocksResponse | null | undefined
|
||||
) {
|
||||
if (showTxData === 'JSON') {
|
||||
if (txData) {
|
||||
return JSON.stringify(txData.command, undefined, 1);
|
||||
}
|
||||
} else {
|
||||
if (txData && blockData) {
|
||||
return get(blockData, `result.block.data.txs[${txData.index}]`);
|
||||
}
|
||||
}
|
||||
|
||||
return '-';
|
||||
}
|
||||
|
||||
type ShowTxDataType = 'JSON' | 'base64';
|
||||
|
||||
interface TxDataViewProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
export const TxDataView = ({ txData, blockData }: TxDataViewProps) => {
|
||||
const [showTxData, setShowTxData] = useState<ShowTxDataType>('JSON');
|
||||
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<details title={t('Show raw transaction')}>
|
||||
<summary className="cursor-pointer">{t('Show raw transaction')}</summary>
|
||||
<div className="py-4">
|
||||
<textarea
|
||||
readOnly={true}
|
||||
className={getClassName(showTxData)}
|
||||
rows={12}
|
||||
cols={120}
|
||||
value={getContents(showTxData, txData, blockData)}
|
||||
/>
|
||||
<div className="w-40">
|
||||
<Select
|
||||
placeholder="View as..."
|
||||
onChange={(v) => setShowTxData(v.target.value as ShowTxDataType)}
|
||||
value={'JSON'}
|
||||
>
|
||||
<option value={'JSON'}>JSON</option>
|
||||
<option value={'base64'}>Base64</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
};
|
||||
@@ -115,7 +115,7 @@ export const TxsInfiniteList = ({
|
||||
className="List"
|
||||
height={995}
|
||||
itemCount={itemCount}
|
||||
itemSize={isStacked ? 134 : 72}
|
||||
itemSize={isStacked ? 134 : 50}
|
||||
onItemsRendered={onItemsRendered}
|
||||
ref={ref}
|
||||
width={'100%'}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "86666.297",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "74139.8676803936448564202",
|
||||
"locked_amount": "73487.251446645509463922",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "86666.297",
|
||||
@@ -71,7 +71,7 @@
|
||||
"tranche_end": "2023-06-01T00:00:00.000Z",
|
||||
"total_added": "2500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1720.3878586691085",
|
||||
"locked_amount": "1682.6333244301995",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "2500",
|
||||
@@ -450,7 +450,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "129999.45",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "74072.224379771147833695",
|
||||
"locked_amount": "73420.20357622098315111",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129999.45",
|
||||
@@ -516,7 +516,7 @@
|
||||
"tranche_end": "2023-09-03T00:00:00.000Z",
|
||||
"total_added": "62600",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "37601.88816590563164",
|
||||
"locked_amount": "37130.49642947742434",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10000",
|
||||
@@ -709,7 +709,7 @@
|
||||
"tranche_end": "2023-09-17T00:00:00.000Z",
|
||||
"total_added": "5000",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "3195.1265220700155",
|
||||
"locked_amount": "3157.4754249112125",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "5000",
|
||||
@@ -920,7 +920,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "97499.58",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "15880.1535996519475746684",
|
||||
"locked_amount": "15240.5819298544871083824",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "97499.58",
|
||||
@@ -953,7 +953,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "135173.4239508",
|
||||
"total_removed": "98230.390980249184455396",
|
||||
"locked_amount": "21705.428376578164333339690044",
|
||||
"locked_amount": "20831.244321407448710024671284",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "135173.4239508",
|
||||
@@ -999,7 +999,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "32499.86",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "6680.506383789325752837",
|
||||
"locked_amount": "6411.4496271174975967098",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "32499.86",
|
||||
@@ -1032,7 +1032,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "10833.29",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "2174.4400018239376500555",
|
||||
"locked_amount": "2086.8646383922243299952",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10833.29",
|
||||
@@ -1065,7 +1065,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "22749.93",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "8128.542379221205676028",
|
||||
"locked_amount": "7801.1661111092940288519",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "6500",
|
||||
@@ -1203,8 +1203,8 @@
|
||||
"tranche_start": "2022-11-01T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-01T00:00:00.000Z",
|
||||
"total_added": "22500",
|
||||
"total_removed": "3539.640404325",
|
||||
"locked_amount": "11715.4437154696125",
|
||||
"total_removed": "3707.308452225",
|
||||
"locked_amount": "11373.775610036832",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -1218,6 +1218,11 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "167.6680479",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x3bd3579f34ddc1eee597ba9b3fbbf18b6c268d085cd299e964b7239b2434fcf3"
|
||||
},
|
||||
{
|
||||
"amount": "305.3119245",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -1281,6 +1286,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "167.6680479",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 33,
|
||||
"tx": "0x3bd3579f34ddc1eee597ba9b3fbbf18b6c268d085cd299e964b7239b2434fcf3"
|
||||
},
|
||||
{
|
||||
"amount": "305.3119245",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -1343,8 +1354,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "7500",
|
||||
"withdrawn_tokens": "3539.640404325",
|
||||
"remaining_tokens": "3960.359595675"
|
||||
"withdrawn_tokens": "3707.308452225",
|
||||
"remaining_tokens": "3792.691547775"
|
||||
},
|
||||
{
|
||||
"address": "0x2539b51EbDE65a75672aBcfE9439a706a99D18D1",
|
||||
@@ -1369,7 +1380,7 @@
|
||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||
"total_added": "1939928.38",
|
||||
"total_removed": "928642.9598472029154",
|
||||
"locked_amount": "670971.990255795291622156",
|
||||
"locked_amount": "656363.903872495430316328",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1852091.69",
|
||||
@@ -1721,7 +1732,7 @@
|
||||
"tranche_end": "2023-02-01T00:00:00.000Z",
|
||||
"total_added": "42500",
|
||||
"total_removed": "24434.0787288",
|
||||
"locked_amount": "1211.304536533817085",
|
||||
"locked_amount": "576.45383579911392",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "12500",
|
||||
@@ -6711,10 +6722,65 @@
|
||||
"tranche_id": 11,
|
||||
"tranche_start": "2021-09-03T00:00:00.000Z",
|
||||
"tranche_end": "2022-09-03T00:00:00.000Z",
|
||||
"total_added": "53657.000000000000000003",
|
||||
"total_removed": "42606.21518131551",
|
||||
"total_added": "53995.000000000000000003",
|
||||
"total_removed": "42939.21518131551",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "45",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0xa6c36806aba504d0a30c574f6ab4dc2d76aad26d8759c778feb6bf27729cddcf"
|
||||
},
|
||||
{
|
||||
"amount": "46",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0x0f0f261dd8e05919d3c6c1d4358434e2c9c1b9087347702d2b8a4b1aeecd7e4b"
|
||||
},
|
||||
{
|
||||
"amount": "50",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0xe66d0dc85202757228ed7c7ea7138710b00755f69d604e6a7199c2759370aef8"
|
||||
},
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0x470591c033f228afd59d498fcf42ecba8b83d389042d5c43e5af277e8deed9fe"
|
||||
},
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0x86543ba39726b072d1b338fc9bf4852b5707ccfdf9abaf6ce21b85d76591b3cd"
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0x0f98bf0636a4f08d09d40b227beb78d7aaf57d6c856dd44ea0c6c29cc965c6c2"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0xca73109b8c348b6df6723c5a67616d2406784d56718731264e97a49cf4d314fb"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0x1a3c1319ece46cec553198ce7fb164670a5322a97afbcf432a44d524ace7267d"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0xb2617627421f96ca8e732982a756dd98ff8889871f64def89f2971ead24c8fe7"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0x8b2831993c44c6baa40677a9ac5055f244f15ed005cddf3419226c86280ff93a"
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0x6957b2bd0f9f04f7cc124c11638be50ff5e2a26412e0be0c16f7aac1f1b73bc6"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
|
||||
@@ -6825,6 +6891,16 @@
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tx": "0xe3fd6d538990df7bf818b8a178ecf7135a5e34f4d07778e9d6dc637cf470e2e0"
|
||||
},
|
||||
{
|
||||
"amount": "12",
|
||||
"user": "0xeD96EAA7D15951ab128abF8918a4D69F0Eef9905",
|
||||
"tx": "0x3775c114790b9a57414975492eec548528a155927399bddc72c40e336f3c9f52"
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"user": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
|
||||
"tx": "0xa83f849c425b63fe042be7d606eaaf64f9e89c20c01aa5ec58a3ebf54c573eba"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
|
||||
@@ -16637,6 +16713,21 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "311",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tx": "0xa94f27a51d17008d4f8c62a11f65db9dc3f94eb430b2842b0cb8403c29951658"
|
||||
},
|
||||
{
|
||||
"amount": "10",
|
||||
"user": "0xa2920abaA03b696C7B90486600D578b455E11609",
|
||||
"tx": "0xc454f6f2572533714b74636b4243ba8fd5456a6f67ef8b93051588b156ab2093"
|
||||
},
|
||||
{
|
||||
"amount": "12",
|
||||
"user": "0xa10aD7E7712617fc4ABe0811D8a30fD96cE48F9f",
|
||||
"tx": "0x7ffe3e795b50a8449052143a739d53aec81c2f53c7981f31496b8c121bd4ec81"
|
||||
},
|
||||
{
|
||||
"amount": "200",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
@@ -17599,6 +17690,112 @@
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"address": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "45",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xa6c36806aba504d0a30c574f6ab4dc2d76aad26d8759c778feb6bf27729cddcf"
|
||||
},
|
||||
{
|
||||
"amount": "46",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x0f0f261dd8e05919d3c6c1d4358434e2c9c1b9087347702d2b8a4b1aeecd7e4b"
|
||||
},
|
||||
{
|
||||
"amount": "50",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xe66d0dc85202757228ed7c7ea7138710b00755f69d604e6a7199c2759370aef8"
|
||||
},
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x470591c033f228afd59d498fcf42ecba8b83d389042d5c43e5af277e8deed9fe"
|
||||
},
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x86543ba39726b072d1b338fc9bf4852b5707ccfdf9abaf6ce21b85d76591b3cd"
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x0f98bf0636a4f08d09d40b227beb78d7aaf57d6c856dd44ea0c6c29cc965c6c2"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xca73109b8c348b6df6723c5a67616d2406784d56718731264e97a49cf4d314fb"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x1a3c1319ece46cec553198ce7fb164670a5322a97afbcf432a44d524ace7267d"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xb2617627421f96ca8e732982a756dd98ff8889871f64def89f2971ead24c8fe7"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x8b2831993c44c6baa40677a9ac5055f244f15ed005cddf3419226c86280ff93a"
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x6957b2bd0f9f04f7cc124c11638be50ff5e2a26412e0be0c16f7aac1f1b73bc6"
|
||||
},
|
||||
{
|
||||
"amount": "100",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x4b249ec22c90d0f4d1d472c407e8c27b1a87db2c5d822d600636f9b17406588a"
|
||||
},
|
||||
{
|
||||
"amount": "60",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x6a8d2bd962a14937de4beb99bff97cf55fd823de00b96c49e5e4e97b5b68f40f"
|
||||
},
|
||||
{
|
||||
"amount": "40",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xea7d189a9ad2d7ee3a93dd352201d19b1128ccf46246b1ec346c8ee1b9f7ea62"
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "311",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xa94f27a51d17008d4f8c62a11f65db9dc3f94eb430b2842b0cb8403c29951658"
|
||||
},
|
||||
{
|
||||
"amount": "200",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xf7ad77b4bb44e24178d182db393ace12f22021706d6583d438b285621eaf8b01"
|
||||
}
|
||||
],
|
||||
"total_tokens": "511",
|
||||
"withdrawn_tokens": "511",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
|
||||
"deposits": [
|
||||
@@ -17680,6 +17877,12 @@
|
||||
"tranche_id": 11,
|
||||
"tx": "0x304ceeda11c67174ffc52347ec240ea5aa832675cbb1c828adc098c8886f96fc"
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"user": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xa83f849c425b63fe042be7d606eaaf64f9e89c20c01aa5ec58a3ebf54c573eba"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
|
||||
@@ -18367,9 +18570,9 @@
|
||||
"tx": "0x6940787f6ceaac0846e69f1bc93ad9efee8c39774b91ea29f84328ae64fc9969"
|
||||
}
|
||||
],
|
||||
"total_tokens": "2585",
|
||||
"total_tokens": "2600",
|
||||
"withdrawn_tokens": "2585",
|
||||
"remaining_tokens": "0"
|
||||
"remaining_tokens": "15"
|
||||
},
|
||||
{
|
||||
"address": "0xE9F41a0090fcc7eaf626037003AAD44B17098E7C",
|
||||
@@ -18591,6 +18794,21 @@
|
||||
"withdrawn_tokens": "124",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xeD96EAA7D15951ab128abF8918a4D69F0Eef9905",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "12",
|
||||
"user": "0xeD96EAA7D15951ab128abF8918a4D69F0Eef9905",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x3775c114790b9a57414975492eec548528a155927399bddc72c40e336f3c9f52"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"total_tokens": "12",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "12"
|
||||
},
|
||||
{
|
||||
"address": "0xc7eC0d77a1417C97fb455f404c4B425A69a0004d",
|
||||
"deposits": [
|
||||
@@ -18922,40 +19140,6 @@
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "905"
|
||||
},
|
||||
{
|
||||
"address": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "100",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x4b249ec22c90d0f4d1d472c407e8c27b1a87db2c5d822d600636f9b17406588a"
|
||||
},
|
||||
{
|
||||
"amount": "60",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x6a8d2bd962a14937de4beb99bff97cf55fd823de00b96c49e5e4e97b5b68f40f"
|
||||
},
|
||||
{
|
||||
"amount": "40",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xea7d189a9ad2d7ee3a93dd352201d19b1128ccf46246b1ec346c8ee1b9f7ea62"
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "200",
|
||||
"user": "0xa7d3D2CC58e1E82e3ff2C2Cd3B750442522aBbfb",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xf7ad77b4bb44e24178d182db393ace12f22021706d6583d438b285621eaf8b01"
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "200",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x6ebf587df7C5C5eb49BB570c240563a98E1b8f4f",
|
||||
"deposits": [
|
||||
@@ -30850,10 +31034,17 @@
|
||||
"tx": "0xee0be2716f9467edff80ca7225263c95e69db8d7f020655ab3621b0966d0a311"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "12",
|
||||
"user": "0xa10aD7E7712617fc4ABe0811D8a30fD96cE48F9f",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x7ffe3e795b50a8449052143a739d53aec81c2f53c7981f31496b8c121bd4ec81"
|
||||
}
|
||||
],
|
||||
"total_tokens": "12",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "12"
|
||||
"withdrawn_tokens": "12",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x804dEbc8807aEe993a727B37d81D7A379DA59fE6",
|
||||
@@ -31279,10 +31470,17 @@
|
||||
"tx": "0xaa605daf521cdcdbbc1d6942a69e1efc86eac89d5804e05f088bce2fabe3d52a"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "10",
|
||||
"user": "0xa2920abaA03b696C7B90486600D578b455E11609",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xc454f6f2572533714b74636b4243ba8fd5456a6f67ef8b93051588b156ab2093"
|
||||
}
|
||||
],
|
||||
"total_tokens": "10",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "10"
|
||||
"withdrawn_tokens": "10",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xA2B5F0114E7935EFcBd7c038d789Bfa71f2Bfe5D",
|
||||
@@ -33097,7 +33295,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "3732368.4671",
|
||||
"total_removed": "442882.3484327902809",
|
||||
"locked_amount": "1055551.66611715142252045388",
|
||||
"locked_amount": "1033104.123341510407167357286",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1998.95815",
|
||||
@@ -34388,8 +34586,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "15870102.715470999700000001",
|
||||
"total_removed": "546406.58625796862040452",
|
||||
"locked_amount": "9042606.0207976516806447593336162220275751",
|
||||
"total_removed": "549182.48543502092691952",
|
||||
"locked_amount": "8963008.4753852283836950014010192461651798",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "16249.93",
|
||||
@@ -34893,6 +35091,16 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "856.08784586478614",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tx": "0x7882fc86536accee89368b825b374eb365ee5f051cb89fb3710c7e2d24b0d29d"
|
||||
},
|
||||
{
|
||||
"amount": "966.75883976995675",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x6c49f9f742a84f7889b90e6f978f3fb1f642ea447f737f348b3fac91716b9717"
|
||||
},
|
||||
{
|
||||
"amount": "858.360074993579125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -35003,6 +35211,11 @@
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
"tx": "0xc6a7b120dc629017c645e277e57c96fec82c78b518c0b54809dbc10f566e204d"
|
||||
},
|
||||
{
|
||||
"amount": "953.052491417563625",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x74f9a06c6e8b7807b9c369ec3762e5d4b518f32303faf8c1d4098f9d33f4a997"
|
||||
},
|
||||
{
|
||||
"amount": "434.254023685890375",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -36471,6 +36684,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "966.75883976995675",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x6c49f9f742a84f7889b90e6f978f3fb1f642ea447f737f348b3fac91716b9717"
|
||||
},
|
||||
{
|
||||
"amount": "858.360074993579125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -36537,6 +36756,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0x6d913b124af23621f8b97087a51d4cd912c6a8d9cca325cb756b56d4970f3f62"
|
||||
},
|
||||
{
|
||||
"amount": "953.052491417563625",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x74f9a06c6e8b7807b9c369ec3762e5d4b518f32303faf8c1d4098f9d33f4a997"
|
||||
},
|
||||
{
|
||||
"amount": "434.254023685890375",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -37553,8 +37778,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "259998.8875",
|
||||
"withdrawn_tokens": "111222.20209807200725",
|
||||
"remaining_tokens": "148776.68540192799275"
|
||||
"withdrawn_tokens": "113142.013429259527625",
|
||||
"remaining_tokens": "146856.874070740472375"
|
||||
},
|
||||
{
|
||||
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
|
||||
@@ -37775,6 +38000,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "856.08784586478614",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x7882fc86536accee89368b825b374eb365ee5f051cb89fb3710c7e2d24b0d29d"
|
||||
},
|
||||
{
|
||||
"amount": "1293.67099136315494",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
@@ -37975,8 +38206,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "150551.801",
|
||||
"withdrawn_tokens": "64216.65872637537368",
|
||||
"remaining_tokens": "86335.14227362462632"
|
||||
"withdrawn_tokens": "65072.74657224015982",
|
||||
"remaining_tokens": "85479.05442775984018"
|
||||
},
|
||||
{
|
||||
"address": "0x4d982Ab0823fD2f48e934a7be2bb0a5374a26148",
|
||||
@@ -39701,8 +39932,8 @@
|
||||
"tranche_start": "2021-11-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-05T00:00:00.000Z",
|
||||
"total_added": "14597706.0446472999",
|
||||
"total_removed": "3706973.15022981060693393",
|
||||
"locked_amount": "2626630.914525500348909892987701163",
|
||||
"total_removed": "3709441.39326687814680893",
|
||||
"locked_amount": "2553146.968835877717601199418901287",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129284.449",
|
||||
@@ -39911,6 +40142,11 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "1333.9237119810715295",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xc8aae35d9d474b83dd5524de9db59f994fd77040397c00a1a4cf30a5c9315826"
|
||||
},
|
||||
{
|
||||
"amount": "8950.14985089483210984",
|
||||
"user": "0x66827bCD635f2bB1779d68c46aEB16541bCA6ba8",
|
||||
@@ -40001,6 +40237,11 @@
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xc1e8ef457dbeb52bd2d02aefd418a9a67bd17f016c1ca6a1e80716d86ce3c132"
|
||||
},
|
||||
{
|
||||
"amount": "1134.3193250864683455",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xfb5cc9685a3c4f5174ff68cc066d8da5121f833d7539f53a78db46c2427eda9b"
|
||||
},
|
||||
{
|
||||
"amount": "12758.73908063512839093",
|
||||
"user": "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E",
|
||||
@@ -42707,6 +42948,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "1333.9237119810715295",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xc8aae35d9d474b83dd5524de9db59f994fd77040397c00a1a4cf30a5c9315826"
|
||||
},
|
||||
{
|
||||
"amount": "1192.05386354121365675",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -42779,6 +43026,12 @@
|
||||
"tranche_id": 3,
|
||||
"tx": "0xc1e8ef457dbeb52bd2d02aefd418a9a67bd17f016c1ca6a1e80716d86ce3c132"
|
||||
},
|
||||
{
|
||||
"amount": "1134.3193250864683455",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xfb5cc9685a3c4f5174ff68cc066d8da5121f833d7539f53a78db46c2427eda9b"
|
||||
},
|
||||
{
|
||||
"amount": "762.7902369187144135",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -45091,8 +45344,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "359123.469575",
|
||||
"withdrawn_tokens": "293815.097748589450163",
|
||||
"remaining_tokens": "65308.371826410549837"
|
||||
"withdrawn_tokens": "296283.340785656990038",
|
||||
"remaining_tokens": "62840.128789343009962"
|
||||
},
|
||||
{
|
||||
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
|
||||
@@ -46403,7 +46656,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "5778205.3912159303",
|
||||
"total_removed": "2622261.560853924298939789",
|
||||
"locked_amount": "720894.356520356942099119968730613",
|
||||
"locked_amount": "691860.405151183386025619771012724",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "552496.6455",
|
||||
@@ -48297,8 +48550,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "472355.6199999996",
|
||||
"total_removed": "29683.1054262326685",
|
||||
"locked_amount": "167258.19528682252509125997412482",
|
||||
"total_removed": "31616.1712341930685",
|
||||
"locked_amount": "163701.253818397273052118746829",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "3000",
|
||||
@@ -54972,6 +55225,46 @@
|
||||
"user": "0xf751033D4e6864a88Be93E36258246F26AEf577c",
|
||||
"tx": "0xb912d9e24a80ec0d9ed4f3b03c20937d4fec08dd99031447ba84a8b6a69ef351"
|
||||
},
|
||||
{
|
||||
"amount": "39.564687976",
|
||||
"user": "0xD27929d68ac0E5fd5C919A5eb5968C1D06D3Fb83",
|
||||
"tx": "0x63c2319241d05630a6d9ea104770cdcb6d93d145554bf2ff9d82cd8ef4e17fc7"
|
||||
},
|
||||
{
|
||||
"amount": "129.409734906",
|
||||
"user": "0x6E624B5788CD721F489074993d5E1Ee2C477eB00",
|
||||
"tx": "0x9b25e6fb021dd73d4022c6879da50a0e2b818a46955153a90e44008f5f4f5a52"
|
||||
},
|
||||
{
|
||||
"amount": "129.527999746",
|
||||
"user": "0x4683B77D114a04f1602dda7B0Ab2A24Fe79295fD",
|
||||
"tx": "0x0a2ab8841a315a8f2bcf534953ff9058607709c1b761f4fded8ba238ea3f3802"
|
||||
},
|
||||
{
|
||||
"amount": "37.881582952",
|
||||
"user": "0xcB555C5602cC0b2434A27d277e14Cb5C7bF4Ead1",
|
||||
"tx": "0x56e6ab9b05da1003e1070f9ffb5cc54038420d69751d49f700bdb008f8600701"
|
||||
},
|
||||
{
|
||||
"amount": "172.613698626",
|
||||
"user": "0x83e600Ae7f4cf265C112314839cDA2341198840B",
|
||||
"tx": "0x871fbcbfe5c488654b8aff67f1c4afff2f5940ea3eb0cdf165b159bb96ee7ffd"
|
||||
},
|
||||
{
|
||||
"amount": "157.9045256224",
|
||||
"user": "0x74Da54F44975a1C224ABb5CAB8e5fc38a268B425",
|
||||
"tx": "0x65f234b1c4da032348199c9b165cdaee8817e428d1bf357309260ac5d33279d5"
|
||||
},
|
||||
{
|
||||
"amount": "1167.346518264",
|
||||
"user": "0x17197a34E926539066de1987d462c996c797b772",
|
||||
"tx": "0xba73b47fe051e5544519f878f34a69a9251494ea5d6a883f308df59c9f0e29c5"
|
||||
},
|
||||
{
|
||||
"amount": "98.817059868",
|
||||
"user": "0x175BEB5A0b07C9FBb640CF8b97352B3F1534E7b3",
|
||||
"tx": "0x40192637adf93f4df1f1e9edb9d5e431e9a18e4642d0d843e1c6f64cba60ba44"
|
||||
},
|
||||
{
|
||||
"amount": "229.648541348",
|
||||
"user": "0x65C13724928ea0AfA68e09c7b70449bB8f6f3Fc8",
|
||||
@@ -56402,6 +56695,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "172.613698626",
|
||||
"user": "0x83e600Ae7f4cf265C112314839cDA2341198840B",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x871fbcbfe5c488654b8aff67f1c4afff2f5940ea3eb0cdf165b159bb96ee7ffd"
|
||||
},
|
||||
{
|
||||
"amount": "87.108637746",
|
||||
"user": "0x83e600Ae7f4cf265C112314839cDA2341198840B",
|
||||
@@ -56416,8 +56715,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "600",
|
||||
"withdrawn_tokens": "216.146099694",
|
||||
"remaining_tokens": "383.853900306"
|
||||
"withdrawn_tokens": "388.75979832",
|
||||
"remaining_tokens": "211.24020168"
|
||||
},
|
||||
{
|
||||
"address": "0x5Ef3F2723f8e8c45aF15Df67dBDfA8a8FF2C3E8F",
|
||||
@@ -67820,6 +68119,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "98.817059868",
|
||||
"user": "0x175BEB5A0b07C9FBb640CF8b97352B3F1534E7b3",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x40192637adf93f4df1f1e9edb9d5e431e9a18e4642d0d843e1c6f64cba60ba44"
|
||||
},
|
||||
{
|
||||
"amount": "30.893512176",
|
||||
"user": "0x175BEB5A0b07C9FBb640CF8b97352B3F1534E7b3",
|
||||
@@ -67828,8 +68133,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "30.893512176",
|
||||
"remaining_tokens": "169.106487824"
|
||||
"withdrawn_tokens": "129.710572044",
|
||||
"remaining_tokens": "70.289427956"
|
||||
},
|
||||
{
|
||||
"address": "0xB95C140fB5c6c881eDc32be1e220D2bD5D8f9c36",
|
||||
@@ -72017,6 +72322,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "157.9045256224",
|
||||
"user": "0x74Da54F44975a1C224ABb5CAB8e5fc38a268B425",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x65f234b1c4da032348199c9b165cdaee8817e428d1bf357309260ac5d33279d5"
|
||||
},
|
||||
{
|
||||
"amount": "49.4479147616",
|
||||
"user": "0x74Da54F44975a1C224ABb5CAB8e5fc38a268B425",
|
||||
@@ -72025,8 +72336,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "320",
|
||||
"withdrawn_tokens": "49.4479147616",
|
||||
"remaining_tokens": "270.5520852384"
|
||||
"withdrawn_tokens": "207.352440384",
|
||||
"remaining_tokens": "112.647559616"
|
||||
},
|
||||
{
|
||||
"address": "0xB7D725753a300FeD6D13f3951D890856EF0C6e30",
|
||||
@@ -72670,6 +72981,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "37.881582952",
|
||||
"user": "0xcB555C5602cC0b2434A27d277e14Cb5C7bF4Ead1",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x56e6ab9b05da1003e1070f9ffb5cc54038420d69751d49f700bdb008f8600701"
|
||||
},
|
||||
{
|
||||
"amount": "40.914155252",
|
||||
"user": "0xcB555C5602cC0b2434A27d277e14Cb5C7bF4Ead1",
|
||||
@@ -72684,8 +73001,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "221.208206492",
|
||||
"remaining_tokens": "178.791793508"
|
||||
"withdrawn_tokens": "259.089789444",
|
||||
"remaining_tokens": "140.910210556"
|
||||
},
|
||||
{
|
||||
"address": "0x3738bec36216eA2F11B954891C65AAd1Bc852156",
|
||||
@@ -74731,10 +75048,17 @@
|
||||
"tx": "0xb59405747c8088945a412703637a7b422f3639439ec2ee15e180c0a2a0d71ee4"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "1167.346518264",
|
||||
"user": "0x17197a34E926539066de1987d462c996c797b772",
|
||||
"tranche_id": 5,
|
||||
"tx": "0xba73b47fe051e5544519f878f34a69a9251494ea5d6a883f308df59c9f0e29c5"
|
||||
}
|
||||
],
|
||||
"total_tokens": "1800",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "1800"
|
||||
"withdrawn_tokens": "1167.346518264",
|
||||
"remaining_tokens": "632.653481736"
|
||||
},
|
||||
{
|
||||
"address": "0x854a6b84ad48645eac476D0D8197f854C06DD79D",
|
||||
@@ -75142,10 +75466,17 @@
|
||||
"tx": "0xb59405747c8088945a412703637a7b422f3639439ec2ee15e180c0a2a0d71ee4"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "129.409734906",
|
||||
"user": "0x6E624B5788CD721F489074993d5E1Ee2C477eB00",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x9b25e6fb021dd73d4022c6879da50a0e2b818a46955153a90e44008f5f4f5a52"
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "200"
|
||||
"withdrawn_tokens": "129.409734906",
|
||||
"remaining_tokens": "70.590265094"
|
||||
},
|
||||
{
|
||||
"address": "0xa9a677b0a3Be231C0654CabB0Eaa0A72E91B3E8d",
|
||||
@@ -76059,6 +76390,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "39.564687976",
|
||||
"user": "0xD27929d68ac0E5fd5C919A5eb5968C1D06D3Fb83",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x63c2319241d05630a6d9ea104770cdcb6d93d145554bf2ff9d82cd8ef4e17fc7"
|
||||
},
|
||||
{
|
||||
"amount": "38.141400304",
|
||||
"user": "0xD27929d68ac0E5fd5C919A5eb5968C1D06D3Fb83",
|
||||
@@ -76109,8 +76446,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "219.120230844",
|
||||
"remaining_tokens": "180.879769156"
|
||||
"withdrawn_tokens": "258.68491882",
|
||||
"remaining_tokens": "141.31508118"
|
||||
},
|
||||
{
|
||||
"address": "0xF5037DDA4A660d67560200f45380FF8364e35540",
|
||||
@@ -76174,10 +76511,17 @@
|
||||
"tx": "0xe32a466fc780a0fb3fd84a804f622931ebfaf3f428bff0dc6d141270410e75f8"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "129.527999746",
|
||||
"user": "0x4683B77D114a04f1602dda7B0Ab2A24Fe79295fD",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x0a2ab8841a315a8f2bcf534953ff9058607709c1b761f4fded8ba238ea3f3802"
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "200"
|
||||
"withdrawn_tokens": "129.527999746",
|
||||
"remaining_tokens": "70.472000254"
|
||||
},
|
||||
{
|
||||
"address": "0x0A67488B946C5Ad7e3afc8766316C9Dc17333C6B",
|
||||
@@ -76692,7 +77036,7 @@
|
||||
"tranche_start": "2021-12-05T00:00:00.000Z",
|
||||
"tranche_end": "2022-06-05T00:00:00.000Z",
|
||||
"total_added": "171288.42",
|
||||
"total_removed": "63896.1049690697989",
|
||||
"total_removed": "64226.1049690697989",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@@ -80917,6 +81261,11 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0x4A13d4dC5e06ACdA81C011D55a7DaAc332bC5Dbf",
|
||||
"tx": "0x72e95b2e51cae897d2c09ca7294c630fb3b969aea950a53e10158570faee89c7"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0xbd09687340A09BeB0B5EE0D3C2bCa8d78eBF6E63",
|
||||
@@ -80952,6 +81301,16 @@
|
||||
"user": "0x92Faf283Ff5Bca05673411091D922a054A959900",
|
||||
"tx": "0x5f47c30b18bfd7c1d7997ebd04ea2460b72f6a5880385075e37090428711215f"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0xaD8a77d52Dd1E2dDD364AfE960DF49CAe1124C6D",
|
||||
"tx": "0xd8084414ae0dcff7b19cc85b92bc4898cb656f485e51bc8f8b0d499f97d5e212"
|
||||
},
|
||||
{
|
||||
"amount": "50",
|
||||
"user": "0xa11930Cd107D363aCc44223F03dDF899ac1c864C",
|
||||
"tx": "0x9d260c971e699ed0255fe230c306c70f076064466c0b50c855a234c6e5034156"
|
||||
},
|
||||
{
|
||||
"amount": "50",
|
||||
"user": "0x300A831523b53112F5CF8B802D85EC48a8CB7dcC",
|
||||
@@ -93380,10 +93739,17 @@
|
||||
"tx": "0x9f916cf09e8a3c4ade0ffce5190db464d0a2b1dadba78e1ee7ba5d6e751d6148"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0xaD8a77d52Dd1E2dDD364AfE960DF49CAe1124C6D",
|
||||
"tranche_id": 6,
|
||||
"tx": "0xd8084414ae0dcff7b19cc85b92bc4898cb656f485e51bc8f8b0d499f97d5e212"
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "250"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xc140931E1A188667395734771f195Ce832c40922",
|
||||
@@ -97107,10 +97473,17 @@
|
||||
"tx": "0xb59405747c8088945a412703637a7b422f3639439ec2ee15e180c0a2a0d71ee4"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0x4A13d4dC5e06ACdA81C011D55a7DaAc332bC5Dbf",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x72e95b2e51cae897d2c09ca7294c630fb3b969aea950a53e10158570faee89c7"
|
||||
}
|
||||
],
|
||||
"total_tokens": "30",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "30"
|
||||
"withdrawn_tokens": "30",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x02e3d96c448d38790151930A39c36273904C1b0B",
|
||||
@@ -97884,10 +98257,17 @@
|
||||
"tx": "0xe32a466fc780a0fb3fd84a804f622931ebfaf3f428bff0dc6d141270410e75f8"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "50",
|
||||
"user": "0xa11930Cd107D363aCc44223F03dDF899ac1c864C",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x9d260c971e699ed0255fe230c306c70f076064466c0b50c855a234c6e5034156"
|
||||
}
|
||||
],
|
||||
"total_tokens": "50",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "50"
|
||||
"withdrawn_tokens": "50",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x75AB80d94F9F7C0FE4E7973C8fF505882bE80f97",
|
||||
|
||||
@@ -5,10 +5,12 @@ import { Heading } from '../../components/heading';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
import {
|
||||
useWithdrawals,
|
||||
withdrawalProvider,
|
||||
useWithdrawalDialog,
|
||||
WithdrawalsTable,
|
||||
} from '@vegaprotocol/withdraws';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import type { RouteChildProps } from '../index';
|
||||
|
||||
@@ -29,7 +31,12 @@ const Withdrawals = ({ name }: RouteChildProps) => {
|
||||
const WithdrawPendingContainer = () => {
|
||||
const openWithdrawalDialog = useWithdrawalDialog((state) => state.open);
|
||||
const { t } = useTranslation();
|
||||
const { data, loading, error } = useWithdrawals();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: withdrawalProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@@ -21,15 +21,21 @@ const orderUpdatedAt = 'updatedAt';
|
||||
const assetSelectField = 'select[name="asset"]';
|
||||
const amountField = 'input[name="amount"]';
|
||||
const txTimeout = Cypress.env('txTimeout');
|
||||
const btcName = 'BTC (local)';
|
||||
const sepoliaUrl = Cypress.env('ETHERSCAN_URL');
|
||||
const btcName =
|
||||
'BTC (local)5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c - tBTC';
|
||||
const btcSymbol = 'tBTC';
|
||||
const usdcSymbol = 'fUSDC';
|
||||
const toastContent = 'toast-content';
|
||||
const ordersTab = 'Orders';
|
||||
const depositsTab = 'Deposits';
|
||||
const toastCloseBtn = 'toast-close';
|
||||
const price = '390';
|
||||
const size = '0.0005';
|
||||
const newPrice = '200';
|
||||
|
||||
// TODO: ensure this test runs only if capsule is running via workflow
|
||||
// Because the tests are run on a live network to optimize time, the tests are interdependent and must be run in the given order.
|
||||
describe('capsule', { tags: '@slow' }, () => {
|
||||
before(() => {
|
||||
cy.createMarket();
|
||||
@@ -50,8 +56,8 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
marketId: market.id,
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '0.0005',
|
||||
price: '390',
|
||||
size: size,
|
||||
price: price,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
};
|
||||
const rawPrice = removeDecimal(order.price, market.decimalPlaces);
|
||||
@@ -64,7 +70,8 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
|
||||
cy.getByTestId(toastContent).should(
|
||||
'contain.text',
|
||||
`ConfirmedYour transaction has been confirmed View in block explorerSubmit order - activeTEST.24h+0.0005 @ 390.00 ${usdcSymbol}`
|
||||
`ConfirmedYour transaction has been confirmed View in block explorerSubmit order - activeTEST.24h+${order.size} @ ${order.price}.00 ${usdcSymbol}`,
|
||||
{ matchCase: false }
|
||||
);
|
||||
cy.getByTestId(toastCloseBtn).click();
|
||||
// orderbook cells are keyed by price level
|
||||
@@ -75,7 +82,7 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
.should('contain.text', rawSize);
|
||||
|
||||
cy.getByTestId(ordersTab).click();
|
||||
cy.getByTestId('edit').should('contain.text', 'Edit');
|
||||
cy.getByTestId('edit', txTimeout).should('contain.text', 'Edit');
|
||||
cy.getByTestId('tab-orders').within(() => {
|
||||
cy.get('.ag-center-cols-container')
|
||||
.children()
|
||||
@@ -113,35 +120,41 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
checkIfDataAndTimeOfCreationAndUpdateIsEqual(orderCreatedAt);
|
||||
});
|
||||
});
|
||||
//edit order
|
||||
});
|
||||
|
||||
it('can edit order', function () {
|
||||
cy.getByTestId(ordersTab).click();
|
||||
cy.getByTestId('edit').first().should('be.visible').click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Edit order');
|
||||
cy.get('#limitPrice').focus().clear().type('200');
|
||||
cy.get('#limitPrice').focus().clear().type(newPrice);
|
||||
cy.getByTestId('edit-order').find('[type="submit"]').click();
|
||||
cy.getByTestId(toastContent).should(
|
||||
'contain.text',
|
||||
`ConfirmedYour transaction has been confirmed View in block explorerEdit order - activeTEST.24h+0.0005 @ 200.00 ${usdcSymbol}+0.0005 @ 200.00 ${usdcSymbol}`
|
||||
`ConfirmedYour transaction has been confirmed View in block explorerEdit order - activeTEST.24h+${size} @ ${price}.00 ${usdcSymbol}+${size} @ ${newPrice}.00 ${usdcSymbol}`,
|
||||
{ matchCase: false }
|
||||
);
|
||||
cy.getByTestId(ordersTab).click();
|
||||
cy.getByTestId(toastCloseBtn).click();
|
||||
cy.getByTestId(toastCloseBtn).click({ multiple: true });
|
||||
cy.get('.ag-center-cols-container')
|
||||
.children()
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(`[col-id='${orderPrice}']`).then(($price) => {
|
||||
expect(parseFloat($price.text())).to.equal(parseFloat('200'));
|
||||
expect(parseFloat($price.text())).to.equal(parseFloat(newPrice));
|
||||
});
|
||||
checkIfDataAndTimeOfCreationAndUpdateIsEqual(orderUpdatedAt);
|
||||
});
|
||||
//cancel order
|
||||
});
|
||||
|
||||
it('can cancel order', function () {
|
||||
cy.getByTestId(ordersTab).click();
|
||||
cy.getByTestId('cancel').first().click();
|
||||
cy.getByTestId(toastContent).should(
|
||||
'contain.text',
|
||||
`ConfirmedYour transaction has been confirmed View in block explorerCancel order - cancelledTEST.24h+0.0005 @ 200.00 ${usdcSymbol}`
|
||||
`ConfirmedYour transaction has been confirmed View in block explorerCancel order - cancelledTEST.24h+${size} @ ${newPrice}.00 ${usdcSymbol}`,
|
||||
{ matchCase: false }
|
||||
);
|
||||
cy.getByTestId(toastCloseBtn).click();
|
||||
cy.getByTestId(toastCloseBtn).click({ multiple: true });
|
||||
|
||||
cy.getByTestId('tab-orders')
|
||||
.get('.ag-center-cols-container')
|
||||
@@ -151,7 +164,10 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
.should('contain.text', OrderStatusMapping.STATUS_CANCELLED);
|
||||
});
|
||||
|
||||
it('can deposit and withdrawal', function () {
|
||||
it('can deposit', function () {
|
||||
cy.visit('/#/portfolio');
|
||||
cy.get('main[data-testid="/portfolio"]').should('exist');
|
||||
|
||||
// 1001-DEPO-001
|
||||
// 1001-DEPO-002
|
||||
// 1001-DEPO-003
|
||||
@@ -160,27 +176,12 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
// 1001-DEPO-007
|
||||
// 1001-DEPO-008
|
||||
// 1001-DEPO-009
|
||||
// 1002-WITH-001
|
||||
// 1002-WITH-006
|
||||
// 1002-WITH-009
|
||||
// 002-WITH-011
|
||||
// 1002-WITH-024
|
||||
// 1002-WITH-012
|
||||
// 1002-WITH-013
|
||||
// 1002-WITH-014
|
||||
// 1002-WITH-015
|
||||
// 1002-WITH-016
|
||||
// 1002-WITH-019
|
||||
|
||||
cy.visit('/#/portfolio');
|
||||
cy.get('main[data-testid="/portfolio"]').should('exist');
|
||||
|
||||
cy.highlight('creating deposit');
|
||||
// 1001-DEPO-010
|
||||
|
||||
cy.getByTestId(depositsTab).click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField, txTimeout).select(btcName);
|
||||
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
||||
cy.getByTestId('deposit-approve-submit').click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
||||
cy.get('[data-testid="Return to deposit"]').click();
|
||||
@@ -188,7 +189,8 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId(toastContent, txTimeout).should(
|
||||
'contain.text',
|
||||
`Transaction completedYour transaction has been completedView on EtherscanDeposit 1.00 ${btcSymbol}`
|
||||
`Transaction confirmedYour transaction has been confirmed.View on EtherscanDeposit 1.00 ${btcSymbol}`,
|
||||
{ matchCase: false }
|
||||
);
|
||||
cy.getByTestId(toastCloseBtn).click();
|
||||
cy.getByTestId('Collateral').click();
|
||||
@@ -196,8 +198,6 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.highlight('deposit verification');
|
||||
|
||||
cy.getByTestId('asset', txTimeout).should('contain.text', btcSymbol);
|
||||
// need to reload page to see deposit history complete
|
||||
cy.reload();
|
||||
cy.getByTestId(depositsTab).click();
|
||||
cy.get('.ag-cell-value', txTimeout).should('contain.text', btcSymbol);
|
||||
cy.get('[col-id="status"]').should('not.have.text', 'Open', txTimeout);
|
||||
@@ -214,15 +214,29 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.get('[col-id="txHash"]')
|
||||
.find('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://sepolia.etherscan.io/tx');
|
||||
.and('contain', `${sepoliaUrl}/tx/0x`);
|
||||
});
|
||||
});
|
||||
|
||||
cy.highlight('creating withdrawals');
|
||||
it('can withdrawal', function () {
|
||||
// 1002-WITH-001
|
||||
// 1002-WITH-006
|
||||
// 1002-WITH-009
|
||||
// 1002-WITH-011
|
||||
// 1002-WITH-024
|
||||
// 1002-WITH-012
|
||||
// 1002-WITH-013
|
||||
// 1002-WITH-014
|
||||
// 1002-WITH-015
|
||||
// 1002-WITH-016
|
||||
// 1002-WITH-019
|
||||
|
||||
cy.getByTestId('Withdrawals').click();
|
||||
cy.getByTestId('withdraw-dialog-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField).select(btcName);
|
||||
cy.get(assetSelectField, txTimeout).select(
|
||||
'BTC (local)5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c - tBTC',
|
||||
{ force: true }
|
||||
);
|
||||
cy.get(amountField).clear().type('1');
|
||||
cy.getByTestId('submit-withdrawal').click();
|
||||
cy.getByTestId(toastContent, txTimeout).should(
|
||||
@@ -243,7 +257,7 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('toast-complete-withdrawal').click();
|
||||
cy.getByTestId(toastContent, txTimeout).should(
|
||||
'contain.text',
|
||||
'Transaction completed'
|
||||
'Transaction confirmed'
|
||||
);
|
||||
|
||||
cy.getByTestId('complete-withdrawal', txTimeout).should('not.exist');
|
||||
@@ -258,17 +272,28 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.get('[col-id="details.receiverAddress"]')
|
||||
.find('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://sepolia.etherscan.io/address/');
|
||||
.and('contain', `${sepoliaUrl}/address/`);
|
||||
cy.get('[col-id="createdTimestamp"]').should('not.be.empty');
|
||||
cy.get('[col-id="withdrawnTimestamp"]').should('not.be.empty');
|
||||
cy.get('[col-id="status"]').should('have.text', 'Completed');
|
||||
cy.get('[col-id="txHash"]')
|
||||
.find('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://sepolia.etherscan.io/tx/0x');
|
||||
.and('contain', `${sepoliaUrl}/tx/0x`);
|
||||
});
|
||||
});
|
||||
|
||||
it('deposit - if approved amount is less than deposit: must see that an approval is needed and be prompted to approve more', function () {
|
||||
// 1001-DEPO-006
|
||||
|
||||
cy.getByTestId(depositsTab).click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
||||
cy.get(amountField).clear().type('20000000');
|
||||
cy.getByTestId('deposit-approve-submit').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
function checkIfDataAndTimeOfCreationAndUpdateIsEqual(date: string) {
|
||||
cy.get(`[col-id='${date}'] .ag-cell-wrapper`)
|
||||
.children('span')
|
||||
|
||||
@@ -56,6 +56,7 @@ 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
|
||||
cy.get(amountField)
|
||||
|
||||
@@ -454,8 +454,4 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
it.skip('tbd for 7003-MORD', () => {
|
||||
// NOT COVERED: must see the reference, offset and direction for each part pegged order - waiting for clarification
|
||||
// NOT COVERED: must see the reference, offset and direction for each part liquidity order order - waiting for clarification
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useDepositDialog, DepositsTable } from '@vegaprotocol/deposits';
|
||||
import { useDeposits } from '@vegaprotocol/deposits';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { depositsProvider } from '@vegaprotocol/deposits';
|
||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
export const DepositsContainer = () => {
|
||||
const { deposits, loading, error } = useDeposits();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: depositsProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
const openDepositDialog = useDepositDialog((state) => state.open);
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[1fr,min-content]">
|
||||
<div className="h-full relative">
|
||||
<DepositsTable
|
||||
rowData={deposits || []}
|
||||
rowData={data || []}
|
||||
noRowsOverlayComponent={() => null}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
data={deposits}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useWithdrawals,
|
||||
withdrawalProvider,
|
||||
useWithdrawalDialog,
|
||||
WithdrawalsTable,
|
||||
} from '@vegaprotocol/withdraws';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
|
||||
export const WithdrawalsContainer = () => {
|
||||
const { data, loading, error } = useWithdrawals();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: withdrawalProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
const openWithdrawDialog = useWithdrawalDialog((state) => state.open);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,26 +1,58 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Footer } from './footer';
|
||||
import { Footer, NodeHealth } from './footer';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
jest.mock('@vegaprotocol/environment');
|
||||
|
||||
describe('Footer', () => {
|
||||
it('renders a button to open node switcher', () => {
|
||||
it('can open node switcher by clicking the node url', () => {
|
||||
const mockOpenNodeSwitcher = jest.fn();
|
||||
const node = 'n99.somenetwork.vega.xyz';
|
||||
const nodeUrl = `https://${node}`;
|
||||
|
||||
// @ts-ignore mock env hook
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_URL: `https://api.${node}/graphql`,
|
||||
blockDifference: 0,
|
||||
setNodeSwitcherOpen: mockOpenNodeSwitcher,
|
||||
}));
|
||||
|
||||
render(<Footer />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
fireEvent.click(screen.getByText(node));
|
||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can open node switcher by clicking health', () => {
|
||||
const mockOpenNodeSwitcher = jest.fn();
|
||||
const node = 'n99.somenetwork.vega.xyz';
|
||||
|
||||
// @ts-ignore mock env hook
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_URL: `https://api.${node}/graphql`,
|
||||
blockDifference: 0,
|
||||
setNodeSwitcherOpen: mockOpenNodeSwitcher,
|
||||
}));
|
||||
|
||||
render(<Footer />);
|
||||
|
||||
fireEvent.click(screen.getByText('Operational'));
|
||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||
const link = screen.getByText(node);
|
||||
expect(link).toHaveAttribute('href', nodeUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NodeHealth', () => {
|
||||
const cases = [
|
||||
{ diff: 0, classname: 'bg-success', text: 'Operational' },
|
||||
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
||||
{ diff: -1, classname: 'bg-danger', text: 'Non operational' },
|
||||
];
|
||||
it.each(cases)(
|
||||
'renders correct text and indicator color for $diff block difference',
|
||||
(elem) => {
|
||||
console.log(elem);
|
||||
render(<NodeHealth blockDiff={elem.diff} openNodeSwitcher={jest.fn()} />);
|
||||
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
||||
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,28 +1,75 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { ButtonLink, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { t, useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||
import { ButtonLink, Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const Footer = () => {
|
||||
const { VEGA_URL, setNodeSwitcherOpen } = useEnvironment();
|
||||
const { VEGA_URL, blockDifference, setNodeSwitcherOpen } = useEnvironment();
|
||||
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 && <NodeUrl url={VEGA_URL} />}
|
||||
<ButtonLink onClick={setNodeSwitcherOpen}>{t('Change')}</ButtonLink>
|
||||
{VEGA_URL && (
|
||||
<>
|
||||
<NodeHealth
|
||||
blockDiff={blockDifference}
|
||||
openNodeSwitcher={setNodeSwitcherOpen}
|
||||
/>
|
||||
{' | '}
|
||||
<NodeUrl url={VEGA_URL} openNodeSwitcher={setNodeSwitcherOpen} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeUrl = ({ url }: { url: string }) => {
|
||||
interface NodeUrlProps {
|
||||
url: string;
|
||||
openNodeSwitcher: () => void;
|
||||
}
|
||||
|
||||
const NodeUrl = ({ url, openNodeSwitcher }: 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>;
|
||||
};
|
||||
|
||||
interface NodeHealthProps {
|
||||
openNodeSwitcher: () => void;
|
||||
blockDiff: number;
|
||||
}
|
||||
|
||||
// How many blocks behind the most advanced block that is
|
||||
// deemed acceptable for "Good" status
|
||||
const BLOCK_THRESHOLD = 3;
|
||||
|
||||
export const NodeHealth = ({
|
||||
blockDiff,
|
||||
openNodeSwitcher,
|
||||
}: NodeHealthProps) => {
|
||||
const online = useNavigatorOnline();
|
||||
|
||||
let intent = Intent.Success;
|
||||
let text = 'Operational';
|
||||
|
||||
if (!online) {
|
||||
text = t('Offline');
|
||||
intent = Intent.Danger;
|
||||
} else if (blockDiff < 0) {
|
||||
// Block height query failed and null was returned
|
||||
text = t('Non operational');
|
||||
intent = Intent.Danger;
|
||||
} else if (blockDiff >= BLOCK_THRESHOLD) {
|
||||
text = t(`${blockDiff} Blocks behind`);
|
||||
intent = Intent.Warning;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={'https://' + nodeUrl} target="_blank">
|
||||
{nodeUrl}
|
||||
</Link>
|
||||
<span>
|
||||
<Indicator variant={intent} />
|
||||
<ButtonLink onClick={openNodeSwitcher}>{text}</ButtonLink>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -112,7 +112,7 @@ export const SelectMarketPopover = ({
|
||||
} = useMarketList();
|
||||
const variables = useMemo(() => ({ partyId: pubKey }), [pubKey]);
|
||||
const {
|
||||
data: party,
|
||||
data: positions,
|
||||
loading: positionsLoading,
|
||||
reload,
|
||||
} = useDataProvider({
|
||||
@@ -132,11 +132,9 @@ export const SelectMarketPopover = ({
|
||||
const markets = useMemo(
|
||||
() =>
|
||||
data?.filter((market) =>
|
||||
party?.positionsConnection?.edges?.find(
|
||||
(edge) => edge.node.market.id === market.id
|
||||
)
|
||||
positions?.find((node) => node.market.id === market.id)
|
||||
),
|
||||
[data, party]
|
||||
[data, positions]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -172,15 +170,13 @@ export const SelectMarketPopover = ({
|
||||
</div>
|
||||
) : (
|
||||
<table className="relative text-sm w-full whitespace-nowrap">
|
||||
{pubKey && (party?.positionsConnection?.edges?.length ?? 0) > 0 ? (
|
||||
{pubKey && (positions?.length ?? 0) && (markets?.length ?? 0) ? (
|
||||
<>
|
||||
<TableTitle>{t('My markets')}</TableTitle>
|
||||
<SelectAllMarketsTableBody
|
||||
inViewRoot={inViewRoot}
|
||||
markets={markets}
|
||||
positions={party?.positionsConnection?.edges
|
||||
?.filter((edge) => edge.node)
|
||||
.map((edge) => edge.node)}
|
||||
positions={positions || undefined}
|
||||
onSelect={onSelectMarket}
|
||||
onCellClick={onCellClick}
|
||||
headers={columnHeadersPositionMarkets}
|
||||
|
||||
@@ -96,7 +96,7 @@ const EthTxPendingToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Awaiting confirmation')}</h3>
|
||||
<p>{t('Please wait for your transaction to be confirmed')}</p>
|
||||
<p>{t('Please wait for your transaction to be confirmed.')}</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
@@ -138,7 +138,7 @@ const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Transaction confirmed')}</h3>
|
||||
<p>{t('Your transaction has been confirmed')}</p>
|
||||
<p>{t('Your transaction has been confirmed.')}</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
@@ -153,7 +153,7 @@ const EthTxCompletedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
{t('Processing')} {isDeposit && t('deposit')}
|
||||
</h3>
|
||||
<p>
|
||||
{t('Your transaction has been completed.')}
|
||||
{t('Your transaction has been completed.')}{' '}
|
||||
{isDeposit && t('Waiting for deposit confirmation.')}
|
||||
</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
|
||||
@@ -50,10 +50,10 @@ export type Account = Omit<AccountFieldsFragment, 'market' | 'asset'> & {
|
||||
};
|
||||
|
||||
const update = (
|
||||
data: AccountFieldsFragment[],
|
||||
data: AccountFieldsFragment[] | null,
|
||||
deltas: AccountEventsSubscription['accounts']
|
||||
) => {
|
||||
return produce(data, (draft) => {
|
||||
return produce(data || [], (draft) => {
|
||||
deltas.forEach((delta) => {
|
||||
const id = getId(delta);
|
||||
const index = draft.findIndex((a) => getId(a) === id);
|
||||
@@ -73,15 +73,8 @@ const update = (
|
||||
});
|
||||
};
|
||||
|
||||
const getData = (
|
||||
responseData: AccountsQuery
|
||||
): AccountFieldsFragment[] | null => {
|
||||
return (
|
||||
removePaginationWrapper(responseData.party?.accountsConnection?.edges) ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const getData = (responseData: AccountsQuery | null): AccountFieldsFragment[] =>
|
||||
removePaginationWrapper(responseData?.party?.accountsConnection?.edges) || [];
|
||||
const getDelta = (
|
||||
subscriptionData: AccountEventsSubscription
|
||||
): AccountEventsSubscription['accounts'] => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApolloError, InMemoryCacheConfig } from '@apollo/client';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import {
|
||||
ApolloClient,
|
||||
from,
|
||||
@@ -13,30 +13,49 @@ import { createClient as createWSClient } from 'graphql-ws';
|
||||
import { onError } from '@apollo/client/link/error';
|
||||
import { RetryLink } from '@apollo/client/link/retry';
|
||||
import ApolloLinkTimeout from 'apollo-link-timeout';
|
||||
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||
import { localLoggerFactory } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
const NOT_FOUND = 'NotFound';
|
||||
|
||||
export function createClient(base?: string, cacheConfig?: InMemoryCacheConfig) {
|
||||
if (!base) {
|
||||
throw new Error('Base must be passed into createClient!');
|
||||
export type ClientOptions = {
|
||||
url?: string;
|
||||
cacheConfig?: InMemoryCacheConfig;
|
||||
retry?: boolean;
|
||||
connectToDevTools?: boolean;
|
||||
};
|
||||
|
||||
export function createClient({
|
||||
url,
|
||||
cacheConfig,
|
||||
retry = true,
|
||||
connectToDevTools = true,
|
||||
}: ClientOptions) {
|
||||
if (!url) {
|
||||
throw new Error('url must be passed into createClient!');
|
||||
}
|
||||
const urlHTTP = new URL(base);
|
||||
const urlWS = new URL(base);
|
||||
const urlHTTP = new URL(url);
|
||||
const urlWS = new URL(url);
|
||||
// Replace http with ws, preserving if its a secure connection eg. https => wss
|
||||
urlWS.protocol = urlWS.protocol.replace('http', 'ws');
|
||||
|
||||
const noOpLink = new ApolloLink((operation, forward) => {
|
||||
return forward(operation);
|
||||
});
|
||||
|
||||
const timeoutLink = new ApolloLinkTimeout(10000);
|
||||
const enlargedTimeoutLink = new ApolloLinkTimeout(100000);
|
||||
const retryLink = new RetryLink({
|
||||
delay: {
|
||||
initial: 300,
|
||||
max: 10000,
|
||||
jitter: true,
|
||||
},
|
||||
});
|
||||
|
||||
const retryLink = retry
|
||||
? new RetryLink({
|
||||
delay: {
|
||||
initial: 300,
|
||||
max: 10000,
|
||||
jitter: true,
|
||||
},
|
||||
})
|
||||
: noOpLink;
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: urlHTTP.href,
|
||||
@@ -49,7 +68,7 @@ export function createClient(base?: string, cacheConfig?: InMemoryCacheConfig) {
|
||||
url: urlWS.href,
|
||||
})
|
||||
)
|
||||
: new ApolloLink((operation, forward) => forward(operation));
|
||||
: noOpLink;
|
||||
|
||||
const splitLink = isBrowser
|
||||
? split(
|
||||
@@ -87,23 +106,6 @@ export function createClient(base?: string, cacheConfig?: InMemoryCacheConfig) {
|
||||
return new ApolloClient({
|
||||
link: from([errorLink, composedTimeoutLink, retryLink, splitLink]),
|
||||
cache: new InMemoryCache(cacheConfig),
|
||||
connectToDevTools,
|
||||
});
|
||||
}
|
||||
|
||||
const isApolloGraphQLError = (
|
||||
error: ApolloError | Error | undefined
|
||||
): error is ApolloError => {
|
||||
return !!error && !!(error as ApolloError).graphQLErrors;
|
||||
};
|
||||
|
||||
const hasNotFoundGraphQLErrors = (errors: GraphQLErrors) => {
|
||||
return errors.some((e) => e.extensions && e.extensions['type'] === NOT_FOUND);
|
||||
};
|
||||
|
||||
export const isNotFoundGraphQLError = (
|
||||
error: Error | ApolloError | undefined
|
||||
) => {
|
||||
return (
|
||||
isApolloGraphQLError(error) && hasNotFoundGraphQLErrors(error.graphQLErrors)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,20 +6,15 @@ import { AssetDocument } from './__generated__/Asset';
|
||||
|
||||
export type Asset = AssetFieldsFragment;
|
||||
|
||||
const getData = (responseData: AssetQuery) => {
|
||||
const foundAssets = responseData.assetsConnection?.edges
|
||||
const getData = (responseData: AssetQuery | null) => {
|
||||
const foundAssets = responseData?.assetsConnection?.edges
|
||||
?.filter((e) => Boolean(e?.node))
|
||||
.map((e) => e?.node as Asset);
|
||||
if (foundAssets && foundAssets?.length > 0) return foundAssets[0];
|
||||
return null;
|
||||
};
|
||||
|
||||
export const assetProvider = makeDataProvider<
|
||||
AssetQuery,
|
||||
Asset | null,
|
||||
never,
|
||||
never
|
||||
>({
|
||||
export const assetProvider = makeDataProvider<AssetQuery, Asset, never, never>({
|
||||
query: AssetDocument,
|
||||
getData,
|
||||
});
|
||||
|
||||
@@ -16,14 +16,14 @@ export type BuiltinAsset = Omit<Asset, 'source'> & {
|
||||
source: BuiltinAssetSource;
|
||||
};
|
||||
|
||||
const getData = (responseData: AssetsQuery) =>
|
||||
responseData.assetsConnection?.edges
|
||||
const getData = (responseData: AssetsQuery | null) =>
|
||||
responseData?.assetsConnection?.edges
|
||||
?.filter((e) => Boolean(e?.node))
|
||||
.map((e) => e?.node as Asset) ?? [];
|
||||
|
||||
export const assetsProvider = makeDataProvider<
|
||||
AssetsQuery,
|
||||
Asset[] | null,
|
||||
Asset[],
|
||||
never,
|
||||
never
|
||||
>({
|
||||
|
||||
@@ -48,7 +48,7 @@ function createNewMarketProposal(): ProposalSubmissionBody {
|
||||
signers: [
|
||||
{
|
||||
pubKey: {
|
||||
key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
|
||||
key: '70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -74,6 +74,7 @@ export const DealTicket = ({ market, submit }: DealTicketProps) => {
|
||||
|
||||
usePersistedOrderStoreSubscription(market.id, (storedOrder) => {
|
||||
if (order.price !== storedOrder.price) {
|
||||
clearErrors('price');
|
||||
setValue('price', storedOrder.price);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import {
|
||||
getEvents,
|
||||
makeDataProvider,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
DepositsDocument,
|
||||
DepositEventDocument,
|
||||
} from './__generated__/Deposit';
|
||||
import type {
|
||||
DepositFieldsFragment,
|
||||
DepositsQuery,
|
||||
DepositEventSubscription,
|
||||
DepositEventSubscriptionVariables,
|
||||
} from './__generated__/Deposit';
|
||||
|
||||
export const depositsProvider = makeDataProvider<
|
||||
DepositsQuery,
|
||||
DepositFieldsFragment[],
|
||||
DepositEventSubscription,
|
||||
DepositEventSubscription,
|
||||
DepositEventSubscriptionVariables
|
||||
>({
|
||||
query: DepositsDocument,
|
||||
subscriptionQuery: DepositEventDocument,
|
||||
getData: (data: DepositsQuery | null) =>
|
||||
orderBy(
|
||||
removePaginationWrapper(data?.party?.depositsConnection?.edges || []),
|
||||
['createdTimestamp'],
|
||||
['desc']
|
||||
),
|
||||
getDelta: (data: DepositEventSubscription) => data,
|
||||
update: (
|
||||
data: DepositFieldsFragment[] | null,
|
||||
delta: DepositEventSubscription
|
||||
) => {
|
||||
if (!delta.busEvents?.length) {
|
||||
return data;
|
||||
}
|
||||
const incoming = getEvents<DepositFieldsFragment>(
|
||||
Schema.BusEventType.Deposit,
|
||||
delta.busEvents
|
||||
);
|
||||
return uniqBy([...incoming, ...(data || [])], 'id');
|
||||
},
|
||||
});
|
||||
@@ -5,7 +5,7 @@ export * from './deposit-limits';
|
||||
export * from './deposit-manager';
|
||||
export * from './deposits-table';
|
||||
export * from './use-deposit-balances';
|
||||
export * from './use-deposits';
|
||||
export * from './deposits-provider';
|
||||
export * from './use-get-allowance';
|
||||
export * from './use-get-balance-of-erc20-token';
|
||||
export * from './use-get-deposit-maximum';
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { getNodes, getEvents } from '@vegaprotocol/react-helpers';
|
||||
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
useDepositsQuery,
|
||||
DepositEventDocument,
|
||||
} from './__generated__/Deposit';
|
||||
import type {
|
||||
DepositFieldsFragment,
|
||||
DepositsQuery,
|
||||
DepositEventSubscription,
|
||||
DepositEventSubscriptionVariables,
|
||||
} from './__generated__/Deposit';
|
||||
|
||||
export const useDeposits = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data, loading, error, subscribeToMore } = useDepositsQuery({
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const deposits = useMemo(() => {
|
||||
if (!data?.party?.depositsConnection?.edges?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return orderBy(
|
||||
getNodes<DepositFieldsFragment>(data.party?.depositsConnection),
|
||||
['createdTimestamp'],
|
||||
['desc']
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pubKey) return;
|
||||
|
||||
const unsub = subscribeToMore<
|
||||
DepositEventSubscription,
|
||||
DepositEventSubscriptionVariables
|
||||
>({
|
||||
document: DepositEventDocument,
|
||||
variables: { partyId: pubKey },
|
||||
updateQuery,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [pubKey, subscribeToMore]);
|
||||
|
||||
return { data, loading, error, deposits };
|
||||
};
|
||||
|
||||
const updateQuery: UpdateQueryFn<
|
||||
DepositsQuery,
|
||||
DepositEventSubscriptionVariables,
|
||||
DepositEventSubscription
|
||||
> = (prev, { subscriptionData, variables }) => {
|
||||
if (!subscriptionData.data.busEvents?.length || !variables?.partyId) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const curr = getNodes<DepositFieldsFragment>(prev.party?.depositsConnection);
|
||||
const incoming = getEvents<DepositFieldsFragment>(
|
||||
Schema.BusEventType.Deposit,
|
||||
subscriptionData.data.busEvents
|
||||
);
|
||||
|
||||
const deposits = uniqBy([...incoming, ...curr], 'id');
|
||||
|
||||
if (!prev.party) {
|
||||
return {
|
||||
...prev,
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: variables?.partyId,
|
||||
depositsConnection: {
|
||||
__typename: 'DepositsConnection',
|
||||
edges: deposits.map((d) => ({ __typename: 'DepositEdge', node: d })),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
party: {
|
||||
...prev.party,
|
||||
id: variables?.partyId,
|
||||
depositsConnection: {
|
||||
__typename: 'DepositsConnection',
|
||||
edges: deposits.map((d) => ({ __typename: 'DepositEdge', node: d })),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -46,7 +46,10 @@ describe('Network loader', () => {
|
||||
render(
|
||||
<NetworkLoader skeleton={SKELETON_TEXT}>{SUCCESS_TEXT}</NetworkLoader>
|
||||
);
|
||||
expect(createClient).toHaveBeenCalledWith('http://vega.node', undefined);
|
||||
expect(createClient).toHaveBeenCalledWith({
|
||||
url: 'http://vega.node',
|
||||
cacheConfig: undefined,
|
||||
});
|
||||
expect(await screen.findByText(SUCCESS_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,10 @@ export function NetworkLoader({
|
||||
|
||||
const client = useMemo(() => {
|
||||
if (VEGA_URL) {
|
||||
return createClient(VEGA_URL, cache);
|
||||
return createClient({
|
||||
url: VEGA_URL,
|
||||
cacheConfig: cache,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}, [VEGA_URL, cache]);
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './use-environment';
|
||||
export * from './use-links';
|
||||
export * from './use-node-health';
|
||||
|
||||
@@ -18,6 +18,9 @@ type UseConfigOptions = {
|
||||
defaultConfig?: Configuration;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch list of hosts from the VEGA_CONFIG_URL
|
||||
*/
|
||||
export const useConfig = (
|
||||
{ environment, defaultConfig }: UseConfigOptions,
|
||||
onError: (errorType: ErrorType) => void
|
||||
|
||||
@@ -101,6 +101,16 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe('throws error', () => {
|
||||
const consoleError = console.error;
|
||||
|
||||
beforeAll(() => {
|
||||
console.error = jest.fn();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore: typescript doesn't recognize the mock implementation
|
||||
global.fetch.mockImplementation(setupFetch());
|
||||
@@ -127,6 +137,7 @@ describe('throws error', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => jest.resetModules()); // clears the cache of the modules
|
||||
|
||||
it('throws a validation error when NX_ETHERSCAN_URL is not a valid url', () => {
|
||||
process.env['NX_ETHERSCAN_URL'] = 'invalid-url';
|
||||
const result = () =>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// having the node switcher dialog in the environment provider breaks the test renderer
|
||||
// workaround based on: https://github.com/facebook/react/issues/11565
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import type { ClientOptions } from '@vegaprotocol/apollo-client';
|
||||
import { createClient } from '@vegaprotocol/apollo-client';
|
||||
import { useEnvironment, EnvironmentProvider } from './use-environment';
|
||||
import { Networks, ErrorType } from '../types';
|
||||
@@ -32,6 +33,7 @@ const mockEnvironmentState = {
|
||||
VEGA_ENV: Networks.TESTNET,
|
||||
VEGA_CONFIG_URL: 'https://vega.xyz/testnet-config.json',
|
||||
VEGA_NETWORKS: {
|
||||
DEVNET: 'https://devnet.url',
|
||||
TESTNET: 'https://testnet.url',
|
||||
STAGNET3: 'https://stagnet3.url',
|
||||
MAINNET: 'https://mainnet.url',
|
||||
@@ -42,6 +44,10 @@ const mockEnvironmentState = {
|
||||
GIT_ORIGIN_URL: 'https://github.com/test/repo',
|
||||
GIT_COMMIT_HASH: 'abcde01234',
|
||||
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
||||
MAINTENANCE_PAGE: false,
|
||||
configLoading: false,
|
||||
blockDifference: 0,
|
||||
nodeSwitcherOpen: false,
|
||||
setNodeSwitcherOpen: noop,
|
||||
networkError: undefined,
|
||||
};
|
||||
@@ -96,7 +102,7 @@ const getQuickestNode = (mockNodes: Record<string, MockRequestConfig>) => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore: typescript doesn't recognize the mock implementation
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(setupFetch());
|
||||
|
||||
window.localStorage.clear();
|
||||
@@ -129,6 +135,7 @@ describe('useEnvironment hook', () => {
|
||||
const { result } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
@@ -136,39 +143,67 @@ describe('useEnvironment hook', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('allows for the VEGA_CONFIG_URL to be missing when there is a VEGA_URL present', async () => {
|
||||
delete process.env['NX_VEGA_CONFIG_URL'];
|
||||
const { result } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_CONFIG_URL: undefined,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows for the VEGA_NETWORKS to be missing from the environment', async () => {
|
||||
act(async () => {
|
||||
delete process.env['NX_VEGA_NETWORKS'];
|
||||
it('allows for the VEGA_CONFIG_URL to be missing when there is a VEGA_URL present', async () => {
|
||||
delete process.env['NX_VEGA_CONFIG_URL'];
|
||||
const { result } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_NETWORKS: {},
|
||||
VEGA_CONFIG_URL: undefined,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('when VEGA_NETWORKS is not a valid json, prints a warning and continues without using the value from it', async () => {
|
||||
act(async () => {
|
||||
it('allows for the VEGA_NETWORKS to be missing from the environment', async () => {
|
||||
delete process.env['NX_VEGA_NETWORKS'];
|
||||
const { result } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_NETWORKS: {
|
||||
TESTNET: window.location.origin,
|
||||
},
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('throws a validation error when NX_VEGA_ENV is not found in the environment', async () => {
|
||||
delete process.env['NX_VEGA_ENV'];
|
||||
const consoleError = console.error;
|
||||
console.error = noop;
|
||||
expect(() => {
|
||||
renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
}).toThrowError(
|
||||
`NX_VEGA_ENV is invalid, received "undefined" instead of: 'CUSTOM' | 'SANDBOX' | 'TESTNET' | 'STAGNET1' | 'STAGNET3' | 'DEVNET' | 'MAINNET' | 'MIRROR'`
|
||||
);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
it('throws a validation error when VEGA_ENV is not a valid network', async () => {
|
||||
process.env['NX_VEGA_ENV'] = 'SOMETHING';
|
||||
const consoleError = console.error;
|
||||
console.error = noop;
|
||||
expect(() => {
|
||||
renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
}).toThrowError(
|
||||
`NX_VEGA_ENV is invalid, received "SOMETHING" instead of: CUSTOM | SANDBOX | TESTNET | STAGNET1 | STAGNET3 | DEVNET | MAINNET | MIRROR`
|
||||
);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
it('when VEGA_NETWORKS is not a valid json, prints a warning and continues without using the value from it', async () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
||||
process.env['NX_VEGA_NETWORKS'] = '{not:{valid:json';
|
||||
const { result } = renderHook(() => useEnvironment(), {
|
||||
@@ -177,26 +212,56 @@ it('when VEGA_NETWORKS is not a valid json, prints a warning and continues witho
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_NETWORKS: {},
|
||||
VEGA_NETWORKS: {
|
||||
TESTNET: window.location.origin,
|
||||
},
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
env | etherscanUrl | providerUrl
|
||||
${Networks.DEVNET} | ${'https://sepolia.etherscan.io'} | ${'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
|
||||
${Networks.TESTNET} | ${'https://sepolia.etherscan.io'} | ${'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
|
||||
${Networks.STAGNET3} | ${'https://sepolia.etherscan.io'} | ${'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
|
||||
${Networks.MAINNET} | ${'https://etherscan.io'} | ${'https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
|
||||
`(
|
||||
'uses correct default ethereum connection variables in $env',
|
||||
async ({ env, etherscanUrl, providerUrl }) => {
|
||||
act(async () => {
|
||||
it('throws a validation error when VEGA_NETWORKS has an invalid network as a key', async () => {
|
||||
process.env['NX_VEGA_NETWORKS'] = JSON.stringify({
|
||||
NOT_A_NETWORK: 'https://somewhere.url',
|
||||
});
|
||||
const consoleError = console.error;
|
||||
console.error = noop;
|
||||
expect(() => {
|
||||
renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
}).toThrowError(
|
||||
`All keys in NX_VEGA_NETWORKS must represent a valid environment: CUSTOM | SANDBOX | TESTNET | STAGNET1 | STAGNET3 | DEVNET | MAINNET | MIRROR`
|
||||
);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
it('throws a validation error when both VEGA_URL and VEGA_CONFIG_URL are missing in the environment', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
delete process.env['NX_VEGA_CONFIG_URL'];
|
||||
const consoleError = console.error;
|
||||
console.error = noop;
|
||||
expect(() => {
|
||||
renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
}).toThrowError(
|
||||
`Must provide either NX_VEGA_CONFIG_URL or NX_VEGA_URL in the environment.`
|
||||
);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
it.each`
|
||||
env | etherscanUrl | providerUrl
|
||||
${Networks.DEVNET} | ${'https://sepolia.etherscan.io'} | ${'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
|
||||
${Networks.TESTNET} | ${'https://sepolia.etherscan.io'} | ${'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
|
||||
${Networks.STAGNET3} | ${'https://sepolia.etherscan.io'} | ${'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
|
||||
${Networks.MAINNET} | ${'https://etherscan.io'} | ${'https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
|
||||
`(
|
||||
'uses correct default ethereum connection variables in $env',
|
||||
async ({ env, etherscanUrl, providerUrl }) => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() => createMockClient({ network: env }));
|
||||
|
||||
@@ -215,13 +280,39 @@ it.each`
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
describe('node selection', () => {
|
||||
it('updates the VEGA_URL from the config when it is missing from the environment', async () => {
|
||||
act(async () => {
|
||||
it('throws a validation error when NX_ETHERSCAN_URL is not a valid url', async () => {
|
||||
process.env['NX_ETHERSCAN_URL'] = 'invalid-url';
|
||||
const consoleError = console.error;
|
||||
console.error = noop;
|
||||
expect(() => {
|
||||
renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
}).toThrowError(
|
||||
`The NX_ETHERSCAN_URL environment variable must be a valid url`
|
||||
);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
it('throws a validation error when NX_ETHEREUM_PROVIDER_URL is not a valid url', async () => {
|
||||
process.env['NX_ETHEREUM_PROVIDER_URL'] = 'invalid-url';
|
||||
const consoleError = console.error;
|
||||
console.error = noop;
|
||||
expect(() => {
|
||||
renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
}).toThrow(
|
||||
`The NX_ETHEREUM_PROVIDER_URL environment variable must be a valid url`
|
||||
);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
describe('node selection', () => {
|
||||
it('updates the VEGA_URL from the config when it is missing from the environment', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
const { result } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
@@ -235,10 +326,9 @@ describe('node selection', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the VEGA_URL with the quickest node to respond from the config urls', async () => {
|
||||
act(async () => {
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('updates the VEGA_URL with the quickest node to respond from the config urls', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
const mockNodes: Record<string, MockRequestConfig> = {
|
||||
@@ -248,13 +338,14 @@ describe('node selection', () => {
|
||||
'https://mock-node-4.com': { hasError: false, delay: 0 },
|
||||
};
|
||||
|
||||
// @ts-ignore: typescript doesn't recognize the mock implementation
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(
|
||||
setupFetch(mockEnvironmentState.VEGA_CONFIG_URL, Object.keys(mockNodes))
|
||||
);
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation((url: keyof typeof mockNodes) => {
|
||||
return createMockClient({ statistics: mockNodes[url] });
|
||||
createClient.mockImplementation((cfg: ClientOptions) => {
|
||||
// eslint-disable-next-line
|
||||
return createMockClient({ statistics: mockNodes[cfg.url!] });
|
||||
});
|
||||
|
||||
const nodeUrl = getQuickestNode(mockNodes);
|
||||
@@ -271,10 +362,8 @@ describe('node selection', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores failing nodes and selects the first successful one to use', async () => {
|
||||
act(async () => {
|
||||
it('ignores failing nodes and selects the first successful one to use', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
const mockNodes: Record<string, MockRequestConfig> = {
|
||||
@@ -284,13 +373,14 @@ describe('node selection', () => {
|
||||
'https://mock-node-4.com': { hasError: true, delay: 0 },
|
||||
};
|
||||
|
||||
// @ts-ignore: typescript doesn't recognize the mock implementation
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(
|
||||
setupFetch(mockEnvironmentState.VEGA_CONFIG_URL, Object.keys(mockNodes))
|
||||
);
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation((url: keyof typeof mockNodes) => {
|
||||
return createMockClient({ statistics: mockNodes[url] });
|
||||
createClient.mockImplementation((cfg: ClientOptions) => {
|
||||
// eslint-disable-next-line
|
||||
return createMockClient({ statistics: mockNodes[cfg.url!] });
|
||||
});
|
||||
|
||||
const nodeUrl = getQuickestNode(mockNodes);
|
||||
@@ -307,10 +397,8 @@ describe('node selection', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has a network error when cannot connect to any nodes', async () => {
|
||||
act(async () => {
|
||||
it('has a network error when cannot connect to any nodes', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
const mockNodes: Record<string, MockRequestConfig> = {
|
||||
@@ -320,13 +408,14 @@ describe('node selection', () => {
|
||||
'https://mock-node-4.com': { hasError: true, delay: 0 },
|
||||
};
|
||||
|
||||
// @ts-ignore: typescript doesn't recognize the mock implementation
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(
|
||||
setupFetch(mockEnvironmentState.VEGA_CONFIG_URL, Object.keys(mockNodes))
|
||||
);
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation((url: keyof typeof mockNodes) => {
|
||||
return createMockClient({ statistics: mockNodes[url] });
|
||||
createClient.mockImplementation((cfg: ClientOptions) => {
|
||||
// eslint-disable-next-line
|
||||
return createMockClient({ statistics: mockNodes[cfg.url!] });
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useEnvironment(), {
|
||||
@@ -338,17 +427,16 @@ describe('node selection', () => {
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: undefined,
|
||||
networkError: ErrorType.CONNECTION_ERROR_ALL,
|
||||
nodeSwitcherOpen: true,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has a network error when it cannot fetch the network config and there is no VEGA_URL in the environment', async () => {
|
||||
act(async () => {
|
||||
it('has a network error when it cannot fetch the network config and there is no VEGA_URL in the environment', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
// @ts-ignore: typescript doesn't recognize the mock implementation
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(() => {
|
||||
throw new Error('Cannot fetch');
|
||||
});
|
||||
@@ -362,19 +450,18 @@ describe('node selection', () => {
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: undefined,
|
||||
networkError: ErrorType.CONFIG_LOAD_ERROR,
|
||||
nodeSwitcherOpen: true,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs an error when it cannot fetch the network config and there is a VEGA_URL in the environment', async () => {
|
||||
act(async () => {
|
||||
it('logs an error when it cannot fetch the network config and there is a VEGA_URL in the environment', async () => {
|
||||
const consoleWarnSpy = jest
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(noop);
|
||||
|
||||
// @ts-ignore: typescript doesn't recognize the mock implementation
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(() => {
|
||||
throw new Error('Cannot fetch');
|
||||
});
|
||||
@@ -386,6 +473,7 @@ describe('node selection', () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
nodeSwitcherOpen: false,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
@@ -396,15 +484,13 @@ describe('node selection', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('has a network error when the config is invalid and there is no VEGA_URL in the environment', async () => {
|
||||
act(async () => {
|
||||
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('has a network error when the config is invalid and there is no VEGA_URL in the environment', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
// @ts-ignore: typescript doesn't recognize the mock implementation
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -421,21 +507,20 @@ describe('node selection', () => {
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: undefined,
|
||||
networkError: ErrorType.CONFIG_VALIDATION_ERROR,
|
||||
nodeSwitcherOpen: true,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('logs an error when the network config in invalid and there is a VEGA_URL in the environment', async () => {
|
||||
act(async () => {
|
||||
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('logs an error when the network config is invalid and there is a VEGA_URL in the environment', async () => {
|
||||
const consoleWarnSpy = jest
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(noop);
|
||||
|
||||
// @ts-ignore: typescript doesn't recognize the mock implementation
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -447,6 +532,8 @@ describe('node selection', () => {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.configLoading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
@@ -460,12 +547,8 @@ describe('node selection', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('has a network error when the selected node is not a valid url', async () => {
|
||||
act(async () => {
|
||||
it('has a network error when the selected node is not a valid url', async () => {
|
||||
process.env['NX_VEGA_URL'] = 'not-url';
|
||||
|
||||
const { result } = renderHook(() => useEnvironment(), {
|
||||
@@ -475,15 +558,15 @@ describe('node selection', () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: 'not-url',
|
||||
nodeSwitcherOpen: true,
|
||||
networkError: ErrorType.INVALID_URL,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has a network error when cannot connect to the selected node', async () => {
|
||||
act(async () => {
|
||||
it('has a network error when cannot connect to the selected node', async () => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() => {
|
||||
return createMockClient({ statistics: { hasError: true } });
|
||||
@@ -496,15 +579,14 @@ describe('node selection', () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
nodeSwitcherOpen: true,
|
||||
networkError: ErrorType.CONNECTION_ERROR,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has a network error when the selected node has not subscription available', async () => {
|
||||
act(async () => {
|
||||
it('has a network error when the selected node has no subscription available', async () => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() => {
|
||||
return createMockClient({ busEvents: { hasError: true } });
|
||||
@@ -519,6 +601,7 @@ describe('node selection', () => {
|
||||
...mockEnvironmentState,
|
||||
networkError: ErrorType.SUBSCRIPTION_ERROR,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
nodeSwitcherOpen: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
NodeData,
|
||||
Configuration,
|
||||
} from '../types';
|
||||
import { useNodeHealth } from './use-node-health';
|
||||
|
||||
type EnvironmentProviderProps = {
|
||||
config?: Configuration;
|
||||
@@ -33,7 +34,10 @@ type EnvironmentProviderProps = {
|
||||
};
|
||||
|
||||
export type EnvironmentState = Environment & {
|
||||
configLoading: boolean;
|
||||
networkError?: ErrorType;
|
||||
blockDifference: number;
|
||||
nodeSwitcherOpen: boolean;
|
||||
setNodeSwitcherOpen: () => void;
|
||||
};
|
||||
|
||||
@@ -76,10 +80,14 @@ export const EnvironmentProvider = ({
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { state: nodes, clients } = useNodes(
|
||||
config,
|
||||
environment.MAINTENANCE_PAGE
|
||||
);
|
||||
|
||||
const blockDifference = useNodeHealth(clients, environment.VEGA_URL);
|
||||
|
||||
const nodeKeys = Object.keys(nodes);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -89,9 +97,10 @@ export const EnvironmentProvider = ({
|
||||
);
|
||||
if (successfulNodeKey && nodes[successfulNodeKey]) {
|
||||
Object.keys(clients).forEach((node) => clients[node]?.stop());
|
||||
const url = nodes[successfulNodeKey].url;
|
||||
updateEnvironment((prevEnvironment) => ({
|
||||
...prevEnvironment,
|
||||
VEGA_URL: nodes[successfulNodeKey].url,
|
||||
VEGA_URL: url,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -139,7 +148,10 @@ export const EnvironmentProvider = ({
|
||||
<EnvironmentContext.Provider
|
||||
value={{
|
||||
...environment,
|
||||
configLoading: loading,
|
||||
networkError,
|
||||
blockDifference,
|
||||
nodeSwitcherOpen: isNodeSwitcherOpen,
|
||||
setNodeSwitcherOpen: () => setNodeSwitcherOpen(true),
|
||||
}}
|
||||
>
|
||||
@@ -149,9 +161,9 @@ export const EnvironmentProvider = ({
|
||||
setDialogOpen={setNodeSwitcherOpen}
|
||||
loading={loading}
|
||||
config={config}
|
||||
onConnect={(url) =>
|
||||
updateEnvironment((env) => ({ ...env, VEGA_URL: url }))
|
||||
}
|
||||
onConnect={(url) => {
|
||||
updateEnvironment((env) => ({ ...env, VEGA_URL: url }));
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</EnvironmentContext.Provider>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import {
|
||||
useNodeHealth,
|
||||
NODE_SUBSET_COUNT,
|
||||
INTERVAL_TIME,
|
||||
} from './use-node-health';
|
||||
import type { createClient } from '@vegaprotocol/apollo-client';
|
||||
import type { ClientCollection } from './use-nodes';
|
||||
|
||||
function setup(...args: Parameters<typeof useNodeHealth>) {
|
||||
return renderHook(() => useNodeHealth(...args));
|
||||
}
|
||||
|
||||
function createMockClient(blockHeight: number) {
|
||||
return {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
statistics: {
|
||||
chainId: 'chain-id',
|
||||
blockHeight: blockHeight.toString(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as unknown as ReturnType<typeof createClient>;
|
||||
}
|
||||
|
||||
function createRejectingClient() {
|
||||
return {
|
||||
query: () => Promise.reject(new Error('request failed')),
|
||||
} as unknown as ReturnType<typeof createClient>;
|
||||
}
|
||||
|
||||
function createErroringClient() {
|
||||
return {
|
||||
query: () =>
|
||||
Promise.resolve({
|
||||
error: new Error('failed'),
|
||||
}),
|
||||
} as unknown as ReturnType<typeof createClient>;
|
||||
}
|
||||
|
||||
const CURRENT_URL = 'https://current.test.com';
|
||||
|
||||
describe('useNodeHealth', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('provides difference between the highest block and the current block', async () => {
|
||||
const highest = 100;
|
||||
const curr = 97;
|
||||
const clientCollection: ClientCollection = {
|
||||
[CURRENT_URL]: createMockClient(curr),
|
||||
'https://n02.test.com': createMockClient(98),
|
||||
'https://n03.test.com': createMockClient(highest),
|
||||
};
|
||||
const { result } = setup(clientCollection, CURRENT_URL);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
});
|
||||
expect(result.current).toBe(highest - curr);
|
||||
});
|
||||
|
||||
it('returns -1 if the current node query fails', async () => {
|
||||
const clientCollection: ClientCollection = {
|
||||
[CURRENT_URL]: createRejectingClient(),
|
||||
'https://n02.test.com': createMockClient(200),
|
||||
'https://n03.test.com': createMockClient(102),
|
||||
};
|
||||
const { result } = setup(clientCollection, CURRENT_URL);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
});
|
||||
expect(result.current).toBe(-1);
|
||||
});
|
||||
|
||||
it('returns -1 if the current node query returns an error', async () => {
|
||||
const clientCollection: ClientCollection = {
|
||||
[CURRENT_URL]: createErroringClient(),
|
||||
'https://n02.test.com': createMockClient(200),
|
||||
'https://n03.test.com': createMockClient(102),
|
||||
};
|
||||
const { result } = setup(clientCollection, CURRENT_URL);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
});
|
||||
expect(result.current).toBe(-1);
|
||||
});
|
||||
|
||||
it('queries against 5 random nodes along with the current url', async () => {
|
||||
const clientCollection: ClientCollection = new Array(20)
|
||||
.fill(null)
|
||||
.reduce((obj, x, i) => {
|
||||
obj[`https://n${i}.test.com`] = createMockClient(100);
|
||||
return obj;
|
||||
}, {} as ClientCollection);
|
||||
clientCollection[CURRENT_URL] = createMockClient(100);
|
||||
const spyOnCurrent = jest.spyOn(clientCollection[CURRENT_URL], 'query');
|
||||
|
||||
const { result } = setup(clientCollection, CURRENT_URL);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
Object.values(clientCollection).forEach((client) => {
|
||||
// @ts-ignore jest.fn() in client setup means mock will be present
|
||||
if (client?.query.mock.calls.length) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
expect(count).toBe(NODE_SUBSET_COUNT + 1);
|
||||
expect(spyOnCurrent).toHaveBeenCalledTimes(1);
|
||||
expect(result.current).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import compact from 'lodash/compact';
|
||||
import shuffle from 'lodash/shuffle';
|
||||
import type { createClient } from '@vegaprotocol/apollo-client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { StatisticsQuery } from '../utils/__generated__/Node';
|
||||
import { StatisticsDocument } from '../utils/__generated__/Node';
|
||||
import type { ClientCollection } from './use-nodes';
|
||||
|
||||
// How often to query other nodes
|
||||
export const INTERVAL_TIME = 30 * 1000;
|
||||
// Number of nodes to query against
|
||||
export const NODE_SUBSET_COUNT = 5;
|
||||
|
||||
// Queries all nodes from the environment provider via an interval
|
||||
// to calculate and return the difference between the most advanced block
|
||||
// and the block height of the current node
|
||||
export const useNodeHealth = (clients: ClientCollection, vegaUrl?: string) => {
|
||||
const [blockDiff, setBlockDiff] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clients || !vegaUrl) return;
|
||||
|
||||
const fetchBlockHeight = async (
|
||||
client?: ReturnType<typeof createClient>
|
||||
) => {
|
||||
try {
|
||||
const result = await client?.query<StatisticsQuery>({
|
||||
query: StatisticsDocument,
|
||||
fetchPolicy: 'no-cache', // always fetch and never cache
|
||||
});
|
||||
|
||||
if (!result) return null;
|
||||
if (result.error) return null;
|
||||
return result;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getBlockHeights = async () => {
|
||||
const nodes = Object.keys(clients).filter((key) => key !== vegaUrl);
|
||||
// make sure that your current vega url is always included
|
||||
// so we can compare later
|
||||
const testNodes = [vegaUrl, ...randomSubset(nodes, NODE_SUBSET_COUNT)];
|
||||
const result = await Promise.all(
|
||||
testNodes.map((node) => fetchBlockHeight(clients[node]))
|
||||
);
|
||||
const blockHeights: { [node: string]: number | null } = {};
|
||||
testNodes.forEach((node, i) => {
|
||||
const data = result[i];
|
||||
const blockHeight = data
|
||||
? Number(data?.data.statistics.blockHeight)
|
||||
: null;
|
||||
blockHeights[node] = blockHeight;
|
||||
});
|
||||
return blockHeights;
|
||||
};
|
||||
|
||||
// Every INTERVAL_TIME get block heights of a random subset
|
||||
// of nodes and determine if your current node is falling behind
|
||||
const interval = setInterval(async () => {
|
||||
const blockHeights = await getBlockHeights();
|
||||
const highestBlock = Math.max.apply(
|
||||
null,
|
||||
compact(Object.values(blockHeights))
|
||||
);
|
||||
const currNodeBlock = blockHeights[vegaUrl];
|
||||
|
||||
if (!currNodeBlock) {
|
||||
// Block height query failed and null was returned
|
||||
setBlockDiff(-1);
|
||||
} else {
|
||||
setBlockDiff(highestBlock - currNodeBlock);
|
||||
}
|
||||
}, INTERVAL_TIME);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [clients, vegaUrl]);
|
||||
|
||||
return blockDiff;
|
||||
};
|
||||
|
||||
const randomSubset = (arr: string[], size: number) => {
|
||||
const shuffled = shuffle(arr);
|
||||
return shuffled.slice(0, size);
|
||||
};
|
||||
@@ -74,7 +74,7 @@ const getInitialState = (config?: Configuration) =>
|
||||
{}
|
||||
);
|
||||
|
||||
type ClientCollection = Record<
|
||||
export type ClientCollection = Record<
|
||||
string,
|
||||
undefined | ReturnType<typeof createClient>
|
||||
>;
|
||||
@@ -178,6 +178,10 @@ const reducer = (state: Record<string, NodeData>, action: Action) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests each node to see if its suitable for connecting to and returns that data
|
||||
* as a map of node urls to an object of that data
|
||||
*/
|
||||
export const useNodes = (config?: Configuration, skip?: boolean) => {
|
||||
const [clients, setClients] = useState<ClientCollection>({});
|
||||
const [state, dispatch] = useReducer(reducer, getInitialState(config));
|
||||
|
||||
@@ -83,9 +83,7 @@ const getBundledEnvironmentValue = (key: EnvKey) => {
|
||||
case 'ETH_WALLET_MNEMONIC':
|
||||
return process.env['NX_ETH_WALLET_MNEMONIC'];
|
||||
case 'MAINTENANCE_PAGE':
|
||||
return (
|
||||
process.env['MAINTENANCE_PAGE'] || process.env['NX_MAINTENANCE_PAGE']
|
||||
);
|
||||
return process.env['NX_MAINTENANCE_PAGE'];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,7 +108,7 @@ export const compileEnvironment = (
|
||||
const environment = ENV_KEYS.reduce((acc, key) => {
|
||||
const value = getValue(key, definitions);
|
||||
|
||||
if (value) {
|
||||
if (value !== undefined && value !== null) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: value,
|
||||
|
||||
@@ -38,7 +38,11 @@ export const requestNode = (
|
||||
|
||||
let subscriptionSucceeded = false;
|
||||
|
||||
const client = createClient(url);
|
||||
const client = createClient({
|
||||
url,
|
||||
retry: false,
|
||||
connectToDevTools: false,
|
||||
});
|
||||
|
||||
// make a query for block height
|
||||
client
|
||||
|
||||
@@ -59,8 +59,8 @@ const update = (
|
||||
export type Trade = Omit<FillFieldsFragment, 'market'> & { market?: Market };
|
||||
export type TradeEdge = Edge<Trade>;
|
||||
|
||||
const getData = (responseData: FillsQuery): FillEdgeFragment[] =>
|
||||
responseData.party?.tradesConnection?.edges || [];
|
||||
const getData = (responseData: FillsQuery | null): FillEdgeFragment[] =>
|
||||
responseData?.party?.tradesConnection?.edges || [];
|
||||
|
||||
const getPageInfo = (responseData: FillsQuery): PageInfo | null =>
|
||||
responseData.party?.tradesConnection?.pageInfo || null;
|
||||
|
||||
@@ -5,8 +5,8 @@ import type {
|
||||
} from './__generated__/Proposals';
|
||||
import { ProposalsListDocument } from './__generated__/Proposals';
|
||||
|
||||
const getData = (responseData: ProposalsListQuery) =>
|
||||
responseData.proposalsConnection?.edges
|
||||
const getData = (responseData: ProposalsListQuery | null) =>
|
||||
responseData?.proposalsConnection?.edges
|
||||
?.filter((edge) => Boolean(edge?.node))
|
||||
.map((edge) => edge?.node as ProposalListFieldsFragment) || null;
|
||||
|
||||
|
||||
@@ -35,12 +35,12 @@ export type LedgerEntry = LedgerEntryFragment & {
|
||||
|
||||
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
|
||||
|
||||
const getData = (responseData: LedgerEntriesQuery) => {
|
||||
return responseData.ledgerEntries?.edges || [];
|
||||
const getData = (responseData: LedgerEntriesQuery | null) => {
|
||||
return responseData?.ledgerEntries?.edges || [];
|
||||
};
|
||||
|
||||
export const update = (
|
||||
data: ReturnType<typeof getData>,
|
||||
data: ReturnType<typeof getData> | null,
|
||||
delta: ReturnType<typeof getData>,
|
||||
reload: () => void,
|
||||
variables?: LedgerEntriesQueryVariables
|
||||
|
||||
@@ -35,10 +35,10 @@ export const liquidityProvisionsDataProvider = makeDataProvider<
|
||||
query: LiquidityProvisionsDocument,
|
||||
subscriptionQuery: LiquidityProvisionsUpdateDocument,
|
||||
update: (
|
||||
data: LiquidityProvisionFieldsFragment[],
|
||||
data: LiquidityProvisionFieldsFragment[] | null,
|
||||
deltas: LiquidityProvisionsUpdateSubscription['liquidityProvisions']
|
||||
) => {
|
||||
return produce(data, (draft) => {
|
||||
return produce(data || [], (draft) => {
|
||||
deltas?.forEach((delta) => {
|
||||
const id = getId(delta);
|
||||
const index = draft.findIndex((a) => getId(a) === id);
|
||||
@@ -63,9 +63,9 @@ export const liquidityProvisionsDataProvider = makeDataProvider<
|
||||
});
|
||||
});
|
||||
},
|
||||
getData: (responseData: LiquidityProvisionsQuery) => {
|
||||
getData: (responseData: LiquidityProvisionsQuery | null) => {
|
||||
return (
|
||||
responseData.market?.liquidityProvisionsConnection?.edges?.map(
|
||||
responseData?.market?.liquidityProvisionsConnection?.edges?.map(
|
||||
(e) => e?.node
|
||||
) ?? []
|
||||
).filter((e) => !!e) as LiquidityProvisionFieldsFragment[];
|
||||
@@ -105,7 +105,7 @@ export const marketLiquidityDataProvider = makeDataProvider<
|
||||
never
|
||||
>({
|
||||
query: MarketLpDocument,
|
||||
getData: (responseData: MarketLpQuery) => {
|
||||
getData: (responseData: MarketLpQuery | null) => {
|
||||
return responseData;
|
||||
},
|
||||
});
|
||||
@@ -119,10 +119,10 @@ export const liquidityFeeShareDataProvider = makeDataProvider<
|
||||
query: LiquidityProviderFeeShareDocument,
|
||||
subscriptionQuery: LiquidityProviderFeeShareUpdateDocument,
|
||||
update: (
|
||||
data: LiquidityProviderFeeShareFieldsFragment[],
|
||||
data: LiquidityProviderFeeShareFieldsFragment[] | null,
|
||||
deltas: LiquidityProviderFeeShareUpdateSubscription['marketsData'][0]['liquidityProviderFeeShare']
|
||||
) => {
|
||||
return produce(data, (draft) => {
|
||||
return produce(data || [], (draft) => {
|
||||
deltas?.forEach((delta) => {
|
||||
const id = delta.partyId;
|
||||
const index = draft.findIndex((a) => a.party.id === id);
|
||||
@@ -143,7 +143,7 @@ export const liquidityFeeShareDataProvider = makeDataProvider<
|
||||
});
|
||||
},
|
||||
getData: (data) => {
|
||||
return data.market?.data?.liquidityProviderFeeShare || [];
|
||||
return data?.market?.data?.liquidityProviderFeeShare || [];
|
||||
},
|
||||
getDelta: (subscriptionData: LiquidityProviderFeeShareUpdateSubscription) => {
|
||||
return subscriptionData.marketsData[0].liquidityProviderFeeShare;
|
||||
|
||||
@@ -58,7 +58,7 @@ export interface Markets {
|
||||
}
|
||||
|
||||
const getData = (
|
||||
responseData: LiquidityProvisionMarketsQuery
|
||||
responseData: LiquidityProvisionMarketsQuery | null
|
||||
): LiquidityProvisionMarket[] | null => {
|
||||
return (
|
||||
responseData?.marketsConnection?.edges.map((edge) => {
|
||||
|
||||
@@ -53,7 +53,7 @@ export const update: Update<
|
||||
return data;
|
||||
};
|
||||
|
||||
const getData = (responseData: MarketDepthQuery) => responseData.market;
|
||||
const getData = (responseData: MarketDepthQuery | null) => responseData?.market;
|
||||
|
||||
const getDelta = (subscriptionData: MarketDepthUpdateSubscription) =>
|
||||
subscriptionData.marketsDepthUpdate;
|
||||
|
||||
@@ -9,5 +9,5 @@ export const marketInfoDataProvider = makeDataProvider<
|
||||
never
|
||||
>({
|
||||
query: MarketInfoDocument,
|
||||
getData: (responseData: MarketInfoQuery) => responseData,
|
||||
getData: (responseData: MarketInfoQuery | null) => responseData,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export const update = (data: Candle[] | null, delta: Candle) => {
|
||||
return [delta];
|
||||
};
|
||||
|
||||
const getData = (responseData: MarketCandlesQuery): Candle[] | null =>
|
||||
const getData = (responseData: MarketCandlesQuery | null): Candle[] | null =>
|
||||
responseData?.marketsConnection?.edges[0]?.node.candlesConnection?.edges
|
||||
?.filter((edge) => edge?.node)
|
||||
.map((edge) => edge?.node as Candle) || null;
|
||||
|
||||
@@ -18,14 +18,20 @@ import type {
|
||||
|
||||
export type MarketData = MarketDataFieldsFragment;
|
||||
|
||||
const update = (data: MarketData, delta: MarketDataUpdateFieldsFragment) => {
|
||||
return produce(data, (draft) => {
|
||||
const { marketId, __typename, ...marketData } = delta;
|
||||
Object.assign(draft, marketData);
|
||||
});
|
||||
const update = (
|
||||
data: MarketData | null,
|
||||
delta: MarketDataUpdateFieldsFragment
|
||||
) => {
|
||||
return (
|
||||
data &&
|
||||
produce(data, (draft) => {
|
||||
const { marketId, __typename, ...marketData } = delta;
|
||||
Object.assign(draft, marketData);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getData = (responseData: MarketDataQuery): MarketData | null =>
|
||||
const getData = (responseData: MarketDataQuery | null): MarketData | null =>
|
||||
responseData?.marketsConnection?.edges[0].node.data || null;
|
||||
|
||||
const getDelta = (
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { MarketData } from './market-data-provider';
|
||||
import { marketDataProvider } from './market-data-provider';
|
||||
|
||||
const getData = (
|
||||
responseData: MarketQuery
|
||||
responseData: MarketQuery | null
|
||||
): SingleMarketFieldsFragment | null => responseData?.market || null;
|
||||
|
||||
export const marketProvider = makeDataProvider<
|
||||
|
||||
@@ -8,7 +8,9 @@ export interface MarketCandles {
|
||||
candles: Candle[] | undefined;
|
||||
}
|
||||
|
||||
const getData = (responseData: MarketsCandlesQuery): MarketCandles[] | null =>
|
||||
const getData = (
|
||||
responseData: MarketsCandlesQuery | null
|
||||
): MarketCandles[] | null =>
|
||||
responseData?.marketsConnection?.edges.map((edge) => ({
|
||||
marketId: edge.node.id,
|
||||
candles: edge.node.candlesConnection?.edges
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { MarketsDataQuery } from './__generated__/markets-data';
|
||||
import { MarketsDataDocument } from './__generated__/markets-data';
|
||||
import type { MarketData } from './market-data-provider';
|
||||
|
||||
const getData = (responseData: MarketsDataQuery): MarketData[] | null =>
|
||||
responseData.marketsConnection?.edges
|
||||
const getData = (responseData: MarketsDataQuery | null): MarketData[] | null =>
|
||||
responseData?.marketsConnection?.edges
|
||||
.filter((edge) => edge.node.data)
|
||||
.map((edge) => edge.node.data as MarketData) || null;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { Candle } from './market-candles-provider';
|
||||
|
||||
export type Market = MarketFieldsFragment;
|
||||
|
||||
const getData = (responseData: MarketsQuery): Market[] | null =>
|
||||
const getData = (responseData: MarketsQuery | null): Market[] | null =>
|
||||
responseData?.marketsConnection?.edges.map((edge) => edge.node) || null;
|
||||
|
||||
export const marketsProvider = makeDataProvider<
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('order data provider', () => {
|
||||
expect(updatedData && updatedData[1].node.updatedAt).toEqual(
|
||||
delta[4].updatedAt
|
||||
);
|
||||
expect(update([], delta, () => null, { partyId: '0x123' }).length).toEqual(
|
||||
expect(update([], delta, () => null, { partyId: '0x123' })?.length).toEqual(
|
||||
4
|
||||
);
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ const orderMatchFilters = (
|
||||
return true;
|
||||
};
|
||||
|
||||
const getData = (responseData: OrdersQuery) =>
|
||||
const getData = (responseData: OrdersQuery | null) =>
|
||||
responseData?.party?.ordersConnection?.edges || [];
|
||||
|
||||
const getDelta = (subscriptionData: OrdersUpdateSubscription) =>
|
||||
@@ -81,7 +81,7 @@ const getPageInfo = (responseData: OrdersQuery): PageInfo | null =>
|
||||
responseData.party?.ordersConnection?.pageInfo || null;
|
||||
|
||||
export const update = (
|
||||
data: ReturnType<typeof getData>,
|
||||
data: ReturnType<typeof getData> | null,
|
||||
delta: ReturnType<typeof getDelta>,
|
||||
reload: () => void,
|
||||
variables?: OrdersQueryVariables
|
||||
|
||||
@@ -1,64 +1,63 @@
|
||||
import produce from 'immer';
|
||||
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
makeDataProvider,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
MarginsSubscriptionDocument,
|
||||
MarginsDocument,
|
||||
} from './__generated__/Positions';
|
||||
import type {
|
||||
MarginsQuery,
|
||||
MarginFieldsFragment,
|
||||
MarginsSubscriptionSubscription,
|
||||
} from './__generated__/Positions';
|
||||
|
||||
const update = (
|
||||
data: MarginsQuery['party'],
|
||||
data: MarginFieldsFragment[] | null,
|
||||
delta: MarginsSubscriptionSubscription['margins']
|
||||
) => {
|
||||
return produce(data, (draft) => {
|
||||
return produce(data || [], (draft) => {
|
||||
const { marketId } = delta;
|
||||
if (marketId && draft?.marginsConnection?.edges) {
|
||||
const index = draft.marginsConnection.edges.findIndex(
|
||||
(edge) => edge.node.market.id === marketId
|
||||
);
|
||||
if (index !== -1) {
|
||||
const currNode = draft.marginsConnection.edges[index].node;
|
||||
draft.marginsConnection.edges[index].node = {
|
||||
...currNode,
|
||||
maintenanceLevel: delta.maintenanceLevel,
|
||||
searchLevel: delta.searchLevel,
|
||||
initialLevel: delta.initialLevel,
|
||||
collateralReleaseLevel: delta.collateralReleaseLevel,
|
||||
};
|
||||
} else {
|
||||
draft.marginsConnection.edges.unshift({
|
||||
__typename: 'MarginEdge',
|
||||
node: {
|
||||
__typename: 'MarginLevels',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: delta.marketId,
|
||||
},
|
||||
maintenanceLevel: delta.maintenanceLevel,
|
||||
searchLevel: delta.searchLevel,
|
||||
initialLevel: delta.initialLevel,
|
||||
collateralReleaseLevel: delta.collateralReleaseLevel,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: delta.asset,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const index = draft.findIndex((node) => node.market.id === marketId);
|
||||
if (index !== -1) {
|
||||
const currNode = draft[index];
|
||||
draft[index] = {
|
||||
...currNode,
|
||||
maintenanceLevel: delta.maintenanceLevel,
|
||||
searchLevel: delta.searchLevel,
|
||||
initialLevel: delta.initialLevel,
|
||||
collateralReleaseLevel: delta.collateralReleaseLevel,
|
||||
};
|
||||
} else {
|
||||
draft.unshift({
|
||||
__typename: 'MarginLevels',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: delta.marketId,
|
||||
},
|
||||
maintenanceLevel: delta.maintenanceLevel,
|
||||
searchLevel: delta.searchLevel,
|
||||
initialLevel: delta.initialLevel,
|
||||
collateralReleaseLevel: delta.collateralReleaseLevel,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: delta.asset,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getData = (responseData: MarginsQuery) => responseData.party;
|
||||
const getData = (responseData: MarginsQuery | null) =>
|
||||
removePaginationWrapper(responseData?.party?.marginsConnection?.edges) || [];
|
||||
|
||||
const getDelta = (subscriptionData: MarginsSubscriptionSubscription) =>
|
||||
subscriptionData.margins;
|
||||
|
||||
export const marginsDataProvider = makeDataProvider<
|
||||
MarginsQuery,
|
||||
MarginsQuery['party'],
|
||||
MarginFieldsFragment[],
|
||||
MarginsSubscriptionSubscription,
|
||||
MarginsSubscriptionSubscription['margins']
|
||||
>({
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import type { MarketWithData } from '@vegaprotocol/market-list';
|
||||
import type { PositionsQuery, MarginsQuery } from './__generated__/Positions';
|
||||
import type {
|
||||
PositionFieldsFragment,
|
||||
MarginFieldsFragment,
|
||||
} from './__generated__/Positions';
|
||||
import { getMetrics, rejoinPositionData } from './positions-data-providers';
|
||||
|
||||
const accounts = [
|
||||
@@ -63,47 +66,32 @@ const accounts = [
|
||||
},
|
||||
] as Account[];
|
||||
|
||||
const positions: PositionsQuery = {
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
|
||||
positionsConnection: {
|
||||
__typename: 'PositionConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PositionEdge',
|
||||
node: {
|
||||
__typename: 'Position',
|
||||
openVolume: '100',
|
||||
averageEntryPrice: '8993727',
|
||||
updatedAt: '2022-07-28T14:53:54.725477Z',
|
||||
realisedPNL: '0',
|
||||
unrealisedPNL: '43804770',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'PositionEdge',
|
||||
node: {
|
||||
__typename: 'Position',
|
||||
openVolume: '-100',
|
||||
realisedPNL: '0',
|
||||
unrealisedPNL: '-9112700',
|
||||
averageEntryPrice: '840158',
|
||||
updatedAt: '2022-07-28T15:09:34.441143Z',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
const positions: PositionFieldsFragment[] = [
|
||||
{
|
||||
__typename: 'Position',
|
||||
openVolume: '100',
|
||||
averageEntryPrice: '8993727',
|
||||
updatedAt: '2022-07-28T14:53:54.725477Z',
|
||||
realisedPNL: '0',
|
||||
unrealisedPNL: '43804770',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
||||
},
|
||||
},
|
||||
};
|
||||
{
|
||||
__typename: 'Position',
|
||||
openVolume: '-100',
|
||||
realisedPNL: '0',
|
||||
unrealisedPNL: '-9112700',
|
||||
averageEntryPrice: '840158',
|
||||
updatedAt: '2022-07-28T15:09:34.441143Z',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const marketsData = [
|
||||
{
|
||||
@@ -162,60 +150,44 @@ const marketsData = [
|
||||
},
|
||||
] as MarketWithData[];
|
||||
|
||||
const margins: MarginsQuery = {
|
||||
party: {
|
||||
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
|
||||
marginsConnection: {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'MarginEdge',
|
||||
node: {
|
||||
__typename: 'MarginLevels',
|
||||
maintenanceLevel: '0',
|
||||
searchLevel: '0',
|
||||
initialLevel: '0',
|
||||
collateralReleaseLevel: '0',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'tDAI-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'MarginEdge',
|
||||
node: {
|
||||
__typename: 'MarginLevels',
|
||||
maintenanceLevel: '0',
|
||||
searchLevel: '0',
|
||||
initialLevel: '0',
|
||||
collateralReleaseLevel: '0',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'tDAI-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
__typename: 'MarginConnection',
|
||||
const margins: MarginFieldsFragment[] = [
|
||||
{
|
||||
__typename: 'MarginLevels',
|
||||
maintenanceLevel: '0',
|
||||
searchLevel: '0',
|
||||
initialLevel: '0',
|
||||
collateralReleaseLevel: '0',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'tDAI-id',
|
||||
},
|
||||
__typename: 'Party',
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
__typename: 'MarginLevels',
|
||||
maintenanceLevel: '0',
|
||||
searchLevel: '0',
|
||||
initialLevel: '0',
|
||||
collateralReleaseLevel: '0',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'tDAI-id',
|
||||
},
|
||||
},
|
||||
];
|
||||
describe('getMetrics && rejoinPositionData', () => {
|
||||
it('returns positions metrics', () => {
|
||||
const positionsRejoined = rejoinPositionData(
|
||||
positions.party,
|
||||
positions,
|
||||
marketsData,
|
||||
margins.party
|
||||
margins
|
||||
);
|
||||
const metrics = getMetrics(positionsRejoined, accounts || null);
|
||||
expect(metrics.length).toEqual(2);
|
||||
@@ -223,9 +195,9 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
|
||||
it('calculates metrics', () => {
|
||||
const positionsRejoined = rejoinPositionData(
|
||||
positions.party,
|
||||
positions,
|
||||
marketsData,
|
||||
margins.party
|
||||
margins
|
||||
);
|
||||
const metrics = getMetrics(positionsRejoined, accounts || null);
|
||||
|
||||
|
||||
@@ -4,18 +4,19 @@ import BigNumber from 'bignumber.js';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { accountsDataProvider } from '@vegaprotocol/accounts';
|
||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
toBigNum,
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { MarketWithData } from '@vegaprotocol/market-list';
|
||||
import { marketsWithDataProvider } from '@vegaprotocol/market-list';
|
||||
import type {
|
||||
PositionsQuery,
|
||||
PositionFieldsFragment,
|
||||
PositionsSubscriptionSubscription,
|
||||
MarginsQuery,
|
||||
MarginFieldsFragment,
|
||||
} from './__generated__/Positions';
|
||||
import {
|
||||
@@ -170,20 +171,17 @@ export const getMetrics = (
|
||||
};
|
||||
|
||||
export const update = (
|
||||
data: PositionsQuery['party'],
|
||||
data: PositionFieldsFragment[] | null,
|
||||
deltas: PositionsSubscriptionSubscription['positions']
|
||||
) => {
|
||||
return produce(data, (draft) => {
|
||||
return produce(data || [], (draft) => {
|
||||
deltas.forEach((delta) => {
|
||||
if (!draft?.positionsConnection?.edges || !delta) {
|
||||
return;
|
||||
}
|
||||
const index = draft.positionsConnection.edges.findIndex(
|
||||
(edge) => edge.node.market.id === delta.marketId
|
||||
const index = draft.findIndex(
|
||||
(node) => node.market.id === delta.marketId
|
||||
);
|
||||
if (index !== -1) {
|
||||
const currNode = draft.positionsConnection.edges[index].node;
|
||||
draft.positionsConnection.edges[index].node = {
|
||||
const currNode = draft[index];
|
||||
draft[index] = {
|
||||
...currNode,
|
||||
realisedPNL: delta.realisedPNL,
|
||||
unrealisedPNL: delta.unrealisedPNL,
|
||||
@@ -192,15 +190,12 @@ export const update = (
|
||||
updatedAt: delta.updatedAt,
|
||||
};
|
||||
} else {
|
||||
draft.positionsConnection.edges.unshift({
|
||||
__typename: 'PositionEdge',
|
||||
node: {
|
||||
...delta,
|
||||
__typename: 'Position',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: delta.marketId,
|
||||
},
|
||||
draft.unshift({
|
||||
...delta,
|
||||
__typename: 'Position',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: delta.marketId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -210,29 +205,29 @@ export const update = (
|
||||
|
||||
export const positionsDataProvider = makeDataProvider<
|
||||
PositionsQuery,
|
||||
PositionsQuery['party'],
|
||||
PositionFieldsFragment[],
|
||||
PositionsSubscriptionSubscription,
|
||||
PositionsSubscriptionSubscription['positions']
|
||||
>({
|
||||
query: PositionsDocument,
|
||||
subscriptionQuery: PositionsSubscriptionDocument,
|
||||
update,
|
||||
getData: (responseData: PositionsQuery) => responseData.party,
|
||||
getData: (responseData: PositionsQuery | null) =>
|
||||
removePaginationWrapper(responseData?.party?.positionsConnection?.edges) ||
|
||||
[],
|
||||
getDelta: (subscriptionData: PositionsSubscriptionSubscription) =>
|
||||
subscriptionData.positions,
|
||||
});
|
||||
|
||||
const upgradeMarginsConnection = (
|
||||
marketId: string,
|
||||
margins: MarginsQuery['party'] | null
|
||||
margins: MarginFieldsFragment[] | null
|
||||
) => {
|
||||
if (marketId && margins?.marginsConnection?.edges) {
|
||||
if (marketId && margins) {
|
||||
const index =
|
||||
margins.marginsConnection.edges.findIndex(
|
||||
(edge) => edge.node.market.id === marketId
|
||||
) ?? -1;
|
||||
margins.findIndex((node) => node.market.id === marketId) ?? -1;
|
||||
if (index >= 0) {
|
||||
const marginLevel = margins.marginsConnection.edges[index].node;
|
||||
const marginLevel = margins[index];
|
||||
return {
|
||||
maintenanceLevel: marginLevel.maintenanceLevel,
|
||||
searchLevel: marginLevel.searchLevel,
|
||||
@@ -244,12 +239,12 @@ const upgradeMarginsConnection = (
|
||||
};
|
||||
|
||||
export const rejoinPositionData = (
|
||||
positions: PositionsQuery['party'] | null,
|
||||
positions: PositionFieldsFragment[] | null,
|
||||
marketsData: MarketWithData[] | null,
|
||||
margins: MarginsQuery['party'] | null
|
||||
margins: MarginFieldsFragment[] | null
|
||||
): PositionRejoined[] | null => {
|
||||
if (positions?.positionsConnection?.edges && marketsData && margins) {
|
||||
return positions.positionsConnection.edges.map(({ node }) => {
|
||||
if (positions && marketsData && margins) {
|
||||
return positions.map((node) => {
|
||||
return {
|
||||
realisedPNL: node.realisedPNL,
|
||||
openVolume: node.openVolume,
|
||||
|
||||
@@ -172,3 +172,33 @@ it('displays realised and unrealised PNL', async () => {
|
||||
expect(cells[9].textContent).toEqual('1.23');
|
||||
expect(cells[10].textContent).toEqual('4.56');
|
||||
});
|
||||
|
||||
it('displays close button', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<PositionsTable
|
||||
rowData={singleRowData}
|
||||
onClose={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[12].textContent).toEqual('Close');
|
||||
});
|
||||
|
||||
it('do not display close button if openVolume is zero', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<PositionsTable
|
||||
rowData={[{ ...singleRow, openVolume: '0' }]}
|
||||
onClose={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[12].textContent).toEqual('');
|
||||
});
|
||||
|
||||
@@ -381,14 +381,16 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
{onClose ? (
|
||||
<AgGridColumn
|
||||
type="rightAligned"
|
||||
cellRenderer={({ data }: VegaICellRendererParams<Position>) => (
|
||||
<ButtonLink
|
||||
data-testid="close-position"
|
||||
onClick={() => data && onClose(data)}
|
||||
>
|
||||
{t('Close')}
|
||||
</ButtonLink>
|
||||
)}
|
||||
cellRenderer={({ data }: VegaICellRendererParams<Position>) =>
|
||||
data?.openVolume && data?.openVolume !== '0' ? (
|
||||
<ButtonLink
|
||||
data-testid="close-position"
|
||||
onClick={() => data && onClose(data)}
|
||||
>
|
||||
{t('Close')}
|
||||
</ButtonLink>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</AgGrid>
|
||||
|
||||
@@ -2,30 +2,17 @@ import { useCallback, useState } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { marginsDataProvider } from './margin-data-provider';
|
||||
import type {
|
||||
MarginsQuery,
|
||||
MarginsSubscriptionSubscription,
|
||||
} from './__generated__/Positions';
|
||||
|
||||
const getMarketMarginPosition = ({
|
||||
data,
|
||||
marketId,
|
||||
}: {
|
||||
data: MarginsQuery['party'] | null;
|
||||
marketId: string;
|
||||
}) => {
|
||||
const positions =
|
||||
data?.marginsConnection?.edges?.map((item) => item.node) ?? [];
|
||||
return positions.find((item) => item.market.id === marketId);
|
||||
};
|
||||
import type { MarginFieldsFragment } from './__generated__/Positions';
|
||||
|
||||
export const useMarketMargin = (marketId: string) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [marginLevel, setMarginLevel] = useState<string>('');
|
||||
|
||||
const update = useCallback(
|
||||
({ data }: { data: MarginsQuery['party'] | null }) => {
|
||||
const marginMarketPosition = getMarketMarginPosition({ data, marketId });
|
||||
({ data }: { data: MarginFieldsFragment[] | null }) => {
|
||||
const marginMarketPosition = data?.find(
|
||||
(item) => item.market.id === marketId
|
||||
);
|
||||
if (marginMarketPosition?.maintenanceLevel) {
|
||||
setMarginLevel(marginMarketPosition?.maintenanceLevel || '');
|
||||
}
|
||||
@@ -34,10 +21,7 @@ export const useMarketMargin = (marketId: string) => {
|
||||
[setMarginLevel, marketId]
|
||||
);
|
||||
|
||||
useDataProvider<
|
||||
MarginsQuery['party'],
|
||||
MarginsSubscriptionSubscription['margins']
|
||||
>({
|
||||
useDataProvider({
|
||||
dataProvider: marginsDataProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey || !marketId,
|
||||
|
||||
@@ -2,29 +2,14 @@ import { useCallback, useState } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { positionsDataProvider } from './positions-data-providers';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
PositionsQuery,
|
||||
PositionsSubscriptionSubscription,
|
||||
} from './__generated__/Positions';
|
||||
|
||||
const getMarketPosition = ({
|
||||
data,
|
||||
marketId,
|
||||
}: {
|
||||
data: PositionsQuery['party'];
|
||||
marketId: string;
|
||||
}) => {
|
||||
const positions =
|
||||
data?.positionsConnection?.edges?.map((item) => item.node) ?? [];
|
||||
return positions.find((item) => item.market.id === marketId);
|
||||
};
|
||||
import type { PositionFieldsFragment } from './__generated__/Positions';
|
||||
|
||||
export const useMarketPositionOpenVolume = (marketId: string) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [openVolume, setOpenVolume] = useState<string>('');
|
||||
const update = useCallback(
|
||||
({ data }: { data: PositionsQuery['party'] | undefined }) => {
|
||||
const position = getMarketPosition({ data, marketId });
|
||||
({ data }: { data: PositionFieldsFragment[] | null }) => {
|
||||
const position = data?.find((node) => node.market.id === marketId);
|
||||
if (position?.openVolume) {
|
||||
setOpenVolume(position?.openVolume || '');
|
||||
}
|
||||
@@ -33,10 +18,7 @@ export const useMarketPositionOpenVolume = (marketId: string) => {
|
||||
[setOpenVolume, marketId]
|
||||
);
|
||||
|
||||
useDataProvider<
|
||||
PositionsQuery['party'],
|
||||
PositionsSubscriptionSubscription['positions']
|
||||
>({
|
||||
useDataProvider({
|
||||
dataProvider: positionsDataProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey || !marketId,
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './use-data-provider';
|
||||
export * from './use-fetch';
|
||||
export * from './use-mutation-observer';
|
||||
export * from './use-network-params';
|
||||
export * from './use-navigator-online';
|
||||
export * from './use-outside-click';
|
||||
export * from './use-resize-observer';
|
||||
export * from './use-resize';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { act, fireEvent, renderHook } from '@testing-library/react';
|
||||
import { useNavigatorOnline } from './use-navigator-online';
|
||||
|
||||
const setup = () => {
|
||||
return renderHook(() => useNavigatorOnline());
|
||||
};
|
||||
|
||||
const turnOn = () => {
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(true);
|
||||
fireEvent(window, new Event('online'));
|
||||
};
|
||||
|
||||
const turnOff = () => {
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(false);
|
||||
fireEvent(window, new Event('offline'));
|
||||
};
|
||||
|
||||
describe('useNavigatorOnline', () => {
|
||||
it('returns true if connected and false if not', () => {
|
||||
const { result } = setup();
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
act(turnOff);
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
act(turnOn);
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
const subscribe = (onStoreChange: () => void) => {
|
||||
window.addEventListener('online', onStoreChange);
|
||||
window.addEventListener('offline', onStoreChange);
|
||||
return () => {
|
||||
window.removeEventListener('online', onStoreChange);
|
||||
window.removeEventListener('offline', onStoreChange);
|
||||
};
|
||||
};
|
||||
export const useNavigatorOnline = () =>
|
||||
useSyncExternalStore(
|
||||
subscribe,
|
||||
() => window.navigator.onLine,
|
||||
() => true
|
||||
);
|
||||
@@ -1,20 +1,2 @@
|
||||
export * from './hooks';
|
||||
export * from './lib/format';
|
||||
export * from './lib/generic-data-provider';
|
||||
export * from './lib/get-nodes';
|
||||
export * from './lib/get-events';
|
||||
export * from './lib/grid';
|
||||
export * from './lib/i18n';
|
||||
export * from './lib/pagination';
|
||||
export * from './lib/ag-grid-update';
|
||||
export * from './lib/remove-0x';
|
||||
export * from './lib/storage';
|
||||
export * from './lib/time';
|
||||
export * from './lib/validate';
|
||||
export * from './lib/links';
|
||||
export * from './lib/is-asset-erc20';
|
||||
export * from './lib/remove-pagination-wrapper';
|
||||
export * from './lib/__generated__/ChainId';
|
||||
export * from './lib/data-grid';
|
||||
export * from './lib/local-logger';
|
||||
export * from './lib/market-expires';
|
||||
export * from './lib';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ApolloError } from '@apollo/client';
|
||||
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||
|
||||
const NOT_FOUND = 'NotFound';
|
||||
|
||||
const isApolloGraphQLError = (
|
||||
error: ApolloError | Error | undefined
|
||||
): error is ApolloError => {
|
||||
return !!error && !!(error as ApolloError).graphQLErrors;
|
||||
};
|
||||
|
||||
const hasNotFoundGraphQLErrors = (errors: GraphQLErrors, path?: string) => {
|
||||
return errors.some(
|
||||
(e) =>
|
||||
e.extensions &&
|
||||
e.extensions['type'] === NOT_FOUND &&
|
||||
(!path || e.path?.[0] === path)
|
||||
);
|
||||
};
|
||||
|
||||
export const isNotFoundGraphQLError = (
|
||||
error: Error | ApolloError | undefined,
|
||||
path?: string
|
||||
) => {
|
||||
return (
|
||||
isApolloGraphQLError(error) &&
|
||||
hasNotFoundGraphQLErrors(error.graphQLErrors, path)
|
||||
);
|
||||
};
|
||||
@@ -62,7 +62,7 @@ const subscribe = makeDataProvider<QueryData, Data, SubscriptionData, Delta>({
|
||||
query,
|
||||
subscriptionQuery,
|
||||
update,
|
||||
getData: (r) => r.data,
|
||||
getData: (r) => r?.data || null,
|
||||
getDelta: (r) => r.data,
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ const paginatedSubscribe = makeDataProvider<
|
||||
query,
|
||||
subscriptionQuery,
|
||||
update,
|
||||
getData: (r) => r.data,
|
||||
getData: (r) => r?.data || null,
|
||||
getDelta: (r) => r.data,
|
||||
pagination: {
|
||||
first,
|
||||
@@ -215,7 +215,7 @@ describe('data provider', () => {
|
||||
const subscription = subscribe(callback, client);
|
||||
await resolveQuery({ data });
|
||||
const delta: Item[] = [];
|
||||
update.mockImplementationOnce((data, delta) => [...data, ...delta]);
|
||||
update.mockImplementationOnce((data, delta) => [...(data || []), ...delta]);
|
||||
// calling onNext from client.subscribe({ query }).subscribe(onNext)
|
||||
await clientSubscribeSubscribe.mock.calls[
|
||||
clientSubscribeSubscribe.mock.calls.length - 1
|
||||
@@ -234,7 +234,7 @@ describe('data provider', () => {
|
||||
const subscription = subscribe(callback, client);
|
||||
await resolveQuery({ data });
|
||||
const delta: Item[] = [];
|
||||
update.mockImplementationOnce((data, delta) => data);
|
||||
update.mockImplementationOnce((data, delta) => data || []);
|
||||
const callbackCallsLength = callback.mock.calls.length;
|
||||
// calling onNext from client.subscribe({ query }).subscribe(onNext)
|
||||
await clientSubscribeSubscribe.mock.calls[
|
||||
@@ -591,7 +591,7 @@ describe('derived data provider', () => {
|
||||
await resolveQuery({ data: part2 });
|
||||
expect(combineData).toBeCalledTimes(1);
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
update.mockImplementation((data, delta) => [...data, ...delta]);
|
||||
update.mockImplementation((data, delta) => [...(data || []), ...delta]);
|
||||
combineData.mockReturnValueOnce({ ...data });
|
||||
const combinedDelta = {};
|
||||
combineDelta.mockReturnValueOnce(combinedDelta);
|
||||
@@ -629,7 +629,7 @@ describe('derived data provider', () => {
|
||||
await resolveQuery({ data: [] });
|
||||
expect(combineData).toBeCalledTimes(1);
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
update.mockImplementation((data, delta) => [...data, ...delta]);
|
||||
update.mockImplementation((data, delta) => [...(data || []), ...delta]);
|
||||
combineData.mockReturnValueOnce({ ...data });
|
||||
const combinedInsertionData = {};
|
||||
combineInsertionData.mockReturnValueOnce(combinedInsertionData);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from '@apollo/client';
|
||||
import type { Subscription } from 'zen-observable-ts';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { isNotFoundGraphQLError } from './apollo-client';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
interface UpdateData<Data, Delta> {
|
||||
delta?: Delta;
|
||||
@@ -71,7 +72,12 @@ export interface Update<
|
||||
Delta,
|
||||
Variables extends OperationVariables = OperationVariables
|
||||
> {
|
||||
(data: Data, delta: Delta, reload: Reload, variables?: Variables): Data;
|
||||
(
|
||||
data: Data | null,
|
||||
delta: Delta,
|
||||
reload: Reload,
|
||||
variables?: Variables
|
||||
): Data;
|
||||
}
|
||||
|
||||
export interface Append<Data> {
|
||||
@@ -88,7 +94,7 @@ export interface Append<Data> {
|
||||
}
|
||||
|
||||
interface GetData<QueryData, Data, Variables> {
|
||||
(queryData: QueryData, variables?: Variables): Data | null;
|
||||
(queryData: QueryData | null, variables?: Variables): Data | null;
|
||||
}
|
||||
|
||||
interface GetPageInfo<QueryData> {
|
||||
@@ -160,7 +166,7 @@ interface DataProviderParams<
|
||||
> {
|
||||
query: Query<QueryData>;
|
||||
subscriptionQuery?: Query<SubscriptionData>;
|
||||
update?: Update<Data, Delta, Variables>;
|
||||
update?: Update<Data | null, Delta, Variables>;
|
||||
getData: GetData<QueryData, Data, Variables>;
|
||||
getDelta?: GetDelta<SubscriptionData, Delta, Variables>;
|
||||
pagination?: {
|
||||
@@ -349,6 +355,11 @@ function makeDataProviderInternal<
|
||||
}
|
||||
loaded = true;
|
||||
} catch (e) {
|
||||
if (isNotFoundGraphQLError(e as Error, 'party')) {
|
||||
data = getData(null, variables);
|
||||
loaded = true;
|
||||
return;
|
||||
}
|
||||
// if error will occur data provider stops subscription
|
||||
error = e as Error;
|
||||
if (subscription) {
|
||||
@@ -384,7 +395,7 @@ function makeDataProviderInternal<
|
||||
return;
|
||||
}
|
||||
const delta = getDelta(subscriptionData, variables);
|
||||
if (loading || !data) {
|
||||
if (loading) {
|
||||
updateQueue.push(delta);
|
||||
} else {
|
||||
const updatedData = update(data, delta, reload, variables);
|
||||
@@ -409,7 +420,6 @@ function makeDataProviderInternal<
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscriptionQuery && getDelta && update) {
|
||||
subscription = client
|
||||
.subscribe<SubscriptionData>({
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
export * from './format';
|
||||
export * from './grid';
|
||||
export * from './storage';
|
||||
export * from './validate';
|
||||
export * from './generic-data-provider';
|
||||
export * from './get-nodes';
|
||||
export * from './get-events';
|
||||
export * from './i18n';
|
||||
export * from './pagination';
|
||||
export * from './remove-0x';
|
||||
export * from './time';
|
||||
export * from './links';
|
||||
export * from './remove-pagination-wrapper';
|
||||
export * from './__generated__/ChainId';
|
||||
export * from './ag-grid-update';
|
||||
export * from './apollo-client';
|
||||
export * from './data-grid';
|
||||
export * from './format';
|
||||
export * from './generic-data-provider';
|
||||
export * from './get-events';
|
||||
export * from './get-nodes';
|
||||
export * from './grid';
|
||||
export * from './i18n';
|
||||
export * from './is-asset-erc20';
|
||||
export * from './links';
|
||||
export * from './local-logger';
|
||||
export * from './market-expires';
|
||||
export * from './pagination';
|
||||
export * from './remove-0x';
|
||||
export * from './remove-pagination-wrapper';
|
||||
export * from './storage';
|
||||
export * from './time';
|
||||
export * from './validate';
|
||||
|
||||
@@ -3,6 +3,7 @@ fragment TradeFields on Trade {
|
||||
price
|
||||
size
|
||||
createdAt
|
||||
aggressor
|
||||
market {
|
||||
id
|
||||
}
|
||||
@@ -35,5 +36,6 @@ subscription TradesUpdate($marketId: ID!) {
|
||||
size
|
||||
createdAt
|
||||
marketId
|
||||
aggressor
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TradeFieldsFragment = { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, market: { __typename?: 'Market', id: string } };
|
||||
export type TradeFieldsFragment = { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } };
|
||||
|
||||
export type TradesQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
@@ -11,14 +11,14 @@ export type TradesQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type TradesQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, tradesConnection?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null } | null };
|
||||
export type TradesQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, tradesConnection?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null } | null };
|
||||
|
||||
export type TradesUpdateSubscriptionVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type TradesUpdateSubscription = { __typename?: 'Subscription', trades?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string }> | null };
|
||||
export type TradesUpdateSubscription = { __typename?: 'Subscription', trades?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null };
|
||||
|
||||
export const TradeFieldsFragmentDoc = gql`
|
||||
fragment TradeFields on Trade {
|
||||
@@ -26,6 +26,7 @@ export const TradeFieldsFragmentDoc = gql`
|
||||
price
|
||||
size
|
||||
createdAt
|
||||
aggressor
|
||||
market {
|
||||
id
|
||||
}
|
||||
@@ -89,6 +90,7 @@ export const TradesUpdateDocument = gql`
|
||||
size
|
||||
createdAt
|
||||
marketId
|
||||
aggressor
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -20,7 +20,7 @@ import produce from 'immer';
|
||||
export const MAX_TRADES = 50;
|
||||
|
||||
const getData = (
|
||||
responseData: TradesQuery
|
||||
responseData: TradesQuery | null
|
||||
): ({
|
||||
cursor: string;
|
||||
node: TradeFieldsFragment;
|
||||
@@ -30,7 +30,7 @@ const getDelta = (subscriptionData: TradesUpdateSubscription) =>
|
||||
subscriptionData?.trades || [];
|
||||
|
||||
const update = (
|
||||
data: ReturnType<typeof getData>,
|
||||
data: ReturnType<typeof getData> | null,
|
||||
delta: ReturnType<typeof getDelta>
|
||||
) => {
|
||||
return produce(data, (draft) => {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
|
||||
import { DOWN_CLASS, TradesTable, UP_CLASS } from './trades-table';
|
||||
import { SELL_CLASS, TradesTable, BUY_CLASS } from './trades-table';
|
||||
import type { Trade } from './trades-data-provider';
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
|
||||
const trade: Trade = {
|
||||
__typename: 'Trade',
|
||||
id: 'trade-id',
|
||||
price: '111122200',
|
||||
size: '2000',
|
||||
aggressor: Side.SIDE_BUY,
|
||||
createdAt: new Date('2022-04-06T19:00:00').toISOString(),
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
@@ -17,59 +19,58 @@ const trade: Trade = {
|
||||
} as Trade['market'],
|
||||
};
|
||||
|
||||
it('Correct columns are rendered', async () => {
|
||||
await act(async () => {
|
||||
render(<TradesTable rowData={[trade]} />);
|
||||
});
|
||||
const expectedHeaders = ['Price', 'Size', 'Created at'];
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('Number and data columns are formatted', async () => {
|
||||
await act(async () => {
|
||||
render(<TradesTable rowData={[trade]} />);
|
||||
describe('TradesTable', () => {
|
||||
it('should render correct columns', async () => {
|
||||
await act(async () => {
|
||||
render(<TradesTable rowData={[trade]} />);
|
||||
});
|
||||
const expectedHeaders = ['Price', 'Size', 'Created at'];
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'1,111,222.00',
|
||||
'20.00',
|
||||
getDateTimeFormat().format(new Date(trade.createdAt)),
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
it('should format number and data columns', async () => {
|
||||
await act(async () => {
|
||||
render(<TradesTable rowData={[trade]} />);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'1,111,222.00',
|
||||
'20.00',
|
||||
getDateTimeFormat().format(new Date(trade.createdAt)),
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should format price and size columns', async () => {
|
||||
const trade2 = {
|
||||
...trade,
|
||||
id: 'trade-id-2',
|
||||
price: (Number(trade.price) + 10).toString(),
|
||||
size: (Number(trade.size) - 10).toString(),
|
||||
};
|
||||
await act(async () => {
|
||||
render(<TradesTable rowData={[trade2, trade]} />);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
|
||||
const priceCells = cells.filter(
|
||||
(cell) => cell.getAttribute('col-id') === 'price'
|
||||
);
|
||||
const sizeCells = cells.filter(
|
||||
(cell) => cell.getAttribute('col-id') === 'size'
|
||||
);
|
||||
|
||||
// For first trade price should have green class
|
||||
// row 1
|
||||
expect(priceCells[0]).toHaveClass(BUY_CLASS);
|
||||
expect(priceCells[1]).not.toHaveClass(SELL_CLASS);
|
||||
expect(sizeCells[1]).not.toHaveClass(SELL_CLASS);
|
||||
expect(sizeCells[1]).not.toHaveClass(BUY_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
it('Price and size columns are formatted', async () => {
|
||||
const trade2 = {
|
||||
...trade,
|
||||
id: 'trade-id-2',
|
||||
price: (Number(trade.price) + 10).toString(),
|
||||
size: (Number(trade.size) - 10).toString(),
|
||||
};
|
||||
await act(async () => {
|
||||
render(<TradesTable rowData={[trade2, trade]} />);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
|
||||
const priceCells = cells.filter(
|
||||
(cell) => cell.getAttribute('col-id') === 'price'
|
||||
);
|
||||
const sizeCells = cells.filter(
|
||||
(cell) => cell.getAttribute('col-id') === 'size'
|
||||
);
|
||||
|
||||
// For first trade price should have green class and size should have red class
|
||||
// row 1
|
||||
expect(priceCells[0]).toHaveClass(UP_CLASS);
|
||||
expect(priceCells[1]).not.toHaveClass(DOWN_CLASS);
|
||||
expect(priceCells[1]).not.toHaveClass(UP_CLASS);
|
||||
|
||||
expect(sizeCells[0]).toHaveClass(DOWN_CLASS);
|
||||
expect(sizeCells[1]).not.toHaveClass(DOWN_CLASS);
|
||||
expect(sizeCells[1]).not.toHaveClass(UP_CLASS);
|
||||
});
|
||||
|
||||
@@ -13,31 +13,22 @@ import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
|
||||
import type { CellClassParams, ValueFormatterParams } from 'ag-grid-community';
|
||||
import type { AgGridReactProps } from 'ag-grid-react';
|
||||
import type { Trade } from './trades-data-provider';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
|
||||
export const UP_CLASS = 'text-vega-green dark:text-vega-green';
|
||||
export const DOWN_CLASS = 'text-vega-pink dark:text-vega-pink';
|
||||
export const BUY_CLASS = 'text-vega-green dark:text-vega-green';
|
||||
export const SELL_CLASS = 'text-vega-pink dark:text-vega-pink';
|
||||
|
||||
const changeCellClass =
|
||||
(dataKey: string) =>
|
||||
({ api, value, node }: CellClassParams) => {
|
||||
const rowIndex = node?.rowIndex;
|
||||
let colorClass = '';
|
||||
const changeCellClass = ({ node }: CellClassParams) => {
|
||||
let colorClass = '';
|
||||
|
||||
if (typeof rowIndex === 'number') {
|
||||
const prevRowNode = api.getModel().getRow(rowIndex + 1);
|
||||
const prevValue = prevRowNode?.data && prevRowNode.data[dataKey];
|
||||
const valueNum = new BigNumber(value);
|
||||
if (node.data?.aggressor === Side.SIDE_BUY) {
|
||||
colorClass = BUY_CLASS;
|
||||
} else if (node.data?.aggressor === Side.SIDE_SELL) {
|
||||
colorClass = SELL_CLASS;
|
||||
}
|
||||
|
||||
if (valueNum.isGreaterThan(prevValue)) {
|
||||
colorClass = UP_CLASS;
|
||||
} else if (valueNum.isLessThan(prevValue)) {
|
||||
colorClass = DOWN_CLASS;
|
||||
}
|
||||
}
|
||||
|
||||
return ['font-mono text-right', colorClass].join(' ');
|
||||
};
|
||||
return ['font-mono text-right', colorClass].join(' ');
|
||||
};
|
||||
|
||||
export interface GetRowsParams extends Omit<IGetRowsParams, 'successCallback'> {
|
||||
successCallback(rowsThisBlock: (Trade | null)[], lastRow?: number): void;
|
||||
@@ -77,7 +68,7 @@ export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
field="price"
|
||||
type="rightAligned"
|
||||
width={130}
|
||||
cellClass={changeCellClass('price')}
|
||||
cellClass={changeCellClass}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
@@ -127,7 +118,6 @@ export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
}
|
||||
return addDecimal(value, data.market.positionDecimalPlaces);
|
||||
}}
|
||||
cellClass={changeCellClass('size')}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Created at')}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
import merge from 'lodash/merge';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type {
|
||||
@@ -47,6 +48,7 @@ export const tradesUpdateSubscription = (
|
||||
size: '24',
|
||||
createdAt: '2022-04-06T16:19:42.692598951Z',
|
||||
marketId: 'market-0',
|
||||
aggressor: Side.SIDE_BUY,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -59,6 +61,7 @@ const trades: TradeFieldsFragment[] = [
|
||||
price: '17116898',
|
||||
size: '24',
|
||||
createdAt: '2022-04-06T16:19:42.692598951Z',
|
||||
aggressor: Side.SIDE_BUY,
|
||||
market: {
|
||||
id: 'market-0',
|
||||
__typename: 'Market',
|
||||
@@ -70,6 +73,7 @@ const trades: TradeFieldsFragment[] = [
|
||||
price: '17209102',
|
||||
size: '7',
|
||||
createdAt: '2022-04-07T06:59:44.835686754Z',
|
||||
aggressor: Side.SIDE_SELL,
|
||||
market: {
|
||||
id: 'market-0',
|
||||
__typename: 'Market',
|
||||
@@ -81,6 +85,7 @@ const trades: TradeFieldsFragment[] = [
|
||||
price: '17106734',
|
||||
size: '18',
|
||||
createdAt: '2022-04-07T17:56:47.997938583Z',
|
||||
aggressor: Side.SIDE_BUY,
|
||||
market: {
|
||||
id: 'market-0',
|
||||
__typename: 'Market',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Splash } from '../splash';
|
||||
import type { ReactNode } from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { isNotFoundGraphQLError } from '@vegaprotocol/apollo-client';
|
||||
|
||||
interface AsyncRendererProps<T> {
|
||||
loading: boolean;
|
||||
@@ -26,7 +25,7 @@ export function AsyncRenderer<T = object>({
|
||||
children,
|
||||
render,
|
||||
}: AsyncRendererProps<T>) {
|
||||
if (error && !isNotFoundGraphQLError(error)) {
|
||||
if (error) {
|
||||
return (
|
||||
<Splash>
|
||||
{errorMessage
|
||||
|
||||
@@ -11,5 +11,5 @@ export const Indicator = ({ variant = Intent.None }: IndicatorProps) => {
|
||||
'inline-block w-2 h-2 mt-1 mr-2 rounded-full',
|
||||
getIntentTextAndBackground(variant)
|
||||
);
|
||||
return <div className={names} />;
|
||||
return <div className={names} data-testid="indicator" />;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { VegaWalletProvider } from '../provider';
|
||||
import { VegaConnectDialog, CLOSE_DELAY } from './connect-dialog';
|
||||
import type { VegaWalletDialogStore } from './connect-dialog';
|
||||
import type { VegaConnectDialogProps } from '..';
|
||||
import {
|
||||
ClientErrors,
|
||||
@@ -23,13 +24,15 @@ import { ChainIdDocument } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const mockUpdateDialogOpen = jest.fn();
|
||||
const mockCloseVegaDialog = jest.fn();
|
||||
const mockStoreObj: Partial<VegaWalletDialogStore> = {
|
||||
updateVegaWalletDialog: mockUpdateDialogOpen,
|
||||
closeVegaWalletDialog: mockCloseVegaDialog,
|
||||
vegaWalletDialogOpen: true,
|
||||
};
|
||||
|
||||
jest.mock('zustand', () => ({
|
||||
create: () => () => ({
|
||||
updateVegaWalletDialog: mockUpdateDialogOpen,
|
||||
closeVegaWalletDialog: mockCloseVegaDialog,
|
||||
vegaWalletDialogOpen: true,
|
||||
}),
|
||||
create: () => (storeGetter: (store: VegaWalletDialogStore) => unknown) =>
|
||||
storeGetter(mockStoreObj as VegaWalletDialogStore),
|
||||
}));
|
||||
|
||||
let defaultProps: VegaConnectDialogProps;
|
||||
@@ -301,7 +304,9 @@ describe('VegaConnectDialog', () => {
|
||||
spyOnConnectWallet
|
||||
.mockClear()
|
||||
.mockImplementation(() =>
|
||||
delayedReject(new WalletError('message', 3001, 'data'))
|
||||
delayedReject(
|
||||
new WalletError('User error', 3001, 'The user rejected the request')
|
||||
)
|
||||
);
|
||||
|
||||
render(generateJSX());
|
||||
@@ -322,9 +327,9 @@ describe('VegaConnectDialog', () => {
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(delay);
|
||||
});
|
||||
expect(screen.getByText('Connection declined')).toBeInTheDocument();
|
||||
expect(screen.getByText('User error')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Your wallet connection was rejected')
|
||||
screen.getByText('The user rejected the request')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@ import {
|
||||
Loader,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { WalletClientError } from '@vegaprotocol/wallet-client';
|
||||
import { ExternalLinks, t, useChainIdQuery } from '@vegaprotocol/react-helpers';
|
||||
import type { VegaConnector, WalletError } from '../connectors';
|
||||
import type { VegaConnector } from '../connectors';
|
||||
import { ViewConnector } from '../connectors';
|
||||
import { JsonRpcConnector, RestConnector } from '../connectors';
|
||||
import { RestConnectorForm } from './rest-connector-form';
|
||||
@@ -44,7 +45,7 @@ export const useVegaWalletDialogStore = create<VegaWalletDialogStore>(
|
||||
})
|
||||
);
|
||||
|
||||
interface VegaWalletDialogStore {
|
||||
export interface VegaWalletDialogStore {
|
||||
vegaWalletDialogOpen: boolean;
|
||||
updateVegaWalletDialog: (open: boolean) => void;
|
||||
openVegaWalletDialog: () => void;
|
||||
@@ -55,25 +56,25 @@ export const VegaConnectDialog = ({
|
||||
connectors,
|
||||
onChangeOpen,
|
||||
}: VegaConnectDialogProps) => {
|
||||
const {
|
||||
vegaWalletDialogOpen,
|
||||
closeVegaWalletDialog,
|
||||
updateVegaWalletDialog,
|
||||
} = useVegaWalletDialogStore((store) => ({
|
||||
vegaWalletDialogOpen: store.vegaWalletDialogOpen,
|
||||
updateVegaWalletDialog: onChangeOpen
|
||||
const vegaWalletDialogOpen = useVegaWalletDialogStore(
|
||||
(store) => store.vegaWalletDialogOpen
|
||||
);
|
||||
const updateVegaWalletDialog = useVegaWalletDialogStore((store) =>
|
||||
onChangeOpen
|
||||
? (open: boolean) => {
|
||||
store.updateVegaWalletDialog(open);
|
||||
onChangeOpen(open);
|
||||
}
|
||||
: store.updateVegaWalletDialog,
|
||||
closeVegaWalletDialog: onChangeOpen
|
||||
: store.updateVegaWalletDialog
|
||||
);
|
||||
const closeVegaWalletDialog = useVegaWalletDialogStore((store) =>
|
||||
onChangeOpen
|
||||
? () => {
|
||||
store.closeVegaWalletDialog();
|
||||
onChangeOpen(false);
|
||||
}
|
||||
: store.closeVegaWalletDialog,
|
||||
}));
|
||||
: store.closeVegaWalletDialog
|
||||
);
|
||||
|
||||
const { data, error, loading } = useChainIdQuery();
|
||||
|
||||
@@ -254,7 +255,7 @@ const SelectedForm = ({
|
||||
appChainId: string;
|
||||
jsonRpcState: {
|
||||
status: Status;
|
||||
error: WalletError | null;
|
||||
error: WalletClientError | null;
|
||||
};
|
||||
reset: () => void;
|
||||
onConnect: () => void;
|
||||
|
||||
@@ -8,16 +8,15 @@ import {
|
||||
Tick,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { WalletClientError } from '@vegaprotocol/wallet-client';
|
||||
import type { JsonRpcConnector } from '../connectors';
|
||||
import { ClientErrors } from '../connectors';
|
||||
import type { WalletError } from '../connectors';
|
||||
import { ConnectDialogTitle } from './connect-dialog-elements';
|
||||
import { Status } from '../use-json-rpc-connect';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
export const ServiceErrors = {
|
||||
NO_HEALTHY_NODE: 1000,
|
||||
CONNECTION_DECLINED: 3001,
|
||||
REQUEST_PROCESSING: -32000,
|
||||
};
|
||||
|
||||
@@ -31,7 +30,7 @@ export const JsonRpcConnectorForm = ({
|
||||
connector: JsonRpcConnector;
|
||||
appChainId: string;
|
||||
status: Status;
|
||||
error: WalletError | null;
|
||||
error: WalletClientError | null;
|
||||
onConnect: () => void;
|
||||
reset: () => void;
|
||||
}) => {
|
||||
@@ -58,7 +57,7 @@ const Connecting = ({
|
||||
reset,
|
||||
}: {
|
||||
status: Status;
|
||||
error: WalletError | null;
|
||||
error: WalletClientError | null;
|
||||
connector: JsonRpcConnector;
|
||||
appChainId: string;
|
||||
reset: () => void;
|
||||
@@ -141,7 +140,7 @@ const Error = ({
|
||||
appChainId,
|
||||
onTryAgain,
|
||||
}: {
|
||||
error: WalletError | null;
|
||||
error: WalletClientError | null;
|
||||
connectorUrl: string | null;
|
||||
appChainId: string;
|
||||
onTryAgain: () => void;
|
||||
@@ -158,18 +157,20 @@ const Error = ({
|
||||
if (error) {
|
||||
if (error.code === ClientErrors.NO_SERVICE.code) {
|
||||
title = t('No wallet detected');
|
||||
text = t(`No wallet application running at ${connectorUrl}`);
|
||||
text = connectorUrl
|
||||
? t('No wallet application running at %s', connectorUrl)
|
||||
: t('No Vega Wallet application running');
|
||||
} else if (error.code === ClientErrors.WRONG_NETWORK.code) {
|
||||
title = t('Wrong network');
|
||||
text = `To complete your wallet connection, set your wallet network in your app to "${appChainId}".`;
|
||||
} else if (error.code === ServiceErrors.CONNECTION_DECLINED) {
|
||||
title = t('Connection declined');
|
||||
text = t('Your wallet connection was rejected');
|
||||
text = t(
|
||||
'To complete your wallet connection, set your wallet network in your app to "%s".',
|
||||
appChainId
|
||||
);
|
||||
} else if (error.code === ServiceErrors.NO_HEALTHY_NODE) {
|
||||
title = error.message;
|
||||
title = error.title;
|
||||
text = (
|
||||
<>
|
||||
{capitalize(error.data)}
|
||||
{capitalize(error.message)}
|
||||
{'. '}
|
||||
{VEGA_DOCS_URL && (
|
||||
<Link
|
||||
@@ -188,20 +189,33 @@ const Error = ({
|
||||
title = t('Wrong network');
|
||||
text = (
|
||||
<>
|
||||
{t(`To complete your wallet connection, set your wallet network in your
|
||||
app to ${appChainId}.`)}
|
||||
{t(
|
||||
`To complete your wallet connection, set your wallet network in your
|
||||
app to %s.`,
|
||||
appChainId
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else if (error.code === ClientErrors.INVALID_WALLET.code) {
|
||||
title = error.title;
|
||||
const errorData = error.message?.split('\n ') || [];
|
||||
text = (
|
||||
<span className="flex flex-col">
|
||||
{errorData.map((str, i) => (
|
||||
<span key={i}>{str}</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
title = error.message;
|
||||
text = `${error.data} (${error.code})`;
|
||||
title = t(error.title);
|
||||
text = t(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogTitle>{title}</ConnectDialogTitle>
|
||||
<p className="text-center mb-2">{text}</p>
|
||||
<p className="text-center mb-2 first-letter:uppercase">{text}</p>
|
||||
{tryAgain}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,8 +8,6 @@ const VERSION = 'v2';
|
||||
|
||||
export const ClientErrors = {
|
||||
NO_SERVICE: new WalletError(t('No service'), 100),
|
||||
NO_TOKEN: new WalletError(t('No token'), 101),
|
||||
INVALID_RESPONSE: new WalletError(t('Something went wrong'), 102),
|
||||
INVALID_WALLET: new WalletError(t('Wallet version invalid'), 103),
|
||||
WRONG_NETWORK: new WalletError(
|
||||
t('Wrong network'),
|
||||
@@ -22,11 +20,6 @@ export const ClientErrors = {
|
||||
t('Unknown error occurred')
|
||||
),
|
||||
NO_CLIENT: new WalletError(t('No client found.'), 106),
|
||||
REQUEST_REJECTED: new WalletError(
|
||||
t('Request rejected'),
|
||||
107,
|
||||
t('The request has been rejected by the user')
|
||||
),
|
||||
} as const;
|
||||
|
||||
export class JsonRpcConnector implements VegaConnector {
|
||||
@@ -70,7 +63,9 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this._url || '';
|
||||
}
|
||||
async getChainId() {
|
||||
if (!this.client) {
|
||||
throw ClientErrors.NO_CLIENT;
|
||||
@@ -79,7 +74,12 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
const { result } = await this.client.GetChainId();
|
||||
return result;
|
||||
} catch (err) {
|
||||
throw ClientErrors.INVALID_RESPONSE;
|
||||
const {
|
||||
code = ClientErrors.UNKNOWN.code,
|
||||
message = ClientErrors.UNKNOWN.message,
|
||||
title,
|
||||
} = err as WalletClientError;
|
||||
throw new WalletError(title, code, message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +92,12 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
await this.client.ConnectWallet();
|
||||
return null;
|
||||
} catch (err) {
|
||||
const clientErr =
|
||||
err instanceof WalletClientError && err.code === 3001
|
||||
? ClientErrors.REQUEST_REJECTED
|
||||
: ClientErrors.INVALID_RESPONSE;
|
||||
throw clientErr;
|
||||
const {
|
||||
code = ClientErrors.UNKNOWN.code,
|
||||
message = ClientErrors.UNKNOWN.message,
|
||||
title,
|
||||
} = err as WalletClientError;
|
||||
throw new WalletError(title, code, message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +112,12 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
const { result } = await this.client.ListKeys();
|
||||
return result.keys;
|
||||
} catch (err) {
|
||||
throw ClientErrors.INVALID_RESPONSE;
|
||||
const {
|
||||
code = ClientErrors.UNKNOWN.code,
|
||||
message = ClientErrors.UNKNOWN.message,
|
||||
title,
|
||||
} = err as WalletClientError;
|
||||
throw new WalletError(title, code, message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,15 +153,21 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
try {
|
||||
const result = await fetch(`${this._url}/api/${this.version}/methods`);
|
||||
if (!result.ok) {
|
||||
const err = ClientErrors.INVALID_WALLET;
|
||||
err.data = t(
|
||||
`The wallet running at ${this._url} is not supported. Required version is ${this.version}`
|
||||
const sent1 = t(
|
||||
'The version of the wallet service running at %s is not supported.',
|
||||
this._url as string
|
||||
);
|
||||
throw err;
|
||||
const sent2 = t(
|
||||
'Update the wallet software to a version that expose the API %s.',
|
||||
this.version
|
||||
);
|
||||
const data = `${sent1}\n ${sent2}`;
|
||||
const title = t('Wallet version invalid');
|
||||
throw new WalletError(title, ClientErrors.INVALID_WALLET.code, data);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof WalletError) {
|
||||
if (err instanceof WalletClientError) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WalletClientError } from '@vegaprotocol/wallet-client';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
|
||||
export interface DelegateSubmissionBody {
|
||||
@@ -329,14 +330,11 @@ export interface TransactionResponse {
|
||||
receivedAt: string;
|
||||
sentAt: string;
|
||||
}
|
||||
export class WalletError {
|
||||
message: string;
|
||||
code: number;
|
||||
data?: string;
|
||||
export class WalletError extends WalletClientError {
|
||||
data: string;
|
||||
|
||||
constructor(message: string, code: number, data?: string) {
|
||||
this.message = message;
|
||||
this.code = code;
|
||||
constructor(message: string, code: number, data = 'Wallet error') {
|
||||
super({ code, message, data });
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user