Compare commits

...
Author SHA1 Message Date
Dariusz Majcherczyk 17bc4541e9 chore: multi click for toast 2023-01-29 19:51:12 +01:00
Dariusz Majcherczyk 2bd8044a6c chore: reverted proper assertion 2023-01-29 16:03:16 +01:00
Dariusz Majcherczyk b3a82fd6f0 chore: capsule tests refactor and increase deposit AC test coverage 2023-01-29 14:09:43 +01:00
mattrussell36 f22fb56afd chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-29 12:06:12 +00:00
mattrussell36 f2627e6ebf chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-29 06:07:03 +00:00
mattrussell36 24e6a13d93 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-29 00:13:02 +00:00
mattrussell36 b56050ad1c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-28 18:07:37 +00:00
mattrussell36 cff83ed062 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-28 12:06:05 +00:00
mattrussell36 8ecce874b0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-28 06:08:30 +00:00
Bartłomiej Głownia 00e319b3c6 chore: handle not found errors as correct response (#2759) 2023-01-27 18:35:45 -08:00
mattrussell36 05559f3ea0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-28 00:11:14 +00:00
m.ray 4b3b5c322a fix: trades grid colors (#2766) 2023-01-27 18:55:20 +00:00
Bartłomiej Głownia b1280c8285 fix: hide close button if openVolume is zero (#2768) 2023-01-27 18:44:20 +00:00
Edd 1e0e1c7859 feat(explorer): add long text component for hashes and tx viewer (#2765) 2023-01-27 18:37:59 +00:00
mattrussell36 ec2bb81ec8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-27 18:08:36 +00:00
m.ray fe95c6fcbc fix: show markets link & deal ticket validation (#2763) 2023-01-27 18:10:20 +01:00
macqbat 853ec8f69c chore(2351): improve handling wallet errors (#2729) 2023-01-27 15:45:29 +01:00
mattrussell36 b68c090ee5 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-27 12:09:03 +00:00
Matthew Russellandasiaznik cb9b811730 feat(#1791): block height check (#2701)
Co-authored-by: asiaznik <artur@vegaprotocol.io>
2023-01-27 11:31:23 +01:00
Bartłomiej Głownia 98b5260d93 feat: fix positions and margins providers update (#2753) 2023-01-27 09:39:39 +01:00
mattrussell36 613262f7a5 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-27 06:05:57 +00:00
mattrussell36 9ae6f5201c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-27 00:10:54 +00:00
m.ray cfb75fcf8f fix: volume charts position dp (#2756) 2023-01-26 19:36:29 +00:00
mattrussell36 ae6ccdfb52 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-26 18:08:18 +00:00
Radosław Szpiech 9e73c6a462 chore: 2593 trading-e2e refactor - split global.cy (#2754) 2023-01-26 17:50:33 +01:00
Edd 24c017bc2b feat(explorer): add basic votesubmission tx view (#2694) 2023-01-26 14:53:34 +00:00
Edd 3188f23439 fix(explorer): place sensible limits on oracles page queries (#2699) 2023-01-26 14:50:52 +00:00
Edd 33e528c569 fix(explorer): switch decimals used in lp commitment views (#2747) 2023-01-26 14:47:31 +00:00
Radosław Szpiech e10945eeda chore: correct ethereum releated envs in trading-e2e (#2744) 2023-01-26 15:40:49 +01:00
m.ray 47b2a2fdcb fix: volume chart values fixing - position dp (#2749) 2023-01-26 14:21:43 +00:00
m.ray 860938f26c fix: market selected no longer available - add link to all markets (#2748) 2023-01-26 13:46:44 +00:00
161 changed files with 2802 additions and 1473 deletions
-1
View File
@@ -68,7 +68,6 @@ jobs:
working-directory: frontend-monorepo
env:
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
CYPRESS_grepTags: ${{ inputs.tags }}
+2 -1
View File
@@ -6,4 +6,5 @@ NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https:
NX_VEGA_ENV=DEVNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
+1
View File
@@ -7,3 +7,4 @@ NX_VEGA_ENV=MAINNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
+1
View File
@@ -7,6 +7,7 @@ NX_VEGA_ENV=MIRROR
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
# App flags
NX_EXPLORER_ASSETS=1
+1
View File
@@ -9,3 +9,4 @@ NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https:
NX_TENDERMINT_URL=https://tm.n01.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.sandbox.vega.xyz/websocket
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
+1
View File
@@ -11,3 +11,4 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz
+2 -1
View File
@@ -4,4 +4,5 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
+1
View File
@@ -10,3 +10,4 @@ NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_NETWORKS={}
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
+1 -1
View File
@@ -70,7 +70,7 @@
"executor": "@nrwl/workspace:run-commands",
"options": {
"commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.66.1/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.67.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
]
}
},
@@ -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;
@@ -8,6 +8,9 @@ query ExplorerMarket($id: ID!) {
product {
... on Future {
quoteName
settlementAsset {
decimals
}
}
}
}
@@ -8,7 +8,7 @@ export type ExplorerMarketQueryVariables = Types.Exact<{
}>;
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null };
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null };
export const ExplorerMarketDocument = gql`
@@ -22,6 +22,9 @@ export const ExplorerMarketDocument = gql`
product {
... on Future {
quoteName
settlementAsset {
decimals
}
}
}
}
@@ -1,13 +1,14 @@
import React from 'react';
import { Routes } from '../../../routes/route-names';
import { useExplorerMarketQuery } from './__generated__/Market';
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;
showMarketName?: boolean;
};
/**
@@ -15,7 +16,11 @@ export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
* with a link to the markets list. If the name does not come back
* it will use the ID instead
*/
const MarketLink = ({ id, ...props }: MarketLinkProps) => {
const MarketLink = ({
id,
showMarketName = true,
...props
}: MarketLinkProps) => {
const { data, error, loading } = useExplorerMarketQuery({
variables: { id },
});
@@ -31,17 +36,31 @@ const MarketLink = ({ id, ...props }: MarketLinkProps) => {
<span role="img" aria-label="Unknown market" className="img">
&nbsp;{t('Invalid market')}
</span>
&nbsp;{id}
&nbsp;
<Hash text={id} />
</div>
);
}
}
return (
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
{label}
</Link>
);
if (showMarketName) {
return (
<Link
className="underline"
{...props}
to={`/${Routes.MARKETS}#${id}`}
title={id}
>
{label}
</Link>
);
} else {
return (
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
<Hash text={id} />
</Link>
);
}
};
export default MarketLink;
@@ -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>
);
};
@@ -0,0 +1,23 @@
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;
};
const OracleLink = ({ id, ...props }: OracleLinkProps) => {
return (
<Link
className="underline font-mono"
{...props}
to={`/${Routes.ORACLES}/${id}`}
>
<Hash text={id} />
</Link>
);
};
export default OracleLink;
@@ -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>
);
};
@@ -0,0 +1,9 @@
query ExplorerProposal($id: ID!) {
proposal(id: $id) {
id
rationale {
title
description
}
}
}
@@ -0,0 +1,52 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerProposalQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
export const ExplorerProposalDocument = gql`
query ExplorerProposal($id: ID!) {
proposal(id: $id) {
id
rationale {
title
description
}
}
}
`;
/**
* __useExplorerProposalQuery__
*
* To run a query within a React component, call `useExplorerProposalQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerProposalQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerProposalQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerProposalQuery(baseOptions: Apollo.QueryHookOptions<ExplorerProposalQuery, ExplorerProposalQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerProposalQuery, ExplorerProposalQueryVariables>(ExplorerProposalDocument, options);
}
export function useExplorerProposalLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerProposalQuery, ExplorerProposalQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerProposalQuery, ExplorerProposalQueryVariables>(ExplorerProposalDocument, options);
}
export type ExplorerProposalQueryHookResult = ReturnType<typeof useExplorerProposalQuery>;
export type ExplorerProposalLazyQueryHookResult = ReturnType<typeof useExplorerProposalLazyQuery>;
export type ExplorerProposalQueryResult = Apollo.QueryResult<ExplorerProposalQuery, ExplorerProposalQueryVariables>;
@@ -0,0 +1,82 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import ProposalLink from './proposal-link';
import { ExplorerProposalDocument } from './__generated__/Proposal';
import { GraphQLError } from 'graphql';
function renderComponent(id: string, mocks: MockedResponse[]) {
return (
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<ProposalLink id={id} />
</MemoryRouter>
</MockedProvider>
);
}
describe('Proposal link component', () => {
it('Renders the ID at first', () => {
const res = render(renderComponent('123', []));
expect(res.getByText('123')).toBeInTheDocument();
});
it('Renders the ID on error', async () => {
const mock = {
request: {
query: ExplorerProposalDocument,
variables: {
id: '456',
},
},
result: {
errors: [new GraphQLError('No such proposal')],
},
};
const res = render(renderComponent('456', [mock]));
// The ID
expect(res.getByText('456')).toBeInTheDocument();
});
it('Renders the proposal title when the query returns a result', async () => {
const mock = {
request: {
query: ExplorerProposalDocument,
variables: {
id: '123',
},
},
result: {
data: {
proposal: {
id: '123',
rationale: {
title: 'test-title',
description: 'test description',
},
},
},
},
};
const res = render(renderComponent('123', [mock]));
expect(res.getByText('123')).toBeInTheDocument();
expect(await res.findByText('test-title')).toBeInTheDocument();
});
it('Leaves the proposal id when the market is not found', async () => {
const mock = {
request: {
query: ExplorerProposalDocument,
variables: {
id: '123',
},
},
error: new Error('No such asset'),
};
const res = render(renderComponent('123', [mock]));
expect(await res.findByText('123')).toBeInTheDocument();
});
});
@@ -0,0 +1,28 @@
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;
};
/**
* Given a proposal ID, generates an external link over to
* the Governance page for more information
*/
const ProposalLink = ({ id }: ProposalLinkProps) => {
const { data } = useExplorerProposalQuery({
variables: { id },
});
const base = ENV.dataSources.governanceUrl;
const label = data?.proposal?.rationale.title || id;
return (
<ExternalLink href={`${base}/proposals/${id}`}>
<Hash text={label} />
</ExternalLink>
);
};
export default ProposalLink;
@@ -97,6 +97,10 @@ describe('Order TX Summary component', () => {
product: {
__typename: 'Future',
quoteName: 'TEST',
settlementAsset: {
__typeName: 'SettlementAsset',
decimals: 18,
},
},
},
},
@@ -3,61 +3,78 @@ import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import PriceInMarket from './price-in-market';
import type { DecimalSource } from './price-in-market';
import { ExplorerMarketDocument } from '../links/market-link/__generated__/Market';
function renderComponent(
price: string,
marketId: string,
mocks: MockedResponse[]
mocks: MockedResponse[],
decimalSource: DecimalSource = 'MARKET'
) {
return (
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<PriceInMarket marketId={marketId} price={price} />
<PriceInMarket
marketId={marketId}
price={price}
decimalSource={decimalSource}
/>
</MemoryRouter>
</MockedProvider>
);
}
const fullMock = {
request: {
query: ExplorerMarketDocument,
variables: {
id: '123',
},
},
result: {
data: {
market: {
id: '123',
decimalPlaces: 2,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
name: 'test dai',
product: {
__typename: 'Future',
quoteName: 'dai',
settlementAsset: {
decimals: 18,
},
},
},
},
},
},
},
};
describe('Price in Market component', () => {
it('Renders the raw price when there is no market data', () => {
const res = render(renderComponent('100', '123', []));
expect(res.getByText('100')).toBeInTheDocument();
});
it('Renders the formatted price when market data is fetched', async () => {
const mock = {
request: {
query: ExplorerMarketDocument,
variables: {
id: '123',
},
},
result: {
data: {
market: {
id: '123',
decimalPlaces: 2,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
name: 'test dai',
product: {
__typename: 'Future',
quoteName: 'dai',
},
},
},
},
},
},
};
const res = render(renderComponent('100', '123', [mock]));
it('Renders the formatted price when market data is fetched, using market decimals by default', async () => {
const res = render(renderComponent('100', '123', [fullMock]));
expect(await res.findByText('1.00')).toBeInTheDocument();
expect(await res.findByText('dai')).toBeInTheDocument();
});
it('Renders the formatted price when market data is fetched, using settlement decimals', async () => {
const res = render(
renderComponent('100', '123', [fullMock], 'SETTLEMENT_ASSET')
);
expect(await res.findByText('0.0000000000000001')).toBeInTheDocument();
expect(await res.findByText('dai')).toBeInTheDocument();
});
it('Leaves the market id when the market is not found', async () => {
const mock = {
request: {
@@ -3,16 +3,23 @@ import isUndefined from 'lodash/isUndefined';
import { useExplorerMarketQuery } from '../links/market-link/__generated__/Market';
import get from 'lodash/get';
export type DecimalSource = 'MARKET' | 'SETTLEMENT_ASSET';
export type PriceInMarketProps = {
marketId: string;
price: string;
decimalSource?: DecimalSource;
};
/**
* Given a market ID and a price it will fetch the market
* and format the price in that market's decimal places.
*/
const PriceInMarket = ({ marketId, price }: PriceInMarketProps) => {
const PriceInMarket = ({
marketId,
price,
decimalSource = 'MARKET',
}: PriceInMarketProps) => {
const { data } = useExplorerMarketQuery({
variables: { id: marketId },
fetchPolicy: 'cache-first',
@@ -20,8 +27,19 @@ const PriceInMarket = ({ marketId, price }: PriceInMarketProps) => {
let label = price;
if (data && data.market?.decimalPlaces) {
label = addDecimalsFormatNumber(price, data.market.decimalPlaces);
if (data) {
if (decimalSource === 'MARKET' && data.market?.decimalPlaces) {
label = addDecimalsFormatNumber(price, data.market.decimalPlaces);
} else if (
decimalSource === 'SETTLEMENT_ASSET' &&
data.market?.tradableInstrument.instrument.product.settlementAsset
) {
label = addDecimalsFormatNumber(
price,
data.market?.tradableInstrument.instrument.product.settlementAsset
.decimals
);
}
}
const suffix = get(
@@ -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}`
);
});
@@ -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}`
);
});
@@ -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}`
);
});
@@ -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}`
);
});
@@ -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}`
);
});
@@ -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}`
);
});
@@ -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}`
);
});
@@ -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';
@@ -20,6 +18,7 @@ import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
import { TxDetailsLiquidityAmendment } from './tx-liquidity-amend';
import { TxDetailsLiquidityCancellation } from './tx-liquidity-cancel';
import { TxDetailsDataSubmission } from './tx-data-submission';
import { TxProposalVote } from './tx-proposal-vote';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -45,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>
);
};
@@ -91,6 +76,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsOrderAmend;
case 'Validator Heartbeat':
return TxDetailsHeartbeat;
case 'Vote on Proposal':
return TxProposalVote;
case 'Batch Market Instructions':
return TxDetailsBatch;
case 'Chain Event':
@@ -55,6 +55,7 @@ export const TxDetailsLiquidityAmendment = ({
<PriceInMarket
price={amendment.commitmentAmount}
marketId={marketId}
decimalSource="SETTLEMENT_ASSET"
/>
</TableCell>
</TableRow>
@@ -54,6 +54,7 @@ export const TxDetailsLiquiditySubmission = ({
<PriceInMarket
price={submission.commitmentAmount}
marketId={marketId}
decimalSource="SETTLEMENT_ASSET"
/>
</TableCell>
</TableRow>
@@ -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,13 @@ export const TxDetailsOrder = ({
<TableRow modifier="bordered">
<TableCell>{t('Order')}</TableCell>
<TableCell>
<code>{deterministicId}</code>
<Hash text={deterministicId} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Market ID')}</TableCell>
<TableCell>
<MarketLink id={marketId} showMarketName={false} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
@@ -0,0 +1,57 @@
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import ProposalLink from '../../links/proposal-link/proposal-link';
interface TxProposalVoteProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* A vote on a proposal.
*
* One inconsistency here that feels right but should be standardised is that there are two rows
* for the proposal ID, one that creates a link with the text of the proposal title that takes
* a user out to the governance site, and the other that just shows the ID. Both are useful, but
* doesn't feel quite right. This could be fixed with a separate component to display a preview
* of the proposal and link off to the governance site, removing the title from the header. Or
* something else. For now, this is more useful than the default view
*/
export const TxProposalVote = ({
txData,
pubKey,
blockData,
}: TxProposalVoteProps) => {
if (!txData || !txData.command.voteSubmission) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const vote = txData.command.voteSubmission.value ? '👍' : '👎';
return (
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Proposal details')}</TableCell>
<TableCell>
<ProposalLink id={txData.command.voteSubmission.proposalId} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Proposal')}</TableCell>
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Vote')}</TableCell>
<TableCell>{vote}</TableCell>
</TableRow>
</TableWithTbody>
);
};
@@ -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%'}
+1
View File
@@ -16,6 +16,7 @@ export const ENV = {
tendermintUrl: windowOrDefault('NX_TENDERMINT_URL'),
tendermintWebsocketUrl: windowOrDefault('NX_TENDERMINT_WEBSOCKET_URL'),
ethExplorerUrl: windowOrDefault('NX_ETHERSCAN_URL'),
governanceUrl: windowOrDefault('NX_VEGA_GOVERNANCE_URL'),
},
flags: {
assets: truthy.includes(windowOrDefault('NX_EXPLORER_ASSETS')),
@@ -51,6 +51,9 @@ fragment ExplorerOracleDataSource on OracleSpec {
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
pageInfo {
hasNextPage
}
edges {
node {
externalData {
@@ -79,12 +82,21 @@ fragment ExplorerOracleDataConnection on OracleSpec {
}
query ExplorerOracleSpecs {
oracleSpecsConnection {
oracleSpecsConnection(pagination: { first: 50 }) {
pageInfo {
hasNextPage
}
edges {
node {
...ExplorerOracleDataSource
...ExplorerOracleDataConnection
}
}
}
}
query ExplorerOracleSpecById($id: ID!) {
oracleSpec(oracleSpecId: $id) {
...ExplorerOracleDataSource
...ExplorerOracleDataConnection
}
}
+55 -5
View File
@@ -5,12 +5,19 @@ import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerOracleDataSourceFragment = { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } } };
export type ExplorerOracleDataConnectionFragment = { __typename?: 'OracleSpec', dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
export type ExplorerOracleDataConnectionFragment = { __typename?: 'OracleSpec', dataConnection: { __typename?: 'OracleDataConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
export type ExplorerOracleSpecsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null };
export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null };
export type ExplorerOracleSpecByIdQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerOracleSpecByIdQuery = { __typename?: 'Query', oracleSpec?: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } | null };
export const ExplorerOracleDataSourceFragmentDoc = gql`
fragment ExplorerOracleDataSource on OracleSpec {
@@ -66,7 +73,10 @@ export const ExplorerOracleDataSourceFragmentDoc = gql`
`;
export const ExplorerOracleDataConnectionFragmentDoc = gql`
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
dataConnection(pagination: {first: 1}) {
pageInfo {
hasNextPage
}
edges {
node {
externalData {
@@ -96,7 +106,10 @@ export const ExplorerOracleDataConnectionFragmentDoc = gql`
`;
export const ExplorerOracleSpecsDocument = gql`
query ExplorerOracleSpecs {
oracleSpecsConnection {
oracleSpecsConnection(pagination: {first: 50}) {
pageInfo {
hasNextPage
}
edges {
node {
...ExplorerOracleDataSource
@@ -133,4 +146,41 @@ export function useExplorerOracleSpecsLazyQuery(baseOptions?: Apollo.LazyQueryHo
}
export type ExplorerOracleSpecsQueryHookResult = ReturnType<typeof useExplorerOracleSpecsQuery>;
export type ExplorerOracleSpecsLazyQueryHookResult = ReturnType<typeof useExplorerOracleSpecsLazyQuery>;
export type ExplorerOracleSpecsQueryResult = Apollo.QueryResult<ExplorerOracleSpecsQuery, ExplorerOracleSpecsQueryVariables>;
export type ExplorerOracleSpecsQueryResult = Apollo.QueryResult<ExplorerOracleSpecsQuery, ExplorerOracleSpecsQueryVariables>;
export const ExplorerOracleSpecByIdDocument = gql`
query ExplorerOracleSpecById($id: ID!) {
oracleSpec(oracleSpecId: $id) {
...ExplorerOracleDataSource
...ExplorerOracleDataConnection
}
}
${ExplorerOracleDataSourceFragmentDoc}
${ExplorerOracleDataConnectionFragmentDoc}`;
/**
* __useExplorerOracleSpecByIdQuery__
*
* To run a query within a React component, call `useExplorerOracleSpecByIdQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerOracleSpecByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerOracleSpecByIdQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerOracleSpecByIdQuery(baseOptions: Apollo.QueryHookOptions<ExplorerOracleSpecByIdQuery, ExplorerOracleSpecByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerOracleSpecByIdQuery, ExplorerOracleSpecByIdQueryVariables>(ExplorerOracleSpecByIdDocument, options);
}
export function useExplorerOracleSpecByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerOracleSpecByIdQuery, ExplorerOracleSpecByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerOracleSpecByIdQuery, ExplorerOracleSpecByIdQueryVariables>(ExplorerOracleSpecByIdDocument, options);
}
export type ExplorerOracleSpecByIdQueryHookResult = ReturnType<typeof useExplorerOracleSpecByIdQuery>;
export type ExplorerOracleSpecByIdLazyQueryHookResult = ReturnType<typeof useExplorerOracleSpecByIdLazyQuery>;
export type ExplorerOracleSpecByIdQueryResult = Apollo.QueryResult<ExplorerOracleSpecByIdQuery, ExplorerOracleSpecByIdQueryVariables>;
@@ -37,7 +37,7 @@ describe('Oracle Data view', () => {
dataConnection: {
edges: [],
},
} as ExplorerOracleDataConnectionFragment)
} as unknown as ExplorerOracleDataConnectionFragment)
);
expect(res.container).toBeEmptyDOMElement();
});
@@ -67,7 +67,7 @@ describe('Oracle Signers component', () => {
__typename: 'Signer',
signer: {
__typename: 'PubKey',
key: '123',
key: '1234567891234567789123456789123456778912345678912345677891234567',
},
},
],
@@ -4,6 +4,7 @@ import {
EthExplorerLinkTypes,
} from '../../../components/links/eth-explorer-link/eth-explorer-link';
import { TableRow, TableCell, TableHeader } from '../../../components/table';
import { remove0x } from '@vegaprotocol/react-helpers';
import type { SourceType } from './oracle';
@@ -14,7 +15,15 @@ export type Signer = {
};
export function getAddressTypeLabel(signer: Signer) {
return signer.__typename === 'ETHAddress' ? 'ETH' : 'Vega';
const res = signer.__typename === 'ETHAddress' ? 'ETH' : 'Vega';
// This is a hack: some older oracles were submitted before proper checks stopped
// ETH addresses being returned as Vega addresses
if (res === 'Vega' && signer?.key?.length !== 64) {
return 'ETH';
} else {
return res;
}
}
export function getAddress(signer: Signer) {
@@ -29,6 +38,16 @@ export function getAddressLink(signer: Signer) {
if (signer.__typename === 'ETHAddress') {
return <EthExplorerLink id={address} type={EthExplorerLinkTypes.address} />;
} else if (signer.__typename === 'PubKey' && address.length !== 64) {
// This is a hack: some older oracles were submitted before proper checks stopped
// ETH addresses being returned as Vega addresses
// Hacky 0x prefixing as a bonus
return (
<EthExplorerLink
id={`0x${remove0x(address)}`}
type={EthExplorerLinkTypes.address}
/>
);
} else if (signer.__typename === 'PubKey') {
return <PartyLink id={address} />;
}
@@ -13,6 +13,8 @@ import { OracleData } from './oracle-data';
import { OracleFilter } from './oracle-filter';
import { OracleDetailsType } from './oracle-details-type';
import { OracleMarkets } from './oracle-markets';
import { OracleSigners } from './oracle-signers';
import OracleLink from '../../../components/links/oracle-link/oracle-link';
export type SourceType =
ExplorerOracleDataSourceFragment['dataSourceSpec']['spec']['data']['sourceType'];
@@ -21,36 +23,47 @@ interface OracleDetailsProps {
id: string;
dataSource: ExplorerOracleDataSourceFragment;
dataConnection: ExplorerOracleDataConnectionFragment;
// Defaults to false. Hides the count of 'broadcasts' this oracle has seen
showBroadcasts?: boolean;
}
/**
* Notes:
* - Matched data is really 'Data that matched this oracle' and given oracles are unique
* to each market, and each serves either as trading termination or settlement, really
* they will only ever see 1 match (most likely). So it should be more like 'Has seen
* data' vs 'Has not yet seen data'
*/
export const OracleDetails = ({
id,
dataSource,
dataConnection,
showBroadcasts = false,
}: OracleDetailsProps) => {
const sourceType = dataSource.dataSourceSpec.spec.data.sourceType;
const reportsCount: number = dataConnection.dataConnection.edges?.length || 0;
return (
<div>
<TableWithTbody>
<TableWithTbody className="mb-2">
<TableRow modifier="bordered">
<TableHeader scope="row">{t('ID')}</TableHeader>
<TableCell modifier="bordered">{id}</TableCell>
<TableCell modifier="bordered">
<OracleLink id={id} />
</TableCell>
</TableRow>
<OracleDetailsType type={sourceType.__typename} />
{
// Disabled until https://github.com/vegaprotocol/vega/issues/7286 is released
/*<OracleSigners sourceType={sourceType} />*/
}
<OracleSigners sourceType={sourceType} />
<OracleMarkets id={id} />
<TableRow modifier="bordered">
<TableHeader scope="row">{t('Broadcasts')}</TableHeader>
<TableCell modifier="bordered">{reportsCount}</TableCell>
<TableHeader scope="row">{t('Matched data')}</TableHeader>
<TableCell modifier="bordered">
{showBroadcasts ? reportsCount : reportsCount > 0 ? '✅' : '❌'}
</TableCell>
</TableRow>
</TableWithTbody>
<OracleFilter data={dataSource} />
<OracleData data={dataConnection} />
{showBroadcasts ? <OracleData data={dataConnection} /> : null}
</div>
);
};
@@ -0,0 +1,45 @@
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { RouteTitle } from '../../../components/route-title';
import { t } from '@vegaprotocol/react-helpers';
import { useExplorerOracleSpecsQuery } from '../__generated__/Oracles';
import { useDocumentTitle } from '../../../hooks/use-document-title';
import { OracleDetails } from '../components/oracle';
import { useScrollToLocation } from '../../../hooks/scroll-to-location';
import filter from 'recursive-key-filter';
const Oracles = () => {
const { data, loading } = useExplorerOracleSpecsQuery();
useDocumentTitle(['Oracles']);
useScrollToLocation();
return (
<section>
<RouteTitle data-testid="oracle-specs-heading">{t('Oracles')}</RouteTitle>
{loading ? <Loader /> : null}
{data?.oracleSpecsConnection?.edges
? data.oracleSpecsConnection.edges.map((o) => {
const id = o?.node.dataSourceSpec.spec.id;
if (!id) {
return null;
}
return (
<div id={id} key={id} className="mb-10">
<OracleDetails
id={id}
dataSource={o?.node}
dataConnection={o?.node}
/>
<details>
<summary className="pointer">JSON</summary>
<SyntaxHighlighter data={filter(o, ['__typename'])} />
</details>
</div>
);
})
: null}
</section>
);
};
export default Oracles;
@@ -0,0 +1,49 @@
import { RouteTitle } from '../../../components/route-title';
import { RenderFetched } from '../../../components/render-fetched';
import { t, truncateByChars } from '@vegaprotocol/react-helpers';
import { useDocumentTitle } from '../../../hooks/use-document-title';
import { useParams } from 'react-router-dom';
import { useExplorerOracleSpecByIdQuery } from '../__generated__/Oracles';
import { OracleDetails } from '../components/oracle';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import filter from 'recursive-key-filter';
import { TruncateInline } from '../../../components/truncate/truncate';
export const Oracle = () => {
const { id } = useParams<{ id: string }>();
useDocumentTitle(['Oracle', `Oracle #${truncateByChars(id || '1', 5, 5)}`]);
const { data, error, loading } = useExplorerOracleSpecByIdQuery({
variables: {
id: id || '1',
},
});
return (
<section>
<RouteTitle data-testid="block-header">
{t(`Oracle `)}
<TruncateInline startChars={5} endChars={5} text={id || '1'} />
</RouteTitle>
<RenderFetched error={error} loading={loading}>
{data?.oracleSpec ? (
<div id={id} key={id} className="mb-10">
<OracleDetails
id={id || ''}
dataSource={data?.oracleSpec}
dataConnection={data?.oracleSpec}
showBroadcasts={true}
/>
<details>
<summary className="pointer">JSON</summary>
<SyntaxHighlighter data={filter(data, ['__typename'])} />
</details>
</div>
) : (
<span></span>
)}
</RenderFetched>
</section>
);
};
+4 -42
View File
@@ -1,45 +1,7 @@
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { RouteTitle } from '../../components/route-title';
import { t } from '@vegaprotocol/react-helpers';
import { useExplorerOracleSpecsQuery } from './__generated__/Oracles';
import { useDocumentTitle } from '../../hooks/use-document-title';
import { OracleDetails } from './components/oracle';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import filter from 'recursive-key-filter';
import { Outlet } from 'react-router-dom';
const Oracles = () => {
const { data, loading } = useExplorerOracleSpecsQuery();
useDocumentTitle(['Oracles']);
useScrollToLocation();
return (
<section>
<RouteTitle data-testid="oracle-specs-heading">{t('Oracles')}</RouteTitle>
{loading ? <Loader /> : null}
{data?.oracleSpecsConnection?.edges
? data.oracleSpecsConnection.edges.map((o) => {
const id = o?.node.dataSourceSpec.spec.id;
if (!id) {
return null;
}
return (
<div id={id} key={id} className="mb-10 cursor-pointer">
<OracleDetails
id={id}
dataSource={o?.node}
dataConnection={o?.node}
/>
<details>
<summary className="pointer">JSON</summary>
<SyntaxHighlighter data={filter(o, ['__typename'])} />
</details>
</div>
);
})
: null}
</section>
);
const OraclePage = () => {
return <Outlet />;
};
export default Oracles;
export default OraclePage;
+19 -13
View File
@@ -3,7 +3,9 @@ import BlockPage from './blocks';
import Governance from './governance';
import Home from './home';
import Markets from './markets';
import Oracles from './oracles';
import OraclePage from './oracles';
import Oracles from './oracles/home';
import { Oracle } from './oracles/id';
import Party from './parties';
import { Parties } from './parties/home';
import { Party as PartySingle } from './parties/id';
@@ -85,17 +87,6 @@ const marketsRoutes = flags.markets
]
: [];
const oraclesRoutes = flags.oracles
? [
{
path: Routes.ORACLES,
name: 'Oracles',
text: t('Oracles'),
element: <Oracles />,
},
]
: [];
const networkParametersRoutes = flags.networkParameters
? [
{
@@ -161,12 +152,27 @@ const routerConfig = [
},
],
},
{
path: Routes.ORACLES,
name: 'Oracles',
text: t('Oracles'),
element: <OraclePage />,
children: [
{
index: true,
element: <Oracles />,
},
{
path: ':id',
element: <Oracle />,
},
],
},
...partiesRoutes,
...assetsRoutes,
...genesisRoutes,
...governanceRoutes,
...marketsRoutes,
...oraclesRoutes,
...networkParametersRoutes,
...validators,
];
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -97,4 +97,4 @@ export function useRewardsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Re
}
export type RewardsQueryHookResult = ReturnType<typeof useRewardsQuery>;
export type RewardsLazyQueryHookResult = ReturnType<typeof useRewardsLazyQuery>;
export type RewardsQueryResult = Apollo.QueryResult<RewardsQuery, RewardsQueryVariables>;
export type RewardsQueryResult = Apollo.QueryResult<RewardsQuery, RewardsQueryVariables>;
+9 -2
View File
@@ -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 (
+1
View File
@@ -14,6 +14,7 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
# Expose some env vars to cypress environment for market setup
CYPRESS_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
CYPRESS_ETHEREUM_WALLET_ADDRESS=0xEe7D375bcB50C26d52E1A4a472D8822A2A22d94F
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
CYPRESS_EXPLORER_URL=https://explorer.fairground.wtf
CYPRESS_FAUCET_URL=http://localhost:1790/api/v1/mint
+1
View File
@@ -14,6 +14,7 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
# Expose some env vars to cypress environment for market setup
CYPRESS_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
CYPRESS_ETHEREUM_WALLET_ADDRESS=0xEe7D375bcB50C26d52E1A4a472D8822A2A22d94F
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
CYPRESS_EXPLORER_URL=https://explorer.fairground.wtf
CYPRESS_FAUCET_URL=http://localhost:1790/api/v1/mint
+3 -1
View File
@@ -10,7 +10,9 @@ NX_VEGA_TOKEN_URL=https://stagnet3.token.vega.xyz
NX_VEGA_URL=https://api.stagnet3.vega.xyz/graphql
NX_VEGA_WALLET_URL=http://localhost:1789
CYPRESS_ETH_WALLET_MNEMONIC=ugly gallery notice network true range brave clarify flat logic someone chunk
CYPRESS_ETH_WALLET_MNEMONIC=
CYPRESS_ETHEREUM_WALLET_ADDRESS=0xEe7D375bcB50C26d52E1A4a472D8822A2A22d94F
CYPRESS_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
CYPRESS_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY=1a18cd…0cf2e4
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY2=47836c…c7d278
-11
View File
@@ -24,19 +24,8 @@ module.exports = defineConfig({
requestTimeout: 20000,
},
env: {
VEGA_PUBLIC_KEY:
'47836c253520d2661bf5bed6339c0de08fd02cf5d4db0efee3b4373f20c7d278',
VEGA_PUBLIC_KEY2:
'1a18cdcaaa4f44a57b35a4e9b77e0701c17a476f2b407620f8c17371740cf2e4',
TRUNCATED_VEGA_PUBLIC_KEY: '47836c…c7d278',
TRUNCATED_VEGA_PUBLIC_KEY2: '1a18cd…0cf2e4',
ETHEREUM_PROVIDER_URL:
'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8',
ETHEREUM_WALLET_ADDRESS: '0x265Cc6d39a1B53d0d92068443009eE7410807158',
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
ETHEREUM_CHAIN_ID: 11155111,
ETH_WALLET_MNEMONIC:
'ugly gallery notice network true range brave clarify flat logic someone chunk',
TRADING_MODE_LINK:
'https://docs.vega.xyz/testnet/concepts/trading-on-vega/trading-modes#auction-type-liquidity-monitoring',
grepTags: '@regression @smoke @slow',
+66 -41
View File
@@ -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)
@@ -0,0 +1,99 @@
import { mockConnectWallet } from '@vegaprotocol/cypress';
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
cy.wait('@Market');
cy.getByTestId('dialog-close').click();
});
describe('Desktop view', { tags: '@smoke' }, () => {
describe('Navbar', () => {
const links = ['Markets', 'Trading', 'Portfolio'];
const hashes = ['#/markets/all', '#/markets/market-0', '#/portfolio'];
links.forEach((link, index) => {
it(`${link} should be correctly rendered`, () => {
cy.getByTestId('navbar')
.find(`[data-testid="navbar-links"] a[data-testid=${link}]`)
.then((element) => {
cy.wrap(element).click();
cy.wrap(element)
.get('span.absolute.md\\:h-1.w-full')
.should('exist');
cy.location('hash').should('equal', hashes[index]);
});
});
});
});
});
describe('Mobile view', { tags: '@smoke' }, () => {
const viewportHeight = Cypress.config('viewportHeight');
const viewportWidth = Cypress.config('viewportWidth');
before(() => {
// a little hack to keep the viewport size between tests (cypress bug)
Cypress.config({
viewportWidth: 560,
viewportHeight: 890,
});
cy.viewport(560, 890);
});
describe('wallet drawer', () => {
it('wallet drawer should be correctly rendered', () => {
mockConnectWallet();
cy.connectVegaWallet(true);
cy.getByTestId('connect-vega-wallet-mobile').click();
cy.getByTestId('wallets-drawer').should('be.visible');
cy.getByTestId('wallets-drawer').within((el) => {
cy.wrap(el).get('button').contains('Disconnect').click();
});
cy.getByTestId('wallets-drawer').should('not.be.visible');
});
});
describe('menu drawer', () => {
it('Markets should be correctly rendered', () => {
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('be.visible');
cy.getByTestId('menu-drawer').within((el) => {
cy.wrap(el).getByTestId('Markets').click();
cy.location('hash').should('equal', '#/markets/all');
});
});
it('Trading should be correctly rendered', () => {
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').within((el) => {
cy.wrap(el).getByTestId('Trading').click();
cy.location('hash').should('equal', '#/markets/market-0');
});
});
it('Portfolio should be correctly rendered', () => {
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').within((el) => {
cy.wrap(el).getByTestId('Portfolio').click();
cy.location('hash').should('equal', '#/portfolio');
});
});
it('Menu drawer should not be visible until opened', () => {
cy.getByTestId('menu-drawer').should('not.be.visible');
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('be.visible');
cy.getByTestId('menu-drawer')
.find('[data-testid="theme-switcher"]')
.should('be.visible');
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('not.be.visible');
});
});
after(() => {
// a little hack to keep the viewport size between tests (cypress bug)
Cypress.config({
viewportWidth,
viewportHeight,
});
});
});
@@ -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
});
});
@@ -7,7 +7,7 @@ const manageVegaBtn = 'manage-vega-wallet';
const form = 'rest-connector-form';
const dialogContent = 'dialog-content';
describe('vega wallet v1', { tags: '@smoke' }, () => {
describe('connect hosted wallet', { tags: '@smoke' }, () => {
beforeEach(() => {
// Using portfolio page as it requires vega wallet connection
cy.visit('/#/portfolio');
@@ -58,7 +58,7 @@ describe('vega wallet v1', { tags: '@smoke' }, () => {
});
});
describe('vega wallet v2', { tags: '@smoke' }, () => {
describe('connect vega wallet', { tags: '@smoke' }, () => {
beforeEach(() => {
// Using portfolio page as it requires vega wallet connection
cy.visit('/#/portfolio');
@@ -127,63 +127,3 @@ describe('ethereum wallet', { tags: '@smoke' }, () => {
cy.getByTestId(connectEthWalletBtn).should('exist');
});
});
describe('Navbar', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
cy.wait('@Market');
cy.getByTestId('dialog-close').click();
});
it('should be properly rendered', () => {
const links = ['Markets', 'Trading', 'Portfolio'];
const hashes = ['#/markets/all', '#/markets/market-0', '#/portfolio'];
let i = 0;
cy.getByTestId('navbar').within(() => {
cy.get('[data-testid="navbar-links"] a[data-testid]', { log: true })
.should('have.length', 3)
.each((item) => {
cy.wrap(item).click();
cy.wrap(item).get('span.absolute.md\\:h-1.w-full').should('exist');
cy.location('hash').should('equal', hashes[i]);
cy.wrap(item).should('have.data', 'testid', links[i++]);
});
});
});
it('wallet drawer should be correctly rendered', () => {
cy.viewport(560, 890);
mockConnectWallet();
cy.connectVegaWallet(true);
cy.getByTestId('connect-vega-wallet-mobile').click();
cy.getByTestId('wallets-drawer').should('be.visible');
cy.getByTestId('wallets-drawer').within((el) => {
cy.wrap(el).get('button').contains('Disconnect').click();
});
cy.getByTestId('wallets-drawer').should('not.be.visible');
});
it('menu drawer should be correctly rendered', () => {
cy.viewport(560, 890);
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('be.visible');
cy.getByTestId('menu-drawer').within((el) => {
cy.wrap(el).getByTestId('Markets').click();
cy.location('hash').should('equal', '#/markets/all');
});
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').within((el) => {
cy.wrap(el).getByTestId('Trading').click();
cy.location('hash').should('equal', '#/markets/market-0');
});
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').within((el) => {
cy.wrap(el).getByTestId('Portfolio').click();
cy.location('hash').should('equal', '#/portfolio');
cy.wrap(el).getByTestId('theme-switcher').should('be.visible');
});
cy.getByTestId('menu-drawer').should('not.be.visible');
});
});
+12 -2
View File
@@ -7,7 +7,7 @@ import {
useDataProvider,
useThrottledDataProvider,
} from '@vegaprotocol/react-helpers';
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
import type {
SingleMarketFieldsFragment,
MarketData,
@@ -113,7 +113,17 @@ export const MarketPage = () => {
if (!data && marketId) {
return (
<Splash>
<p>{t('Market not found')}</p>
<span className="flex flex-col items-center gap-2">
<p className="text-sm justify-center">
{t('This market URL is not available anymore.')}
</p>
<p className="text-sm justify-center">
{t(`Please choose another market from the`)}{' '}
<ExternalLink onClick={() => navigate(Links[Routes.MARKETS]())}>
market list
</ExternalLink>
</p>
</span>
</Splash>
);
}
@@ -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 (
+38 -6
View File
@@ -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();
}
);
});
+56 -9
View File
@@ -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}
@@ -84,7 +84,7 @@ export const SelectMarketLandingTable = ({
data-testid="view-market-list-link"
onClick={() => onClose()}
>
<UILink className="uppercase underline">
<UILink className="text-sm underline">
{'Or view full market list'}
</UILink>
</Link>
@@ -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'] => {
+35 -33
View File
@@ -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)
);
};
+3 -8
View File
@@ -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,
});
+3 -3
View File
@@ -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
>({
@@ -12,6 +12,7 @@ query Candles($marketId: ID!, $interval: Interval!, $since: String!) {
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
id
+1
View File
@@ -1,6 +1,7 @@
query Chart($marketId: ID!) {
market(id: $marketId) {
decimalPlaces
positionDecimalPlaces
data {
priceMonitoringBounds {
minValidPrice
+2 -1
View File
@@ -12,7 +12,7 @@ export type CandlesQueryVariables = Types.Exact<{
}>;
export type CandlesQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string } }, candlesConnection?: { __typename?: 'CandleDataConnection', edges?: Array<{ __typename?: 'CandleEdge', node: { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string } } | null> | null } | null } | null };
export type CandlesQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string } }, candlesConnection?: { __typename?: 'CandleDataConnection', edges?: Array<{ __typename?: 'CandleEdge', node: { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string } } | null> | null } | null } | null };
export type CandlesEventsSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
@@ -38,6 +38,7 @@ export const CandlesDocument = gql`
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
id
+2 -1
View File
@@ -8,13 +8,14 @@ export type ChartQueryVariables = Types.Exact<{
}>;
export type ChartQuery = { __typename?: 'Query', market?: { __typename?: 'Market', decimalPlaces: number, data?: { __typename?: 'MarketData', priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string }> | null } | null } | null };
export type ChartQuery = { __typename?: 'Query', market?: { __typename?: 'Market', decimalPlaces: number, positionDecimalPlaces: number, data?: { __typename?: 'MarketData', priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string }> | null } | null } | null };
export const ChartDocument = gql`
query Chart($marketId: ID!) {
market(id: $marketId) {
decimalPlaces
positionDecimalPlaces
data {
priceMonitoringBounds {
minValidPrice
@@ -14,6 +14,7 @@ export const candlesQuery = (
market: {
id: 'market-0',
decimalPlaces: 5,
positionDecimalPlaces: 0,
tradableInstrument: {
instrument: {
id: '',
+1
View File
@@ -20,6 +20,7 @@ export const chartQuery = (override?: PartialDeep<ChartQuery>): ChartQuery => {
const defaultResult: ChartQuery = {
market: {
decimalPlaces: 5,
positionDecimalPlaces: 0,
data: {
priceMonitoringBounds: [priceMonitoringBound],
__typename: 'MarketData',
+23 -4
View File
@@ -49,6 +49,7 @@ export class VegaDataSource implements DataSource {
marketId: string;
partyId: null | string;
_decimalPlaces = 0;
_positionDecimalPlaces = 0;
candlesSub: Subscription | null = null;
@@ -60,6 +61,14 @@ export class VegaDataSource implements DataSource {
return this._decimalPlaces;
}
/**
* Indicates the number of position decimal places that an integer must be shifted by in order to get a correct
* number denominated in the unit size of the Market.
*/
get positionDecimalPlaces(): number {
return this._positionDecimalPlaces;
}
/**
*
* @param client - An ApolloClient instance.
@@ -93,9 +102,11 @@ export class VegaDataSource implements DataSource {
if (data && data.market && data.market.data) {
this._decimalPlaces = data.market.decimalPlaces;
this._positionDecimalPlaces = data.market.positionDecimalPlaces;
return {
decimalPlaces: this._decimalPlaces,
positionDecimalPlaces: this._positionDecimalPlaces,
supportedIntervals: [
PennantInterval.I1D,
PennantInterval.I6H,
@@ -145,11 +156,14 @@ export class VegaDataSource implements DataSource {
if (data?.market?.candlesConnection?.edges) {
const decimalPlaces = data.market.decimalPlaces;
const positionDecimalPlaces = data.market.positionDecimalPlaces;
const candles = data.market.candlesConnection.edges
.map((edge) => edge?.node)
.filter((node): node is CandleFieldsFragment => !!node)
.map((node) => parseCandle(node, decimalPlaces));
.map((node) =>
parseCandle(node, decimalPlaces, positionDecimalPlaces)
);
return candles;
} else {
@@ -180,7 +194,11 @@ export class VegaDataSource implements DataSource {
this.candlesSub = res.subscribe(({ data }) => {
if (data) {
const candle = parseCandle(data.candles, this.decimalPlaces);
const candle = parseCandle(
data.candles,
this.decimalPlaces,
this.positionDecimalPlaces
);
onSubscriptionData(candle);
}
@@ -197,7 +215,8 @@ export class VegaDataSource implements DataSource {
function parseCandle(
candle: CandleFieldsFragment,
decimalPlaces: number
decimalPlaces: number,
positionDecimalPlaces: number
): Candle {
return {
date: new Date(candle.periodStart),
@@ -205,6 +224,6 @@ function parseCandle(
low: Number(addDecimal(candle.low, decimalPlaces)),
open: Number(addDecimal(candle.open, decimalPlaces)),
close: Number(addDecimal(candle.close, decimalPlaces)),
volume: Number(candle.volume),
volume: Number(addDecimal(candle.volume, positionDecimalPlaces)),
};
}
@@ -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');
},
});
+1 -1
View File
@@ -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';
-100
View File
@@ -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
View File
@@ -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,
});
});
});
+16 -4
View File
@@ -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);
};

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