Compare commits

...
Author SHA1 Message Date
Bartłomiej Głownia a4793c4743 fix: hide close button if openVolume is zero 2023-01-27 19:17:41 +01: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
98 changed files with 1881 additions and 712 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"
]
}
},
@@ -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,4 +1,3 @@
import React from 'react';
import { Routes } from '../../../routes/route-names';
import { useExplorerMarketQuery } from './__generated__/Market';
import { Link } from 'react-router-dom';
@@ -8,6 +7,7 @@ import { t } from '@vegaprotocol/react-helpers';
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
showMarketName?: boolean;
};
/**
@@ -15,7 +15,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 },
});
@@ -37,11 +41,24 @@ const MarketLink = ({ id, ...props }: MarketLinkProps) => {
}
}
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}`}>
{id}
</Link>
);
}
};
export default MarketLink;
@@ -0,0 +1,22 @@
import { Routes } from '../../../routes/route-names';
import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
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}`}
>
{id}
</Link>
);
};
export default OracleLink;
@@ -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,85 @@
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 with an emoji 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();
// The emoji
expect(await res.findByRole('img')).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,23 @@
import { useExplorerProposalQuery } from './__generated__/Proposal';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ENV } from '../../../config/env';
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}`}>{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(
@@ -20,6 +20,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;
@@ -91,6 +92,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>
@@ -50,6 +50,12 @@ export const TxDetailsOrder = ({
<code>{deterministicId}</code>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Market ID')}</TableCell>
<TableCell>
<MarketLink id={marketId} showMarketName={false} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
@@ -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>
);
};
+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,
];
+209 -52
View File
@@ -5,7 +5,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "86666.297",
"total_removed": "0",
"locked_amount": "74199.563434561395345826",
"locked_amount": "73961.6625805406210086218",
"deposits": [
{
"amount": "86666.297",
@@ -71,7 +71,7 @@
"tranche_end": "2023-06-01T00:00:00.000Z",
"total_added": "2500",
"total_removed": "0",
"locked_amount": "1723.8413207163205",
"locked_amount": "1710.0785065628815",
"deposits": [
{
"amount": "2500",
@@ -450,7 +450,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "129999.45",
"total_removed": "0",
"locked_amount": "74131.865669074416617715",
"locked_amount": "73894.181869649098296045",
"deposits": [
{
"amount": "129999.45",
@@ -516,7 +516,7 @@
"tranche_end": "2023-09-03T00:00:00.000Z",
"total_added": "62600",
"total_removed": "0",
"locked_amount": "37645.00705225773502",
"locked_amount": "37473.16870243530666",
"deposits": [
{
"amount": "10000",
@@ -709,7 +709,7 @@
"tranche_end": "2023-09-17T00:00:00.000Z",
"total_added": "5000",
"total_removed": "0",
"locked_amount": "3198.570522577372",
"locked_amount": "3184.845414764079",
"deposits": [
{
"amount": "5000",
@@ -920,7 +920,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "97499.58",
"total_removed": "0",
"locked_amount": "15938.65614862105375962",
"locked_amount": "15705.5104826322582056334",
"deposits": [
{
"amount": "97499.58",
@@ -953,7 +953,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "135173.4239508",
"total_removed": "98230.390980249184455396",
"locked_amount": "21785.391260975192260443858312",
"locked_amount": "21466.721386488566701750705764",
"deposits": [
{
"amount": "135173.4239508",
@@ -999,7 +999,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "32499.86",
"total_removed": "0",
"locked_amount": "6705.1173958556428760904",
"locked_amount": "6607.0370403844561078578",
"deposits": [
{
"amount": "32499.86",
@@ -1032,7 +1032,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "10833.29",
"total_removed": "0",
"locked_amount": "2182.4506474319168306297",
"locked_amount": "2150.5264434752857381272",
"deposits": [
{
"amount": "10833.29",
@@ -1065,7 +1065,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "22749.93",
"total_removed": "0",
"locked_amount": "8158.48796164921464204",
"locked_amount": "8039.148157117145325033",
"deposits": [
{
"amount": "6500",
@@ -1204,7 +1204,7 @@
"tranche_end": "2023-05-01T00:00:00.000Z",
"total_added": "22500",
"total_removed": "3539.640404325",
"locked_amount": "11746.696593001842",
"locked_amount": "11622.146926795578",
"deposits": [
{
"amount": "7500",
@@ -1369,7 +1369,7 @@
"tranche_end": "2023-06-02T00:00:00.000Z",
"total_added": "1939928.38",
"total_removed": "928642.9598472029154",
"locked_amount": "672308.21312078634624765",
"locked_amount": "666983.06788767317384942",
"deposits": [
{
"amount": "1852091.69",
@@ -1721,7 +1721,7 @@
"tranche_end": "2023-02-01T00:00:00.000Z",
"total_added": "42500",
"total_removed": "24434.0787288",
"locked_amount": "1269.3752516103057225",
"locked_amount": "1037.950539704105075",
"deposits": [
{
"amount": "12500",
@@ -6711,7 +6711,7 @@
"tranche_id": 11,
"tranche_start": "2021-09-03T00:00:00.000Z",
"tranche_end": "2022-09-03T00:00:00.000Z",
"total_added": "53657.000000000000000003",
"total_added": "53684.000000000000000003",
"total_removed": "42606.21518131551",
"locked_amount": "0",
"deposits": [
@@ -6825,6 +6825,16 @@
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
"tx": "0xe3fd6d538990df7bf818b8a178ecf7135a5e34f4d07778e9d6dc637cf470e2e0"
},
{
"amount": "12",
"user": "0xeD96EAA7D15951ab128abF8918a4D69F0Eef9905",
"tx": "0x3775c114790b9a57414975492eec548528a155927399bddc72c40e336f3c9f52"
},
{
"amount": "15",
"user": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
"tx": "0xa83f849c425b63fe042be7d606eaaf64f9e89c20c01aa5ec58a3ebf54c573eba"
},
{
"amount": "20",
"user": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
@@ -17680,6 +17690,12 @@
"tranche_id": 11,
"tx": "0x304ceeda11c67174ffc52347ec240ea5aa832675cbb1c828adc098c8886f96fc"
},
{
"amount": "15",
"user": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
"tranche_id": 11,
"tx": "0xa83f849c425b63fe042be7d606eaaf64f9e89c20c01aa5ec58a3ebf54c573eba"
},
{
"amount": "20",
"user": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
@@ -18367,9 +18383,9 @@
"tx": "0x6940787f6ceaac0846e69f1bc93ad9efee8c39774b91ea29f84328ae64fc9969"
}
],
"total_tokens": "2585",
"total_tokens": "2600",
"withdrawn_tokens": "2585",
"remaining_tokens": "0"
"remaining_tokens": "15"
},
{
"address": "0xE9F41a0090fcc7eaf626037003AAD44B17098E7C",
@@ -18591,6 +18607,21 @@
"withdrawn_tokens": "124",
"remaining_tokens": "0"
},
{
"address": "0xeD96EAA7D15951ab128abF8918a4D69F0Eef9905",
"deposits": [
{
"amount": "12",
"user": "0xeD96EAA7D15951ab128abF8918a4D69F0Eef9905",
"tranche_id": 11,
"tx": "0x3775c114790b9a57414975492eec548528a155927399bddc72c40e336f3c9f52"
}
],
"withdrawals": [],
"total_tokens": "12",
"withdrawn_tokens": "0",
"remaining_tokens": "12"
},
{
"address": "0xc7eC0d77a1417C97fb455f404c4B425A69a0004d",
"deposits": [
@@ -33097,7 +33128,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "3732368.4671",
"total_removed": "442882.3484327902809",
"locked_amount": "1057604.97543725296329993117",
"locked_amount": "1049422.081232611329856667873",
"deposits": [
{
"amount": "1998.95815",
@@ -34388,8 +34419,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "15870102.715470999700000001",
"total_removed": "545536.65519596862040452",
"locked_amount": "9049886.9238116721047082457828299231170587",
"total_removed": "547359.63874938618402952",
"locked_amount": "9020870.9063540351013617047345897584630981",
"deposits": [
{
"amount": "16249.93",
@@ -34998,6 +35029,16 @@
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
"tx": "0x33446fce935f9568b7d6e796e6a9a73049585fc7225f935e343acbb5c68a60b7"
},
{
"amount": "869.931062",
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
"tx": "0xc6a7b120dc629017c645e277e57c96fec82c78b518c0b54809dbc10f566e204d"
},
{
"amount": "953.052491417563625",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x74f9a06c6e8b7807b9c369ec3762e5d4b518f32303faf8c1d4098f9d33f4a997"
},
{
"amount": "434.254023685890375",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -36532,6 +36573,12 @@
"tranche_id": 2,
"tx": "0x6d913b124af23621f8b97087a51d4cd912c6a8d9cca325cb756b56d4970f3f62"
},
{
"amount": "953.052491417563625",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0x74f9a06c6e8b7807b9c369ec3762e5d4b518f32303faf8c1d4098f9d33f4a997"
},
{
"amount": "434.254023685890375",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -37548,8 +37595,8 @@
}
],
"total_tokens": "259998.8875",
"withdrawn_tokens": "111222.20209807200725",
"remaining_tokens": "148776.68540192799275"
"withdrawn_tokens": "112175.254589489570875",
"remaining_tokens": "147823.632910510429125"
},
{
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
@@ -38197,6 +38244,12 @@
"tranche_id": 2,
"tx": "0x108f2ebc78fb2f656e95e842a65a0c522e7ad6b6b06b158dbd549831e63814c2"
},
{
"amount": "869.931062",
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
"tranche_id": 2,
"tx": "0xc6a7b120dc629017c645e277e57c96fec82c78b518c0b54809dbc10f566e204d"
},
{
"amount": "1573.9558",
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
@@ -38349,8 +38402,8 @@
}
],
"total_tokens": "200000",
"withdrawn_tokens": "85146.031866",
"remaining_tokens": "114853.968134"
"withdrawn_tokens": "86015.962928",
"remaining_tokens": "113984.037072"
},
{
"address": "0x1b956E6c00E238194B331eddEFF72Cb5f28A8d01",
@@ -39690,8 +39743,8 @@
"tranche_start": "2021-11-05T00:00:00.000Z",
"tranche_end": "2023-05-05T00:00:00.000Z",
"total_added": "14597706.0446472999",
"total_removed": "3706973.15022981060693393",
"locked_amount": "2633352.597699040729631521134098664",
"total_removed": "3708107.46955489707527943",
"locked_amount": "2606565.19565117133351441770201319",
"deposits": [
{
"amount": "129284.449",
@@ -39990,6 +40043,11 @@
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xc1e8ef457dbeb52bd2d02aefd418a9a67bd17f016c1ca6a1e80716d86ce3c132"
},
{
"amount": "1134.3193250864683455",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xfb5cc9685a3c4f5174ff68cc066d8da5121f833d7539f53a78db46c2427eda9b"
},
{
"amount": "12758.73908063512839093",
"user": "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E",
@@ -42768,6 +42826,12 @@
"tranche_id": 3,
"tx": "0xc1e8ef457dbeb52bd2d02aefd418a9a67bd17f016c1ca6a1e80716d86ce3c132"
},
{
"amount": "1134.3193250864683455",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0xfb5cc9685a3c4f5174ff68cc066d8da5121f833d7539f53a78db46c2427eda9b"
},
{
"amount": "762.7902369187144135",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -45080,8 +45144,8 @@
}
],
"total_tokens": "359123.469575",
"withdrawn_tokens": "293815.097748589450163",
"remaining_tokens": "65308.371826410549837"
"withdrawn_tokens": "294949.4170736759185085",
"remaining_tokens": "64174.0525013240814915"
},
{
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
@@ -46392,7 +46456,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "5778205.3912159303",
"total_removed": "2622261.560853924298939789",
"locked_amount": "723550.13419462381518165594274868",
"locked_amount": "712966.269636651830718004793872006",
"deposits": [
{
"amount": "552496.6455",
@@ -48286,8 +48350,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "472355.6199999996",
"total_removed": "29683.1054262326685",
"locked_amount": "167583.553885809073086558333536292",
"total_removed": "30192.1031304386685",
"locked_amount": "166286.92752366611650390355859972",
"deposits": [
{
"amount": "3000",
@@ -54961,6 +55025,31 @@
"user": "0xf751033D4e6864a88Be93E36258246F26AEf577c",
"tx": "0xb912d9e24a80ec0d9ed4f3b03c20937d4fec08dd99031447ba84a8b6a69ef351"
},
{
"amount": "39.564687976",
"user": "0xD27929d68ac0E5fd5C919A5eb5968C1D06D3Fb83",
"tx": "0x63c2319241d05630a6d9ea104770cdcb6d93d145554bf2ff9d82cd8ef4e17fc7"
},
{
"amount": "129.409734906",
"user": "0x6E624B5788CD721F489074993d5E1Ee2C477eB00",
"tx": "0x9b25e6fb021dd73d4022c6879da50a0e2b818a46955153a90e44008f5f4f5a52"
},
{
"amount": "129.527999746",
"user": "0x4683B77D114a04f1602dda7B0Ab2A24Fe79295fD",
"tx": "0x0a2ab8841a315a8f2bcf534953ff9058607709c1b761f4fded8ba238ea3f3802"
},
{
"amount": "37.881582952",
"user": "0xcB555C5602cC0b2434A27d277e14Cb5C7bF4Ead1",
"tx": "0x56e6ab9b05da1003e1070f9ffb5cc54038420d69751d49f700bdb008f8600701"
},
{
"amount": "172.613698626",
"user": "0x83e600Ae7f4cf265C112314839cDA2341198840B",
"tx": "0x871fbcbfe5c488654b8aff67f1c4afff2f5940ea3eb0cdf165b159bb96ee7ffd"
},
{
"amount": "229.648541348",
"user": "0x65C13724928ea0AfA68e09c7b70449bB8f6f3Fc8",
@@ -56391,6 +56480,12 @@
}
],
"withdrawals": [
{
"amount": "172.613698626",
"user": "0x83e600Ae7f4cf265C112314839cDA2341198840B",
"tranche_id": 5,
"tx": "0x871fbcbfe5c488654b8aff67f1c4afff2f5940ea3eb0cdf165b159bb96ee7ffd"
},
{
"amount": "87.108637746",
"user": "0x83e600Ae7f4cf265C112314839cDA2341198840B",
@@ -56405,8 +56500,8 @@
}
],
"total_tokens": "600",
"withdrawn_tokens": "216.146099694",
"remaining_tokens": "383.853900306"
"withdrawn_tokens": "388.75979832",
"remaining_tokens": "211.24020168"
},
{
"address": "0x5Ef3F2723f8e8c45aF15Df67dBDfA8a8FF2C3E8F",
@@ -72659,6 +72754,12 @@
}
],
"withdrawals": [
{
"amount": "37.881582952",
"user": "0xcB555C5602cC0b2434A27d277e14Cb5C7bF4Ead1",
"tranche_id": 5,
"tx": "0x56e6ab9b05da1003e1070f9ffb5cc54038420d69751d49f700bdb008f8600701"
},
{
"amount": "40.914155252",
"user": "0xcB555C5602cC0b2434A27d277e14Cb5C7bF4Ead1",
@@ -72673,8 +72774,8 @@
}
],
"total_tokens": "400",
"withdrawn_tokens": "221.208206492",
"remaining_tokens": "178.791793508"
"withdrawn_tokens": "259.089789444",
"remaining_tokens": "140.910210556"
},
{
"address": "0x3738bec36216eA2F11B954891C65AAd1Bc852156",
@@ -75131,10 +75232,17 @@
"tx": "0xb59405747c8088945a412703637a7b422f3639439ec2ee15e180c0a2a0d71ee4"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "129.409734906",
"user": "0x6E624B5788CD721F489074993d5E1Ee2C477eB00",
"tranche_id": 5,
"tx": "0x9b25e6fb021dd73d4022c6879da50a0e2b818a46955153a90e44008f5f4f5a52"
}
],
"total_tokens": "200",
"withdrawn_tokens": "0",
"remaining_tokens": "200"
"withdrawn_tokens": "129.409734906",
"remaining_tokens": "70.590265094"
},
{
"address": "0xa9a677b0a3Be231C0654CabB0Eaa0A72E91B3E8d",
@@ -76048,6 +76156,12 @@
}
],
"withdrawals": [
{
"amount": "39.564687976",
"user": "0xD27929d68ac0E5fd5C919A5eb5968C1D06D3Fb83",
"tranche_id": 5,
"tx": "0x63c2319241d05630a6d9ea104770cdcb6d93d145554bf2ff9d82cd8ef4e17fc7"
},
{
"amount": "38.141400304",
"user": "0xD27929d68ac0E5fd5C919A5eb5968C1D06D3Fb83",
@@ -76098,8 +76212,8 @@
}
],
"total_tokens": "400",
"withdrawn_tokens": "219.120230844",
"remaining_tokens": "180.879769156"
"withdrawn_tokens": "258.68491882",
"remaining_tokens": "141.31508118"
},
{
"address": "0xF5037DDA4A660d67560200f45380FF8364e35540",
@@ -76163,10 +76277,17 @@
"tx": "0xe32a466fc780a0fb3fd84a804f622931ebfaf3f428bff0dc6d141270410e75f8"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "129.527999746",
"user": "0x4683B77D114a04f1602dda7B0Ab2A24Fe79295fD",
"tranche_id": 5,
"tx": "0x0a2ab8841a315a8f2bcf534953ff9058607709c1b761f4fded8ba238ea3f3802"
}
],
"total_tokens": "200",
"withdrawn_tokens": "0",
"remaining_tokens": "200"
"withdrawn_tokens": "129.527999746",
"remaining_tokens": "70.472000254"
},
{
"address": "0x0A67488B946C5Ad7e3afc8766316C9Dc17333C6B",
@@ -76681,7 +76802,7 @@
"tranche_start": "2021-12-05T00:00:00.000Z",
"tranche_end": "2022-06-05T00:00:00.000Z",
"total_added": "171288.42",
"total_removed": "63646.1049690697989",
"total_removed": "64196.1049690697989",
"locked_amount": "0",
"deposits": [
{
@@ -80936,6 +81057,21 @@
"user": "0xC343fD1a1dd3F8D5412b2bae1c3A4a0482029f63",
"tx": "0xd6c9e31ca9abc4832ba4d6345ddeefbc3ab947b9c48ad834e2002d790642db22"
},
{
"amount": "250",
"user": "0x92Faf283Ff5Bca05673411091D922a054A959900",
"tx": "0x5f47c30b18bfd7c1d7997ebd04ea2460b72f6a5880385075e37090428711215f"
},
{
"amount": "250",
"user": "0xaD8a77d52Dd1E2dDD364AfE960DF49CAe1124C6D",
"tx": "0xd8084414ae0dcff7b19cc85b92bc4898cb656f485e51bc8f8b0d499f97d5e212"
},
{
"amount": "50",
"user": "0xa11930Cd107D363aCc44223F03dDF899ac1c864C",
"tx": "0x9d260c971e699ed0255fe230c306c70f076064466c0b50c855a234c6e5034156"
},
{
"amount": "50",
"user": "0x300A831523b53112F5CF8B802D85EC48a8CB7dcC",
@@ -90976,10 +91112,17 @@
"tx": "0xadc25e73736cd85e84fee16f2d7ee41f16f7deb83a52ceb82b327974063964f7"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "250",
"user": "0x92Faf283Ff5Bca05673411091D922a054A959900",
"tranche_id": 6,
"tx": "0x5f47c30b18bfd7c1d7997ebd04ea2460b72f6a5880385075e37090428711215f"
}
],
"total_tokens": "250",
"withdrawn_tokens": "0",
"remaining_tokens": "250"
"withdrawn_tokens": "250",
"remaining_tokens": "0"
},
{
"address": "0xB0B91feF5E4Af34F727797079DCDa5CCAc817a4B",
@@ -93357,10 +93500,17 @@
"tx": "0x9f916cf09e8a3c4ade0ffce5190db464d0a2b1dadba78e1ee7ba5d6e751d6148"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "250",
"user": "0xaD8a77d52Dd1E2dDD364AfE960DF49CAe1124C6D",
"tranche_id": 6,
"tx": "0xd8084414ae0dcff7b19cc85b92bc4898cb656f485e51bc8f8b0d499f97d5e212"
}
],
"total_tokens": "250",
"withdrawn_tokens": "0",
"remaining_tokens": "250"
"withdrawn_tokens": "250",
"remaining_tokens": "0"
},
{
"address": "0xc140931E1A188667395734771f195Ce832c40922",
@@ -97861,10 +98011,17 @@
"tx": "0xe32a466fc780a0fb3fd84a804f622931ebfaf3f428bff0dc6d141270410e75f8"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "50",
"user": "0xa11930Cd107D363aCc44223F03dDF899ac1c864C",
"tranche_id": 6,
"tx": "0x9d260c971e699ed0255fe230c306c70f076064466c0b50c855a234c6e5034156"
}
],
"total_tokens": "50",
"withdrawn_tokens": "0",
"remaining_tokens": "50"
"withdrawn_tokens": "50",
"remaining_tokens": "0"
},
{
"address": "0x75AB80d94F9F7C0FE4E7973C8fF505882bE80f97",
+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>;
+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',
@@ -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,
});
});
});
@@ -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>
);
}
+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} />
+34 -13
View File
@@ -20,23 +20,43 @@ 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 +69,7 @@ export function createClient(base?: string, cacheConfig?: InMemoryCacheConfig) {
url: urlWS.href,
})
)
: new ApolloLink((operation, forward) => forward(operation));
: noOpLink;
const splitLink = isBrowser
? split(
@@ -87,6 +107,7 @@ export function createClient(base?: string, cacheConfig?: InMemoryCacheConfig) {
return new ApolloClient({
link: from([errorLink, composedTimeoutLink, retryLink, splitLink]),
cache: new InMemoryCache(cacheConfig),
connectToDevTools,
});
}
@@ -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)),
};
}
@@ -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);
}
});
@@ -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);
};
+5 -1
View File
@@ -74,7 +74,7 @@ const getInitialState = (config?: Configuration) =>
{}
);
type ClientCollection = Record<
export type ClientCollection = Record<
string,
undefined | ReturnType<typeof createClient>
>;
@@ -178,6 +178,10 @@ const reducer = (state: Record<string, NodeData>, action: Action) => {
}
};
/**
* Tests each node to see if its suitable for connecting to and returns that data
* as a map of node urls to an object of that data
*/
export const useNodes = (config?: Configuration, skip?: boolean) => {
const [clients, setClients] = useState<ClientCollection>({});
const [state, dispatch] = useReducer(reducer, getInitialState(config));
@@ -83,9 +83,7 @@ const getBundledEnvironmentValue = (key: EnvKey) => {
case 'ETH_WALLET_MNEMONIC':
return process.env['NX_ETH_WALLET_MNEMONIC'];
case 'MAINTENANCE_PAGE':
return (
process.env['MAINTENANCE_PAGE'] || process.env['NX_MAINTENANCE_PAGE']
);
return process.env['NX_MAINTENANCE_PAGE'];
}
};
@@ -110,7 +108,7 @@ export const compileEnvironment = (
const environment = ENV_KEYS.reduce((acc, key) => {
const value = getValue(key, definitions);
if (value) {
if (value !== undefined && value !== null) {
return {
...acc,
[key]: value,
+5 -1
View File
@@ -38,7 +38,11 @@ export const requestNode = (
let subscriptionSucceeded = false;
const client = createClient(url);
const client = createClient({
url,
retry: false,
connectToDevTools: false,
});
// make a query for block height
client
+35 -37
View File
@@ -1,64 +1,62 @@
import produce from 'immer';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import {
makeDataProvider,
removePaginationWrapper,
} from '@vegaprotocol/react-helpers';
import {
MarginsSubscriptionDocument,
MarginsDocument,
} from './__generated__/Positions';
import type {
MarginsQuery,
MarginFieldsFragment,
MarginsSubscriptionSubscription,
} from './__generated__/Positions';
const update = (
data: MarginsQuery['party'],
data: MarginFieldsFragment[],
delta: MarginsSubscriptionSubscription['margins']
) => {
return produce(data, (draft) => {
const { marketId } = delta;
if (marketId && draft?.marginsConnection?.edges) {
const index = draft.marginsConnection.edges.findIndex(
(edge) => edge.node.market.id === marketId
);
if (index !== -1) {
const currNode = draft.marginsConnection.edges[index].node;
draft.marginsConnection.edges[index].node = {
...currNode,
maintenanceLevel: delta.maintenanceLevel,
searchLevel: delta.searchLevel,
initialLevel: delta.initialLevel,
collateralReleaseLevel: delta.collateralReleaseLevel,
};
} else {
draft.marginsConnection.edges.unshift({
__typename: 'MarginEdge',
node: {
__typename: 'MarginLevels',
market: {
__typename: 'Market',
id: delta.marketId,
},
maintenanceLevel: delta.maintenanceLevel,
searchLevel: delta.searchLevel,
initialLevel: delta.initialLevel,
collateralReleaseLevel: delta.collateralReleaseLevel,
asset: {
__typename: 'Asset',
id: delta.asset,
},
},
});
}
const index = draft.findIndex((node) => node.market.id === marketId);
if (index !== -1) {
const currNode = draft[index];
draft[index] = {
...currNode,
maintenanceLevel: delta.maintenanceLevel,
searchLevel: delta.searchLevel,
initialLevel: delta.initialLevel,
collateralReleaseLevel: delta.collateralReleaseLevel,
};
} else {
draft.unshift({
__typename: 'MarginLevels',
market: {
__typename: 'Market',
id: delta.marketId,
},
maintenanceLevel: delta.maintenanceLevel,
searchLevel: delta.searchLevel,
initialLevel: delta.initialLevel,
collateralReleaseLevel: delta.collateralReleaseLevel,
asset: {
__typename: 'Asset',
id: delta.asset,
},
});
}
});
};
const getData = (responseData: MarginsQuery) => responseData.party;
const getData = (responseData: MarginsQuery) =>
removePaginationWrapper(responseData.party?.marginsConnection?.edges) || [];
const getDelta = (subscriptionData: MarginsSubscriptionSubscription) =>
subscriptionData.margins;
export const marginsDataProvider = makeDataProvider<
MarginsQuery,
MarginsQuery['party'],
MarginFieldsFragment[],
MarginsSubscriptionSubscription,
MarginsSubscriptionSubscription['margins']
>({
@@ -1,7 +1,10 @@
import * as Schema from '@vegaprotocol/types';
import type { Account } from '@vegaprotocol/accounts';
import type { MarketWithData } from '@vegaprotocol/market-list';
import type { PositionsQuery, MarginsQuery } from './__generated__/Positions';
import type {
PositionFieldsFragment,
MarginFieldsFragment,
} from './__generated__/Positions';
import { getMetrics, rejoinPositionData } from './positions-data-providers';
const accounts = [
@@ -63,47 +66,32 @@ const accounts = [
},
] as Account[];
const positions: PositionsQuery = {
party: {
__typename: 'Party',
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
positionsConnection: {
__typename: 'PositionConnection',
edges: [
{
__typename: 'PositionEdge',
node: {
__typename: 'Position',
openVolume: '100',
averageEntryPrice: '8993727',
updatedAt: '2022-07-28T14:53:54.725477Z',
realisedPNL: '0',
unrealisedPNL: '43804770',
market: {
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
},
},
{
__typename: 'PositionEdge',
node: {
__typename: 'Position',
openVolume: '-100',
realisedPNL: '0',
unrealisedPNL: '-9112700',
averageEntryPrice: '840158',
updatedAt: '2022-07-28T15:09:34.441143Z',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
},
},
],
const positions: PositionFieldsFragment[] = [
{
__typename: 'Position',
openVolume: '100',
averageEntryPrice: '8993727',
updatedAt: '2022-07-28T14:53:54.725477Z',
realisedPNL: '0',
unrealisedPNL: '43804770',
market: {
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
},
};
{
__typename: 'Position',
openVolume: '-100',
realisedPNL: '0',
unrealisedPNL: '-9112700',
averageEntryPrice: '840158',
updatedAt: '2022-07-28T15:09:34.441143Z',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
},
];
const marketsData = [
{
@@ -162,60 +150,44 @@ const marketsData = [
},
] as MarketWithData[];
const margins: MarginsQuery = {
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
marginsConnection: {
edges: [
{
__typename: 'MarginEdge',
node: {
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
},
},
{
__typename: 'MarginEdge',
node: {
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
},
},
],
__typename: 'MarginConnection',
const margins: MarginFieldsFragment[] = [
{
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
__typename: 'Party',
},
};
{
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
},
];
describe('getMetrics && rejoinPositionData', () => {
it('returns positions metrics', () => {
const positionsRejoined = rejoinPositionData(
positions.party,
positions,
marketsData,
margins.party
margins
);
const metrics = getMetrics(positionsRejoined, accounts || null);
expect(metrics.length).toEqual(2);
@@ -223,9 +195,9 @@ describe('getMetrics && rejoinPositionData', () => {
it('calculates metrics', () => {
const positionsRejoined = rejoinPositionData(
positions.party,
positions,
marketsData,
margins.party
margins
);
const metrics = getMetrics(positionsRejoined, accounts || null);
@@ -8,14 +8,15 @@ import { toBigNum } from '@vegaprotocol/react-helpers';
import {
makeDataProvider,
makeDerivedDataProvider,
removePaginationWrapper,
} from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
import type { MarketWithData } from '@vegaprotocol/market-list';
import { marketsWithDataProvider } from '@vegaprotocol/market-list';
import type {
PositionsQuery,
PositionFieldsFragment,
PositionsSubscriptionSubscription,
MarginsQuery,
MarginFieldsFragment,
} from './__generated__/Positions';
import {
@@ -170,20 +171,17 @@ export const getMetrics = (
};
export const update = (
data: PositionsQuery['party'],
data: PositionFieldsFragment[],
deltas: PositionsSubscriptionSubscription['positions']
) => {
return produce(data, (draft) => {
deltas.forEach((delta) => {
if (!draft?.positionsConnection?.edges || !delta) {
return;
}
const index = draft.positionsConnection.edges.findIndex(
(edge) => edge.node.market.id === delta.marketId
const index = draft.findIndex(
(node) => node.market.id === delta.marketId
);
if (index !== -1) {
const currNode = draft.positionsConnection.edges[index].node;
draft.positionsConnection.edges[index].node = {
const currNode = draft[index];
draft[index] = {
...currNode,
realisedPNL: delta.realisedPNL,
unrealisedPNL: delta.unrealisedPNL,
@@ -192,15 +190,12 @@ export const update = (
updatedAt: delta.updatedAt,
};
} else {
draft.positionsConnection.edges.unshift({
__typename: 'PositionEdge',
node: {
...delta,
__typename: 'Position',
market: {
__typename: 'Market',
id: delta.marketId,
},
draft.unshift({
...delta,
__typename: 'Position',
market: {
__typename: 'Market',
id: delta.marketId,
},
});
}
@@ -210,29 +205,29 @@ export const update = (
export const positionsDataProvider = makeDataProvider<
PositionsQuery,
PositionsQuery['party'],
PositionFieldsFragment[],
PositionsSubscriptionSubscription,
PositionsSubscriptionSubscription['positions']
>({
query: PositionsDocument,
subscriptionQuery: PositionsSubscriptionDocument,
update,
getData: (responseData: PositionsQuery) => responseData.party,
getData: (responseData: PositionsQuery) =>
removePaginationWrapper(responseData.party?.positionsConnection?.edges) ||
[],
getDelta: (subscriptionData: PositionsSubscriptionSubscription) =>
subscriptionData.positions,
});
const upgradeMarginsConnection = (
marketId: string,
margins: MarginsQuery['party'] | null
margins: MarginFieldsFragment[] | null
) => {
if (marketId && margins?.marginsConnection?.edges) {
if (marketId && margins) {
const index =
margins.marginsConnection.edges.findIndex(
(edge) => edge.node.market.id === marketId
) ?? -1;
margins.findIndex((node) => node.market.id === marketId) ?? -1;
if (index >= 0) {
const marginLevel = margins.marginsConnection.edges[index].node;
const marginLevel = margins[index];
return {
maintenanceLevel: marginLevel.maintenanceLevel,
searchLevel: marginLevel.searchLevel,
@@ -244,12 +239,12 @@ const upgradeMarginsConnection = (
};
export const rejoinPositionData = (
positions: PositionsQuery['party'] | null,
positions: PositionFieldsFragment[] | null,
marketsData: MarketWithData[] | null,
margins: MarginsQuery['party'] | null
margins: MarginFieldsFragment[] | null
): PositionRejoined[] | null => {
if (positions?.positionsConnection?.edges && marketsData && margins) {
return positions.positionsConnection.edges.map(({ node }) => {
if (positions && marketsData && margins) {
return positions.map((node) => {
return {
realisedPNL: node.realisedPNL,
openVolume: node.openVolume,
@@ -172,3 +172,33 @@ it('displays realised and unrealised PNL', async () => {
expect(cells[9].textContent).toEqual('1.23');
expect(cells[10].textContent).toEqual('4.56');
});
it('displays close button', async () => {
await act(async () => {
render(
<PositionsTable
rowData={singleRowData}
onClose={() => {
return;
}}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('Close');
});
it('do not display close button if openVolume is zero', async () => {
await act(async () => {
render(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '0' }]}
onClose={() => {
return;
}}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('');
});
+10 -8
View File
@@ -381,14 +381,16 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
{onClose ? (
<AgGridColumn
type="rightAligned"
cellRenderer={({ data }: VegaICellRendererParams<Position>) => (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
)}
cellRenderer={({ data }: VegaICellRendererParams<Position>) =>
data?.openVolume && data?.openVolume !== '0' ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null
}
/>
) : null}
</AgGrid>
+6 -22
View File
@@ -2,30 +2,17 @@ import { useCallback, useState } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { marginsDataProvider } from './margin-data-provider';
import type {
MarginsQuery,
MarginsSubscriptionSubscription,
} from './__generated__/Positions';
const getMarketMarginPosition = ({
data,
marketId,
}: {
data: MarginsQuery['party'] | null;
marketId: string;
}) => {
const positions =
data?.marginsConnection?.edges?.map((item) => item.node) ?? [];
return positions.find((item) => item.market.id === marketId);
};
import type { MarginFieldsFragment } from './__generated__/Positions';
export const useMarketMargin = (marketId: string) => {
const { pubKey } = useVegaWallet();
const [marginLevel, setMarginLevel] = useState<string>('');
const update = useCallback(
({ data }: { data: MarginsQuery['party'] | null }) => {
const marginMarketPosition = getMarketMarginPosition({ data, marketId });
({ data }: { data: MarginFieldsFragment[] | null }) => {
const marginMarketPosition = data?.find(
(item) => item.market.id === marketId
);
if (marginMarketPosition?.maintenanceLevel) {
setMarginLevel(marginMarketPosition?.maintenanceLevel || '');
}
@@ -34,10 +21,7 @@ export const useMarketMargin = (marketId: string) => {
[setMarginLevel, marketId]
);
useDataProvider<
MarginsQuery['party'],
MarginsSubscriptionSubscription['margins']
>({
useDataProvider({
dataProvider: marginsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey || !marketId,
@@ -3,28 +3,16 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { positionsDataProvider } from './positions-data-providers';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import type {
PositionsQuery,
PositionFieldsFragment,
PositionsSubscriptionSubscription,
} from './__generated__/Positions';
const getMarketPosition = ({
data,
marketId,
}: {
data: PositionsQuery['party'];
marketId: string;
}) => {
const positions =
data?.positionsConnection?.edges?.map((item) => item.node) ?? [];
return positions.find((item) => item.market.id === marketId);
};
export const useMarketPositionOpenVolume = (marketId: string) => {
const { pubKey } = useVegaWallet();
const [openVolume, setOpenVolume] = useState<string>('');
const update = useCallback(
({ data }: { data: PositionsQuery['party'] | undefined }) => {
const position = getMarketPosition({ data, marketId });
({ data }: { data: PositionFieldsFragment[] | null }) => {
const position = data?.find((node) => node.market.id === marketId);
if (position?.openVolume) {
setOpenVolume(position?.openVolume || '');
}
@@ -34,7 +22,7 @@ export const useMarketPositionOpenVolume = (marketId: string) => {
);
useDataProvider<
PositionsQuery['party'],
PositionFieldsFragment[],
PositionsSubscriptionSubscription['positions']
>({
dataProvider: positionsDataProvider,
+1
View File
@@ -4,6 +4,7 @@ export * from './use-data-provider';
export * from './use-fetch';
export * from './use-mutation-observer';
export * from './use-network-params';
export * from './use-navigator-online';
export * from './use-outside-click';
export * from './use-resize-observer';
export * from './use-resize';
@@ -0,0 +1,29 @@
import { act, fireEvent, renderHook } from '@testing-library/react';
import { useNavigatorOnline } from './use-navigator-online';
const setup = () => {
return renderHook(() => useNavigatorOnline());
};
const turnOn = () => {
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(true);
fireEvent(window, new Event('online'));
};
const turnOff = () => {
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(false);
fireEvent(window, new Event('offline'));
};
describe('useNavigatorOnline', () => {
it('returns true if connected and false if not', () => {
const { result } = setup();
expect(result.current).toBe(true);
act(turnOff);
expect(result.current).toBe(false);
act(turnOn);
expect(result.current).toBe(true);
});
});
@@ -0,0 +1,16 @@
import { useSyncExternalStore } from 'react';
const subscribe = (onStoreChange: () => void) => {
window.addEventListener('online', onStoreChange);
window.addEventListener('offline', onStoreChange);
return () => {
window.removeEventListener('online', onStoreChange);
window.removeEventListener('offline', onStoreChange);
};
};
export const useNavigatorOnline = () =>
useSyncExternalStore(
subscribe,
() => window.navigator.onLine,
() => true
);
+4
View File
@@ -3894,6 +3894,10 @@ export type Statistics = {
chainVersion: Scalars['String'];
/** RFC3339Nano current time (real) */
currentTime: Scalars['Timestamp'];
/** Total number of events on the last block */
eventCount: Scalars['String'];
/** The number of events per second on the last block */
eventsPerSecond: Scalars['String'];
/** RFC3339Nano genesis time of the chain */
genesisTime: Scalars['Timestamp'];
/** Number of orders per seconds */
@@ -11,5 +11,5 @@ export const Indicator = ({ variant = Intent.None }: IndicatorProps) => {
'inline-block w-2 h-2 mt-1 mr-2 rounded-full',
getIntentTextAndBackground(variant)
);
return <div className={names} />;
return <div className={names} data-testid="indicator" />;
};
@@ -120,7 +120,7 @@ export const Toast = ({
onClick={closeToast}
className="absolute p-2 top-0 right-0"
>
<Icon name="cross" size={3} className="!block" />
<Icon name="cross" size={3} className="!block dark:text-white" />
</button>
<div
className={classNames(getToastAccent(intent), 'p-2 pt-3 text-center')}
@@ -9,6 +9,7 @@ import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '../provider';
import { VegaConnectDialog, CLOSE_DELAY } from './connect-dialog';
import type { VegaWalletDialogStore } from './connect-dialog';
import type { VegaConnectDialogProps } from '..';
import {
ClientErrors,
@@ -23,13 +24,15 @@ import { ChainIdDocument } from '@vegaprotocol/react-helpers';
const mockUpdateDialogOpen = jest.fn();
const mockCloseVegaDialog = jest.fn();
const mockStoreObj: Partial<VegaWalletDialogStore> = {
updateVegaWalletDialog: mockUpdateDialogOpen,
closeVegaWalletDialog: mockCloseVegaDialog,
vegaWalletDialogOpen: true,
};
jest.mock('zustand', () => ({
create: () => () => ({
updateVegaWalletDialog: mockUpdateDialogOpen,
closeVegaWalletDialog: mockCloseVegaDialog,
vegaWalletDialogOpen: true,
}),
create: () => (storeGetter: (store: VegaWalletDialogStore) => unknown) =>
storeGetter(mockStoreObj as VegaWalletDialogStore),
}));
let defaultProps: VegaConnectDialogProps;
@@ -301,7 +304,9 @@ describe('VegaConnectDialog', () => {
spyOnConnectWallet
.mockClear()
.mockImplementation(() =>
delayedReject(new WalletError('message', 3001, 'data'))
delayedReject(
new WalletError('User error', 3001, 'The user rejected the request')
)
);
render(generateJSX());
@@ -322,9 +327,9 @@ describe('VegaConnectDialog', () => {
await act(async () => {
jest.advanceTimersByTime(delay);
});
expect(screen.getByText('Connection declined')).toBeInTheDocument();
expect(screen.getByText('User error')).toBeInTheDocument();
expect(
screen.getByText('Your wallet connection was rejected')
screen.getByText('The user rejected the request')
).toBeInTheDocument();
});
@@ -9,8 +9,9 @@ import {
Loader,
} from '@vegaprotocol/ui-toolkit';
import { useCallback, useState } from 'react';
import type { WalletClientError } from '@vegaprotocol/wallet-client';
import { ExternalLinks, t, useChainIdQuery } from '@vegaprotocol/react-helpers';
import type { VegaConnector, WalletError } from '../connectors';
import type { VegaConnector } from '../connectors';
import { ViewConnector } from '../connectors';
import { JsonRpcConnector, RestConnector } from '../connectors';
import { RestConnectorForm } from './rest-connector-form';
@@ -44,7 +45,7 @@ export const useVegaWalletDialogStore = create<VegaWalletDialogStore>(
})
);
interface VegaWalletDialogStore {
export interface VegaWalletDialogStore {
vegaWalletDialogOpen: boolean;
updateVegaWalletDialog: (open: boolean) => void;
openVegaWalletDialog: () => void;
@@ -55,25 +56,25 @@ export const VegaConnectDialog = ({
connectors,
onChangeOpen,
}: VegaConnectDialogProps) => {
const {
vegaWalletDialogOpen,
closeVegaWalletDialog,
updateVegaWalletDialog,
} = useVegaWalletDialogStore((store) => ({
vegaWalletDialogOpen: store.vegaWalletDialogOpen,
updateVegaWalletDialog: onChangeOpen
const vegaWalletDialogOpen = useVegaWalletDialogStore(
(store) => store.vegaWalletDialogOpen
);
const updateVegaWalletDialog = useVegaWalletDialogStore((store) =>
onChangeOpen
? (open: boolean) => {
store.updateVegaWalletDialog(open);
onChangeOpen(open);
}
: store.updateVegaWalletDialog,
closeVegaWalletDialog: onChangeOpen
: store.updateVegaWalletDialog
);
const closeVegaWalletDialog = useVegaWalletDialogStore((store) =>
onChangeOpen
? () => {
store.closeVegaWalletDialog();
onChangeOpen(false);
}
: store.closeVegaWalletDialog,
}));
: store.closeVegaWalletDialog
);
const { data, error, loading } = useChainIdQuery();
@@ -254,7 +255,7 @@ const SelectedForm = ({
appChainId: string;
jsonRpcState: {
status: Status;
error: WalletError | null;
error: WalletClientError | null;
};
reset: () => void;
onConnect: () => void;
@@ -8,16 +8,15 @@ import {
Tick,
} from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import type { WalletClientError } from '@vegaprotocol/wallet-client';
import type { JsonRpcConnector } from '../connectors';
import { ClientErrors } from '../connectors';
import type { WalletError } from '../connectors';
import { ConnectDialogTitle } from './connect-dialog-elements';
import { Status } from '../use-json-rpc-connect';
import { useEnvironment } from '@vegaprotocol/environment';
export const ServiceErrors = {
NO_HEALTHY_NODE: 1000,
CONNECTION_DECLINED: 3001,
REQUEST_PROCESSING: -32000,
};
@@ -31,7 +30,7 @@ export const JsonRpcConnectorForm = ({
connector: JsonRpcConnector;
appChainId: string;
status: Status;
error: WalletError | null;
error: WalletClientError | null;
onConnect: () => void;
reset: () => void;
}) => {
@@ -58,7 +57,7 @@ const Connecting = ({
reset,
}: {
status: Status;
error: WalletError | null;
error: WalletClientError | null;
connector: JsonRpcConnector;
appChainId: string;
reset: () => void;
@@ -141,7 +140,7 @@ const Error = ({
appChainId,
onTryAgain,
}: {
error: WalletError | null;
error: WalletClientError | null;
connectorUrl: string | null;
appChainId: string;
onTryAgain: () => void;
@@ -158,18 +157,20 @@ const Error = ({
if (error) {
if (error.code === ClientErrors.NO_SERVICE.code) {
title = t('No wallet detected');
text = t(`No wallet application running at ${connectorUrl}`);
text = connectorUrl
? t('No wallet application running at %s', connectorUrl)
: t('No Vega Wallet application running');
} else if (error.code === ClientErrors.WRONG_NETWORK.code) {
title = t('Wrong network');
text = `To complete your wallet connection, set your wallet network in your app to "${appChainId}".`;
} else if (error.code === ServiceErrors.CONNECTION_DECLINED) {
title = t('Connection declined');
text = t('Your wallet connection was rejected');
text = t(
'To complete your wallet connection, set your wallet network in your app to "%s".',
appChainId
);
} else if (error.code === ServiceErrors.NO_HEALTHY_NODE) {
title = error.message;
title = error.title;
text = (
<>
{capitalize(error.data)}
{capitalize(error.message)}
{'. '}
{VEGA_DOCS_URL && (
<Link
@@ -188,20 +189,33 @@ const Error = ({
title = t('Wrong network');
text = (
<>
{t(`To complete your wallet connection, set your wallet network in your
app to ${appChainId}.`)}
{t(
`To complete your wallet connection, set your wallet network in your
app to %s.`,
appChainId
)}
</>
);
} else if (error.code === ClientErrors.INVALID_WALLET.code) {
title = error.title;
const errorData = error.message?.split('\n ') || [];
text = (
<span className="flex flex-col">
{errorData.map((str, i) => (
<span key={i}>{str}</span>
))}
</span>
);
} else {
title = error.message;
text = `${error.data} (${error.code})`;
title = t(error.title);
text = t(error.message);
}
}
return (
<>
<ConnectDialogTitle>{title}</ConnectDialogTitle>
<p className="text-center mb-2">{text}</p>
<p className="text-center mb-2 first-letter:uppercase">{text}</p>
{tryAgain}
</>
);
@@ -8,8 +8,6 @@ const VERSION = 'v2';
export const ClientErrors = {
NO_SERVICE: new WalletError(t('No service'), 100),
NO_TOKEN: new WalletError(t('No token'), 101),
INVALID_RESPONSE: new WalletError(t('Something went wrong'), 102),
INVALID_WALLET: new WalletError(t('Wallet version invalid'), 103),
WRONG_NETWORK: new WalletError(
t('Wrong network'),
@@ -22,11 +20,6 @@ export const ClientErrors = {
t('Unknown error occurred')
),
NO_CLIENT: new WalletError(t('No client found.'), 106),
REQUEST_REJECTED: new WalletError(
t('Request rejected'),
107,
t('The request has been rejected by the user')
),
} as const;
export class JsonRpcConnector implements VegaConnector {
@@ -70,7 +63,9 @@ export class JsonRpcConnector implements VegaConnector {
}),
});
}
get url() {
return this._url || '';
}
async getChainId() {
if (!this.client) {
throw ClientErrors.NO_CLIENT;
@@ -79,7 +74,12 @@ export class JsonRpcConnector implements VegaConnector {
const { result } = await this.client.GetChainId();
return result;
} catch (err) {
throw ClientErrors.INVALID_RESPONSE;
const {
code = ClientErrors.UNKNOWN.code,
message = ClientErrors.UNKNOWN.message,
title,
} = err as WalletClientError;
throw new WalletError(title, code, message);
}
}
@@ -92,11 +92,12 @@ export class JsonRpcConnector implements VegaConnector {
await this.client.ConnectWallet();
return null;
} catch (err) {
const clientErr =
err instanceof WalletClientError && err.code === 3001
? ClientErrors.REQUEST_REJECTED
: ClientErrors.INVALID_RESPONSE;
throw clientErr;
const {
code = ClientErrors.UNKNOWN.code,
message = ClientErrors.UNKNOWN.message,
title,
} = err as WalletClientError;
throw new WalletError(title, code, message);
}
}
@@ -111,7 +112,12 @@ export class JsonRpcConnector implements VegaConnector {
const { result } = await this.client.ListKeys();
return result.keys;
} catch (err) {
throw ClientErrors.INVALID_RESPONSE;
const {
code = ClientErrors.UNKNOWN.code,
message = ClientErrors.UNKNOWN.message,
title,
} = err as WalletClientError;
throw new WalletError(title, code, message);
}
}
@@ -147,15 +153,21 @@ export class JsonRpcConnector implements VegaConnector {
try {
const result = await fetch(`${this._url}/api/${this.version}/methods`);
if (!result.ok) {
const err = ClientErrors.INVALID_WALLET;
err.data = t(
`The wallet running at ${this._url} is not supported. Required version is ${this.version}`
const sent1 = t(
'The version of the wallet service running at %s is not supported.',
this._url as string
);
throw err;
const sent2 = t(
'Update the wallet software to a version that expose the API %s.',
this.version
);
const data = `${sent1}\n ${sent2}`;
const title = t('Wallet version invalid');
throw new WalletError(title, ClientErrors.INVALID_WALLET.code, data);
}
return true;
} catch (err) {
if (err instanceof WalletError) {
if (err instanceof WalletClientError) {
throw err;
}
+5 -7
View File
@@ -1,3 +1,4 @@
import { WalletClientError } from '@vegaprotocol/wallet-client';
import type * as Schema from '@vegaprotocol/types';
export interface DelegateSubmissionBody {
@@ -329,14 +330,11 @@ export interface TransactionResponse {
receivedAt: string;
sentAt: string;
}
export class WalletError {
message: string;
code: number;
data?: string;
export class WalletError extends WalletClientError {
data: string;
constructor(message: string, code: number, data?: string) {
this.message = message;
this.code = code;
constructor(message: string, code: number, data = 'Wallet error') {
super({ code, message, data });
this.data = data;
}
}
+2 -2
View File
@@ -1,6 +1,7 @@
import { LocalStorage } from '@vegaprotocol/react-helpers';
import type { ReactNode } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { WalletClientError } from '@vegaprotocol/wallet-client';
import type { VegaWalletContextShape } from '.';
import type {
PubKey,
@@ -9,7 +10,6 @@ import type {
} from './connectors/vega-connector';
import { VegaWalletContext } from './context';
import { WALLET_KEY } from './storage';
import { WalletError } from './connectors/vega-connector';
import { ViewConnector } from './connectors';
interface VegaWalletProviderProps {
@@ -53,7 +53,7 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
return null;
}
} catch (err) {
if (err instanceof WalletError) {
if (err instanceof WalletClientError) {
throw err;
}
return null;
+3 -3
View File
@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import { WalletClientError } from '@vegaprotocol/wallet-client';
import type { JsonRpcConnector } from './connectors';
import { ClientErrors } from './connectors';
import { WalletError } from './connectors';
import { useVegaWallet } from './use-vega-wallet';
export enum Status {
@@ -18,7 +18,7 @@ export enum Status {
export const useJsonRpcConnect = (onConnect: () => void) => {
const { connect } = useVegaWallet();
const [status, setStatus] = useState(Status.Idle);
const [error, setError] = useState<WalletError | null>(null);
const [error, setError] = useState<WalletClientError | null>(null);
const attemptConnect = useCallback(
async (connector: JsonRpcConnector, appChainId: string) => {
@@ -56,7 +56,7 @@ export const useJsonRpcConnect = (onConnect: () => void) => {
setStatus(Status.Connected);
onConnect();
} catch (err) {
if (err instanceof WalletError) {
if (err instanceof WalletClientError) {
setError(err);
}
setStatus(Status.Error);
@@ -1,7 +1,6 @@
import { useVegaWallet } from './use-vega-wallet';
import { useEffect, useRef } from 'react';
import { ClientErrors } from './connectors';
import { WalletError } from './connectors';
import { VegaTxStatus } from './use-vega-transaction';
import { useVegaTransactionStore } from './use-vega-transaction-store';
import { WalletClientError } from '@vegaprotocol/wallet-client';
@@ -40,10 +39,7 @@ export const useVegaTransactionManager = () => {
})
.catch((err) => {
update(transaction.id, {
error:
err instanceof WalletError || err instanceof WalletClientError
? err
: ClientErrors.UNKNOWN,
error: err instanceof WalletClientError ? err : ClientErrors.UNKNOWN,
status: VegaTxStatus.Error,
});
});
@@ -98,10 +98,14 @@ describe('useVegaTransaction', () => {
});
expect(result.current.transaction.status).toEqual(VegaTxStatus.Error);
expect(result.current.transaction.error).toHaveProperty(
'message',
'title',
'Something went wrong'
);
expect(result.current.transaction.error).toHaveProperty('code', 105);
expect(result.current.transaction.error).toHaveProperty(
'message',
'Unknown error occurred'
);
expect(result.current.transaction.error).toHaveProperty(
'data',
'Unknown error occurred'
+3 -10
View File
@@ -6,8 +6,6 @@ import { VegaTransactionDialog } from './vega-transaction-dialog';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import type { Transaction } from './connectors';
import { ClientErrors } from './connectors';
import { WalletError } from './connectors';
import type { WalletClientError } from '@vegaprotocol/wallet-client';
export interface DialogProps {
intent?: Intent;
@@ -26,7 +24,7 @@ export enum VegaTxStatus {
export interface VegaTxState {
status: VegaTxStatus;
error: WalletError | WalletClientError | Error | null;
error: Error | null;
txHash: string | null;
signature: string | null;
dialogOpen: boolean;
@@ -90,14 +88,9 @@ export const useVegaTransaction = () => {
return null;
} catch (err) {
const error =
err instanceof WalletError
? err
: err instanceof Error
? err
: ClientErrors.UNKNOWN;
const error = err instanceof Error ? err : ClientErrors.UNKNOWN;
setTransaction({
error: error,
error,
status: VegaTxStatus.Error,
});
return null;
+3 -3
View File
@@ -10,9 +10,9 @@ export function useVegaWallet() {
}
export function useReconnectVegaWallet() {
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const { disconnect } = useVegaWallet();
const reconnect = useCallback(async () => {
await disconnect();
@@ -2,7 +2,7 @@ import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/react-helpers';
import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import { WalletError } from '../connectors';
import { WalletClientError } from '@vegaprotocol/wallet-client';
import type { VegaTxState } from '../use-vega-transaction';
import { VegaTxStatus } from '../use-vega-transaction';
@@ -107,18 +107,13 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
}
if (transaction.status === VegaTxStatus.Error) {
content = (
<div data-testid={transaction.status}>
{transaction.error instanceof WalletError && (
<p>
{transaction.error.message}: {transaction.error.data}
</p>
)}
{transaction.error instanceof Error && (
<p>{transaction.error.message}</p>
)}
</div>
);
let messageText = '';
if (transaction.error instanceof WalletClientError) {
messageText = `${transaction.error.title}: ${transaction.error.message}`;
} else if (transaction.error instanceof Error) {
messageText = transaction.error.message;
}
content = <div data-testid={transaction.status}>{messageText}</div>;
}
if (transaction.status === VegaTxStatus.Pending) {
+1 -1
View File
@@ -36,7 +36,7 @@
"@sentry/nextjs": "^6.19.3",
"@sentry/react": "^6.19.2",
"@sentry/tracing": "^6.19.2",
"@vegaprotocol/wallet-client": "0.1.8",
"@vegaprotocol/wallet-client": "0.1.9",
"@walletconnect/ethereum-provider": "^1.7.5",
"@web3-react/core": "8.0.20-beta.0",
"@web3-react/metamask": "8.0.16-beta.0",
+4 -4
View File
@@ -7321,10 +7321,10 @@
"@typescript-eslint/types" "5.40.0"
eslint-visitor-keys "^3.3.0"
"@vegaprotocol/wallet-client@0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@vegaprotocol/wallet-client/-/wallet-client-0.1.8.tgz#38ca8566d78b9f6694b12ad9364bb34d6482935d"
integrity sha512-FVvDvvlccKyXn0ujhivPUCVnkZYQJxtI1q8OgipNnbmAjU1mLyeuRTBw0Isu330yPI1KppNbW6Qicd8OTHBmxw==
"@vegaprotocol/wallet-client@0.1.9":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@vegaprotocol/wallet-client/-/wallet-client-0.1.9.tgz#8c6a71c8b2222b3de5d73cade8fc6db57e332de9"
integrity sha512-oacfJGT0zHM+1If4I/pgIWi7zzU/3uHy4+sjuFEh8pWI8bWBzCF+mHbOGA6iTM+5/5mhayMFc+YZ9GexH1sBnQ==
dependencies:
express "4.18.2"
nanoid "3.3.4"