Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b430bb86 | ||
|
|
6980f4ff9f | ||
|
|
4d7724ace3 | ||
|
|
8524f55771 | ||
|
|
eda6816c75 | ||
|
|
773d9b2ae6 | ||
|
|
f3674b3e38 |
@@ -1,11 +1,12 @@
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
||||
NX_TENDERMINT_URL=https://be.testnet.vega.xyz
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.testnet.vega.xyz/
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_URL=https://api.n09.testnet.vega.xyz/graphql
|
||||
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_VEGA_NETWORKS={}
|
||||
@@ -1,23 +1,15 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
EnvironmentProvider,
|
||||
NetworkLoader,
|
||||
useEnvironment,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
||||
import { NetworkInfo } from '@vegaprotocol/network-info';
|
||||
import { Nav } from './components/nav';
|
||||
import { Header } from './components/header';
|
||||
import { Main } from './components/main';
|
||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||
import { ENV } from './config/env';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
|
||||
function App() {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const [theme, toggleTheme] = useThemeSwitcher();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
@@ -27,22 +19,7 @@ function App() {
|
||||
setMenuOpen(false);
|
||||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
Sentry.init({
|
||||
dsn: ENV.dsn,
|
||||
integrations: [new BrowserTracing()],
|
||||
tracesSampleRate: 1,
|
||||
environment: VEGA_ENV,
|
||||
});
|
||||
}, [VEGA_ENV]);
|
||||
|
||||
const cacheConfig: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
Node: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const cacheConfig: InMemoryCacheConfig = {};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
query ExplorerAsset($id: ID!) {
|
||||
asset(id: $id) {
|
||||
id
|
||||
name
|
||||
status
|
||||
decimals
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerAssetQueryVariables = Types.Exact<{
|
||||
id: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerAssetQuery = { __typename?: 'Query', asset?: { __typename?: 'Asset', id: string, name: string, status: Types.AssetStatus, decimals: number } | null };
|
||||
|
||||
|
||||
export const ExplorerAssetDocument = gql`
|
||||
query ExplorerAsset($id: ID!) {
|
||||
asset(id: $id) {
|
||||
id
|
||||
name
|
||||
status
|
||||
decimals
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerAssetQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerAssetQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerAssetQuery` 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 } = useExplorerAssetQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerAssetQuery(baseOptions: Apollo.QueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
|
||||
}
|
||||
export function useExplorerAssetLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
|
||||
}
|
||||
export type ExplorerAssetQueryHookResult = ReturnType<typeof useExplorerAssetQuery>;
|
||||
export type ExplorerAssetLazyQueryHookResult = ReturnType<typeof useExplorerAssetLazyQuery>;
|
||||
export type ExplorerAssetQueryResult = Apollo.QueryResult<ExplorerAssetQuery, ExplorerAssetQueryVariables>;
|
||||
@@ -0,0 +1,63 @@
|
||||
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 AssetLink from './asset-link';
|
||||
import { ExplorerAssetDocument } from './__generated__/Asset';
|
||||
|
||||
function renderComponent(id: string, mock: MockedResponse[]) {
|
||||
return (
|
||||
<MockedProvider mocks={mock}>
|
||||
<MemoryRouter>
|
||||
<AssetLink id={id} />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Asset link component', () => {
|
||||
it('Renders the ID at first', () => {
|
||||
const res = render(renderComponent('123', []));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the asset name when the query returns a result', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerAssetDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
asset: {
|
||||
id: '123',
|
||||
name: 'test-label',
|
||||
status: 'irrelevant-test-data',
|
||||
decimals: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
expect(await res.findByText('test-label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Leaves the asset id when the asset is not found', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerAssetDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
error: new Error('No such asset'),
|
||||
};
|
||||
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(await res.findByText('123')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { useExplorerAssetQuery } from './__generated__/Asset';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export type AssetLinkProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an asset ID, it will fetch the asset name and show that,
|
||||
* with a link to the assets list. If the name does not come back
|
||||
* it will use the ID instead.
|
||||
*/
|
||||
const AssetLink = ({ id, ...props }: AssetLinkProps) => {
|
||||
const { data } = useExplorerAssetQuery({
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
let label: string = id;
|
||||
|
||||
if (data?.asset?.name) {
|
||||
label = data.asset.name;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="underline" to={`/${Routes.MARKETS}#${id}`} {...props}>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetLink;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export type BlockLinkProps = {
|
||||
height: string;
|
||||
};
|
||||
|
||||
const BlockLink = ({ height, ...props }: BlockLinkProps) => {
|
||||
return (
|
||||
<Link className="underline" to={`/${Routes.BLOCKS}/${height}`} {...props}>
|
||||
{height}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockLink;
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as BlockLink } from './block-link/block-link';
|
||||
export { default as PartyLink } from './party-link/party-link';
|
||||
export { default as NodeLink } from './node-link/node-link';
|
||||
export { default as MarketLink } from './market-link/market-link';
|
||||
export { default as AssetLink } from './asset-link/asset-link';
|
||||
@@ -0,0 +1,11 @@
|
||||
query ExplorerMarket($id: ID!) {
|
||||
market(id: $id) {
|
||||
id
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerMarketQueryVariables = Types.Exact<{
|
||||
id: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } | null };
|
||||
|
||||
|
||||
export const ExplorerMarketDocument = gql`
|
||||
query ExplorerMarket($id: ID!) {
|
||||
market(id: $id) {
|
||||
id
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerMarketQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerMarketQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerMarketQuery` 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 } = useExplorerMarketQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerMarketQuery(baseOptions: Apollo.QueryHookOptions<ExplorerMarketQuery, ExplorerMarketQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerMarketQuery, ExplorerMarketQueryVariables>(ExplorerMarketDocument, options);
|
||||
}
|
||||
export function useExplorerMarketLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerMarketQuery, ExplorerMarketQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerMarketQuery, ExplorerMarketQueryVariables>(ExplorerMarketDocument, options);
|
||||
}
|
||||
export type ExplorerMarketQueryHookResult = ReturnType<typeof useExplorerMarketQuery>;
|
||||
export type ExplorerMarketLazyQueryHookResult = ReturnType<typeof useExplorerMarketLazyQuery>;
|
||||
export type ExplorerMarketQueryResult = Apollo.QueryResult<ExplorerMarketQuery, ExplorerMarketQueryVariables>;
|
||||
@@ -0,0 +1,66 @@
|
||||
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 MarketLink from './market-link';
|
||||
import { ExplorerMarketDocument } from './__generated__/Market';
|
||||
|
||||
function renderComponent(id: string, mock: MockedResponse[]) {
|
||||
return (
|
||||
<MockedProvider mocks={mock}>
|
||||
<MemoryRouter>
|
||||
<MarketLink id={id} />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Market link component', () => {
|
||||
it('Renders the ID at first', () => {
|
||||
const res = render(renderComponent('123', []));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the market name when the query returns a result', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerMarketDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
market: {
|
||||
id: '123',
|
||||
state: 'irrelevant-test-data',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'test-label',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
expect(await res.findByText('test-label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Leaves the market id when the market is not found', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerMarketDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
error: new Error('No such market'),
|
||||
};
|
||||
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(await res.findByText('123')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { useExplorerMarketQuery } from './__generated__/Market';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export type MarketLinkProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a market ID, it will fetch the market name and show that,
|
||||
* 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 { data } = useExplorerMarketQuery({
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
let label: string = id;
|
||||
|
||||
if (data?.market?.tradableInstrument.instrument.name) {
|
||||
label = data.market.tradableInstrument.instrument.name;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="underline" to={`/${Routes.MARKETS}#${id}`} {...props}>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketLink;
|
||||
@@ -0,0 +1,7 @@
|
||||
query ExplorerNode($id: ID!) {
|
||||
node(id: $id) {
|
||||
id
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerNodeQueryVariables = Types.Exact<{
|
||||
id: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerNodeQuery = { __typename?: 'Query', node?: { __typename?: 'Node', id: string, name: string, status: Types.NodeStatus } | null };
|
||||
|
||||
|
||||
export const ExplorerNodeDocument = gql`
|
||||
query ExplorerNode($id: ID!) {
|
||||
node(id: $id) {
|
||||
id
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerNodeQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerNodeQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerNodeQuery` 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 } = useExplorerNodeQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerNodeQuery(baseOptions: Apollo.QueryHookOptions<ExplorerNodeQuery, ExplorerNodeQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerNodeQuery, ExplorerNodeQueryVariables>(ExplorerNodeDocument, options);
|
||||
}
|
||||
export function useExplorerNodeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerNodeQuery, ExplorerNodeQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerNodeQuery, ExplorerNodeQueryVariables>(ExplorerNodeDocument, options);
|
||||
}
|
||||
export type ExplorerNodeQueryHookResult = ReturnType<typeof useExplorerNodeQuery>;
|
||||
export type ExplorerNodeLazyQueryHookResult = ReturnType<typeof useExplorerNodeLazyQuery>;
|
||||
export type ExplorerNodeQueryResult = Apollo.QueryResult<ExplorerNodeQuery, ExplorerNodeQueryVariables>;
|
||||
@@ -0,0 +1,62 @@
|
||||
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 NodeLink from './node-link';
|
||||
import { ExplorerNodeDocument } from './__generated__/Node';
|
||||
|
||||
function renderComponent(id: string, mock: MockedResponse[]) {
|
||||
return (
|
||||
<MockedProvider mocks={mock}>
|
||||
<MemoryRouter>
|
||||
<NodeLink id={id} />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Node link component', () => {
|
||||
it('Renders the ID at first', () => {
|
||||
const res = render(renderComponent('123', []));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the node name when the query returns a result', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerNodeDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
node: {
|
||||
id: '123',
|
||||
status: 'irrelevant-test-data',
|
||||
name: 'test-label',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
expect(await res.findByText('test-label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Leaves the node id when the node is not found', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerNodeDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
error: new Error('No such node'),
|
||||
};
|
||||
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(await res.findByText('123')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { useExplorerNodeQuery } from './__generated__/Node';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export type NodeLinkProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const NodeLink = ({ id, ...props }: NodeLinkProps) => {
|
||||
const { data } = useExplorerNodeQuery({
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
let label: string = id;
|
||||
|
||||
if (data?.node?.name) {
|
||||
label = data.node.name;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="underline" to={`/${Routes.VALIDATORS}#${id}`} {...props}>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeLink;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export type PartyLinkProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
|
||||
return (
|
||||
<Link className="underline" to={`/${Routes.PARTIES}/${id}`} {...props}>
|
||||
{id}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default PartyLink;
|
||||
@@ -41,10 +41,10 @@ describe('NestedDataList', () => {
|
||||
const parent = getAllByRole('listitem', { name: 'Validator Heartbeat' });
|
||||
const nestedContainer = parent[0].querySelector('[aria-hidden]');
|
||||
const expandBtn = parent[0].querySelector('button');
|
||||
expect(nestedContainer).toHaveAttribute('aria-hidden', 'true');
|
||||
expect(nestedContainer).toHaveAttribute('aria-hidden', 'false');
|
||||
await user.click(expandBtn as HTMLButtonElement);
|
||||
await waitFor(() => nestedContainer);
|
||||
expect(nestedContainer).toHaveAttribute('aria-hidden', 'false');
|
||||
expect(nestedContainer).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('add border to the title of the parent', () => {
|
||||
|
||||
@@ -55,7 +55,7 @@ const NestedDataListItem = ({
|
||||
value,
|
||||
index,
|
||||
}: NestedDataListItemProps) => {
|
||||
const [isCollapsed, setCollapsed] = useState(true);
|
||||
const [isCollapsed, setCollapsed] = useState(false);
|
||||
const toggleVisible = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -20,10 +20,10 @@ export const PageHeader = ({
|
||||
copy = false,
|
||||
className,
|
||||
}: PageHeaderProps) => {
|
||||
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha';
|
||||
const titleClasses = 'text-xl xl:text-xl uppercase ';
|
||||
return (
|
||||
<header className={className}>
|
||||
<span className={`${titleClasses} block`}>{prefix}</span>
|
||||
<span className={`${titleClasses}`}>{prefix}</span>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<h2 className={titleClasses}>
|
||||
{truncateStart && truncateEnd ? (
|
||||
|
||||
@@ -18,7 +18,7 @@ export const TimeAgo = ({ date, ...props }: TimeAgoProps) => {
|
||||
return () => clearInterval(int);
|
||||
}, [setDistanceToNow, date]);
|
||||
|
||||
if (!date) {
|
||||
if (!date || date.length === 0) {
|
||||
return <>{t('Date unknown')}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { TxDetailsOrder } from './tx-order';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsHeartbeat } from './tx-hearbeat';
|
||||
import { TxDetailsLPAmend } from './tx-lp-amend';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
height: string;
|
||||
}
|
||||
|
||||
export const TxDetailsWrapper = ({
|
||||
txData,
|
||||
pubKey,
|
||||
height,
|
||||
}: TxDetailsWrapperProps) => {
|
||||
const {
|
||||
state: { data: blockData, loading, error },
|
||||
} = useFetch<TendermintBlocksResponse>(
|
||||
`${DATA_SOURCES.tendermintUrl}/block?height=${height}`
|
||||
);
|
||||
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
let child;
|
||||
|
||||
if (txData.type === 'Submit Order') {
|
||||
child = (
|
||||
<TxDetailsOrder txData={txData} blockData={blockData} pubKey={pubKey} />
|
||||
);
|
||||
} else if (txData.type === 'Validator Heartbeat') {
|
||||
child = (
|
||||
<TxDetailsHeartbeat
|
||||
txData={txData}
|
||||
blockData={blockData}
|
||||
pubKey={pubKey}
|
||||
/>
|
||||
);
|
||||
} else if (txData.type === 'Amend LiquidityProvision Order') {
|
||||
child = (
|
||||
<TxDetailsLPAmend txData={txData} blockData={blockData} pubKey={pubKey} />
|
||||
);
|
||||
} else {
|
||||
child = <code>{JSON.stringify(txData)}</code>;
|
||||
}
|
||||
|
||||
if (!child) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <section>{child}</section>;
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
BlockExplorerTransactionResult,
|
||||
ValidatorHeartbeat,
|
||||
} from '../../../routes/types/block-explorer-response';
|
||||
import { BlockLink, NodeLink, PartyLink } from '../../links/';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TimeAgo } from '../../time-ago';
|
||||
import { Block } from '../../../routes/blocks/id';
|
||||
|
||||
/**
|
||||
* Returns an integer representing how fresh the signature is, ranging from 1 to 500.
|
||||
* Below 1 should be impossible - you can't sign a block before it is finished
|
||||
* Any result about 500 is counted as stale in core and would be bad
|
||||
*
|
||||
* The precise freshness isn't that important, as long as it is within bounds
|
||||
*
|
||||
* @param txHeight string Block number that this signature was in
|
||||
* @param signatureForHeight string Block number that this signature is signing
|
||||
* @returns
|
||||
*/
|
||||
export function scoreFreshness(
|
||||
txHeight: string,
|
||||
signatureForHeight: string
|
||||
): number {
|
||||
const txHeightInt = parseInt(txHeight, 10);
|
||||
const signatureForHeightInt = parseInt(signatureForHeight, 10);
|
||||
|
||||
return txHeightInt - signatureForHeightInt;
|
||||
}
|
||||
|
||||
interface TxDetailsHeartbeatProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator Heartbeat transactions are a way for non-consensus validators to signal that they
|
||||
* are still alive, still following along, and still valid for consideration in the validator set.
|
||||
* To indicate they are still alive, they use their Ethereum and Vega private keys to provide two
|
||||
* signatures of a recent block on chain.
|
||||
*
|
||||
* Blocks must be signed within 500 seconds (i.e. roughly 500 blocks) to not be considered stale
|
||||
*
|
||||
* For the sake of block explorer, these design decisions were made:
|
||||
* - The signature values are not interesting. They're available in details but not worth displaying
|
||||
* - Freshness is a word that isn't used anywhere else. It's meant to imply how close to the lower
|
||||
* bound the signature was. But it doesn't matter as long as it's less than 500.
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export const TxDetailsHeartbeat = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsHeartbeatProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const cmd = txData.command as ValidatorHeartbeat;
|
||||
const time: string = blockData?.result.block.header.time || '';
|
||||
const height: string = blockData?.result.block.header.height || '';
|
||||
|
||||
return (
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{t('Submitter')}
|
||||
{pubKey ? <PartyLink id={pubKey} /> : '-'}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Block')}
|
||||
<BlockLink height={height} />
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Time')}
|
||||
{time ? (
|
||||
<span>
|
||||
{time} (<TimeAgo date={time} /> )
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Node')}
|
||||
<NodeLink id={cmd.validatorHeartbeat.nodeId} />
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Signed block height')}
|
||||
<BlockLink height={cmd.blockHeight} />
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Freshness (lower is better)')}
|
||||
{scoreFreshness(txData.block, cmd.blockHeight)}
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
AmendLiquidityProvisionOrder,
|
||||
BlockExplorerTransactionResult,
|
||||
} from '../../../routes/types/block-explorer-response';
|
||||
import { BlockLink, PartyLink } from '../../links/';
|
||||
import { MarketLink } from '../../links/';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TimeAgo } from '../../time-ago';
|
||||
|
||||
interface TxDetailsOrderProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies changes to the shape of a users Liquidity Commitment order for
|
||||
* a specific market
|
||||
*/
|
||||
export const TxDetailsLPAmend = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsOrderProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const cmd = txData.command as AmendLiquidityProvisionOrder;
|
||||
const time: string = blockData?.result.block.header.time || '';
|
||||
const height: string = blockData?.result.block.header.height || '';
|
||||
|
||||
return (
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{t('Submitter')}
|
||||
{pubKey ? <PartyLink id={pubKey} /> : '-'}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Block')}
|
||||
<BlockLink height={height} />
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Time')}
|
||||
{time ? (
|
||||
<span>
|
||||
{time} (<TimeAgo date={time} /> )
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Market')}
|
||||
<MarketLink id={cmd.liquidityProvisionAmendment.marketId} />
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
BlockExplorerTransactionResult,
|
||||
SubmitOrder,
|
||||
} from '../../../routes/types/block-explorer-response';
|
||||
import { BlockLink, PartyLink } from '../../links/';
|
||||
import { MarketLink } from '../../links/';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TimeAgo } from '../../time-ago';
|
||||
|
||||
interface TxDetailsOrderProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* An order type is probably the most interesting type we'll see! Except until:
|
||||
* https://github.com/vegaprotocol/vega/issues/6832 is complete, we can only
|
||||
* fetch the actual transaction and not more details about the order. So for now
|
||||
* this view is very basic
|
||||
*/
|
||||
export const TxDetailsOrder = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsOrderProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const cmd = txData.command as SubmitOrder;
|
||||
const time: string = blockData?.result.block.header.time || '';
|
||||
const height: string = blockData?.result.block.header.height || '';
|
||||
|
||||
return (
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{t('Submitter')}
|
||||
{pubKey ? <PartyLink id={pubKey} /> : '-'}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Block')}
|
||||
<BlockLink height={height} />
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Time')}
|
||||
{time ? (
|
||||
<span>
|
||||
{time} (<TimeAgo date={time} /> )
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Market')}
|
||||
<MarketLink id={cmd.orderSubmission.marketId} />
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Routes as RouteNames } from '../../route-names';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
jest.mock('@vegaprotocol/react-helpers', () => {
|
||||
const original = jest.requireActual('@vegaprotocol/react-helpers');
|
||||
@@ -121,11 +122,13 @@ const createBlockResponse = (id: number = blockId) => {
|
||||
|
||||
const renderComponent = (id: number = blockId) => {
|
||||
return (
|
||||
<MemoryRouter initialEntries={[`/${RouteNames.BLOCKS}/${id}`]}>
|
||||
<Routes>
|
||||
<Route path={`/${RouteNames.BLOCKS}/:block`} element={<Block />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
<MockedProvider>
|
||||
<MemoryRouter initialEntries={[`/${RouteNames.BLOCKS}/${id}`]}>
|
||||
<Routes>
|
||||
<Route path={`/${RouteNames.BLOCKS}/:block`} element={<Block />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -168,11 +171,13 @@ describe('Block', () => {
|
||||
expect(screen.getByTestId('block-header')).toHaveTextContent(
|
||||
`BLOCK ${blockId}`
|
||||
);
|
||||
const expectedValidator = '1C9B6E2708F8217F8D5BFC8D8734ED9A5BC19B21';
|
||||
const proposer = screen.getByTestId('block-validator');
|
||||
expect(proposer).toHaveTextContent(
|
||||
'1C9B6E2708F8217F8D5BFC8D8734ED9A5BC19B21'
|
||||
expect(proposer).toHaveTextContent(expectedValidator);
|
||||
expect(proposer).toHaveAttribute(
|
||||
'href',
|
||||
`/${RouteNames.VALIDATORS}#${expectedValidator}`
|
||||
);
|
||||
expect(proposer).toHaveAttribute('href', `/${RouteNames.VALIDATORS}`);
|
||||
expect(screen.getByTestId('block-time')).toHaveTextContent(
|
||||
'59 minutes ago'
|
||||
);
|
||||
|
||||
@@ -15,8 +15,8 @@ import { TxsPerBlock } from '../../../components/txs/txs-per-block';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { Routes } from '../../route-names';
|
||||
import { RenderFetched } from '../../../components/render-fetched';
|
||||
import { HighlightedLink } from '../../../components/highlighted-link';
|
||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { NodeLink } from '../../../components/links';
|
||||
|
||||
const Block = () => {
|
||||
const { block } = useParams<{ block: string }>();
|
||||
@@ -60,9 +60,8 @@ const Block = () => {
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row">Mined by</TableHeader>
|
||||
<TableCell modifier="bordered">
|
||||
<HighlightedLink
|
||||
to={`/${Routes.VALIDATORS}`}
|
||||
text={blockData.result.block.header.proposer_address}
|
||||
<NodeLink
|
||||
id={blockData.result.block.header.proposer_address}
|
||||
data-testid="block-validator"
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
@@ -8,7 +8,7 @@ export type ExplorerPartyAssetsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } } } } | null> | null } | null } }> } | null };
|
||||
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string } | null } } | null> | null } | null } }> } | null };
|
||||
|
||||
|
||||
export const ExplorerPartyAssetsDocument = gql`
|
||||
@@ -49,6 +49,9 @@ export const ExplorerPartyAssetsDocument = gql`
|
||||
}
|
||||
type
|
||||
balance
|
||||
market {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,29 @@ import { useTxsData } from '../../../hooks/use-txs-data';
|
||||
import { TxsInfiniteList } from '../../../components/txs';
|
||||
import { PageHeader } from '../../../components/page-header';
|
||||
import { useExplorerPartyAssetsQuery } from './__generated__/party-assets';
|
||||
import { MarketLink } from '../../../components/links';
|
||||
|
||||
function getMarketLink(id: string | undefined) {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MarketLink id={id} />;
|
||||
}
|
||||
|
||||
function getTypeLabel(label: string) {
|
||||
switch (label) {
|
||||
case 'ACCOUNT_TYPE_BOND':
|
||||
return 'Bond';
|
||||
case 'ACCOUNT_TYPE_MARGIN':
|
||||
return 'Margin';
|
||||
case 'ACCOUNT_TYPE_GENERAL':
|
||||
return 'General';
|
||||
|
||||
default:
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
const Party = () => {
|
||||
const { party } = useParams<{ party: string }>();
|
||||
@@ -69,16 +92,24 @@ const Party = () => {
|
||||
return (
|
||||
<InfoPanel title={account.asset.name} id={account.asset.id}>
|
||||
<section>
|
||||
<dl className="flex gap-2">
|
||||
<dt className="text-zinc-500 dark:text-zinc-400 text-md">
|
||||
{t('Balance')} ({account.asset.symbol})
|
||||
<dl className="flex gap-2 flex-wrap">
|
||||
<dt className="text-zinc-500 dark:text-zinc-400 text-md flex-1">
|
||||
<p>
|
||||
{t('Balance')} ({account.asset.symbol})
|
||||
</p>
|
||||
</dt>
|
||||
<dd className="text-md">
|
||||
<dd className="text-md flex-2">
|
||||
{addDecimalsFormatNumber(
|
||||
account.balance,
|
||||
account.asset.decimals
|
||||
)}
|
||||
</dd>
|
||||
<dt className="text-zinc-500 dark:text-zinc-400 text-md flex-1">
|
||||
{getTypeLabel(account.type)}
|
||||
</dt>
|
||||
<dd className="text-md flex-2">
|
||||
{getMarketLink(account.market?.id)}
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
</InfoPanel>
|
||||
|
||||
@@ -35,6 +35,9 @@ query ExplorerPartyAssets($partyId: ID!) {
|
||||
}
|
||||
type
|
||||
balance
|
||||
market {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,7 @@ const Tx = () => {
|
||||
</Link>
|
||||
|
||||
<PageHeader
|
||||
title={hash}
|
||||
prefix="Transaction"
|
||||
copy
|
||||
title="transaction"
|
||||
truncateStart={5}
|
||||
truncateEnd={9}
|
||||
className="mb-5"
|
||||
@@ -52,7 +50,9 @@ const Tx = () => {
|
||||
pubKey={data?.transaction.submitter}
|
||||
/>
|
||||
|
||||
<TxContent data={data?.transaction} />
|
||||
<details>
|
||||
<TxContent data={data?.transaction} />
|
||||
</details>
|
||||
</>
|
||||
</RenderFetched>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { StatusMessage } from '../../../components/status-message';
|
||||
import { NestedDataList } from '../../../components/nested-data-list';
|
||||
import type { UnknownObject } from '../../../components/nested-data-list';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
|
||||
interface TxContentProps {
|
||||
@@ -16,5 +17,5 @@ export const TxContent = ({ data }: TxContentProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
return <NestedDataList data={data.command} />;
|
||||
return <NestedDataList data={data.command as unknown as UnknownObject} />;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { TxDetails } from './tx-details';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type {
|
||||
BlockExplorerTransactionResult,
|
||||
ValidatorHeartbeat,
|
||||
} from '../../../routes/types/block-explorer-response';
|
||||
|
||||
const pubKey = 'test';
|
||||
const hash = '7416753A30622A9E24A06F0172D6C33A95186B36806D96345C6DC5A23FA3F283';
|
||||
@@ -15,7 +18,7 @@ const txData: BlockExplorerTransactionResult = {
|
||||
code: 0,
|
||||
cursor: `${height}.0`,
|
||||
type: 'type',
|
||||
command: {},
|
||||
command: {} as ValidatorHeartbeat,
|
||||
};
|
||||
|
||||
const renderComponent = (txData: BlockExplorerTransactionResult) => (
|
||||
@@ -29,9 +32,4 @@ describe('Transaction details', () => {
|
||||
render(renderComponent(txData));
|
||||
expect(screen.getByText(pubKey)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the height', () => {
|
||||
render(renderComponent(txData));
|
||||
expect(screen.getByText(height)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { BlockExplorerTransactionResult } from '../../../routes/types/block
|
||||
import React from 'react';
|
||||
import { TruncateInline } from '../../../components/truncate/truncate';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { TxDetailsWrapper } from '../../../components/txs/details/tx-details-wrapper';
|
||||
|
||||
interface TxDetailsProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -24,7 +25,7 @@ export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
|
||||
|
||||
return (
|
||||
<section className="mb-10">
|
||||
<h3 className="text-3xl xl:text-4xl uppercase font-alpha mb-4">
|
||||
<h3 className="text-l xl:text-l uppercase mb-4">
|
||||
{txData.type} by{' '}
|
||||
<Link
|
||||
className="font-bold underline"
|
||||
@@ -33,17 +34,7 @@ export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
|
||||
{truncatedSubmitter}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-xl xl:text-2xl uppercase font-alpha">
|
||||
Block{' '}
|
||||
<Link
|
||||
className="font-bold underline"
|
||||
to={`/${Routes.BLOCKS}/${txData.block}`}
|
||||
>
|
||||
{txData.block}
|
||||
</Link>
|
||||
{', '}
|
||||
Index {txData.index}
|
||||
</p>
|
||||
<TxDetailsWrapper height={txData.block} txData={txData} pubKey={pubKey} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { UnknownObject } from '../../components/nested-data-list';
|
||||
|
||||
export interface BlockExplorerTransactionResult {
|
||||
block: string;
|
||||
index: number;
|
||||
@@ -6,7 +8,11 @@ export interface BlockExplorerTransactionResult {
|
||||
type: string;
|
||||
code: number;
|
||||
cursor: string;
|
||||
command: Record<string, unknown>;
|
||||
command:
|
||||
| ValidatorHeartbeat
|
||||
| SubmitOrder
|
||||
| StateVariableProposal
|
||||
| AmendLiquidityProvisionOrder;
|
||||
}
|
||||
|
||||
export interface BlockExplorerTransactions {
|
||||
@@ -16,3 +22,58 @@ export interface BlockExplorerTransactions {
|
||||
export interface BlockExplorerTransaction {
|
||||
transaction: BlockExplorerTransactionResult;
|
||||
}
|
||||
|
||||
export interface ValidatorHeartbeat {
|
||||
blockHeight: string;
|
||||
nonce: string;
|
||||
validatorHeartbeat: {
|
||||
nodeId: string;
|
||||
ethereumSignature: ValidatorHeartbeatSignature;
|
||||
vegaSignature: ValidatorHeartbeatSignature;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ValidatorHeartbeatSignature {
|
||||
algo: string;
|
||||
value: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface SubmitOrder {
|
||||
orderSubmission: {
|
||||
marketId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StateVariableProposal {
|
||||
proposal: {
|
||||
stateVarId: string;
|
||||
eventId: string;
|
||||
kvb: StateVariableProposalValues[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface StateVariableProposalValues {
|
||||
key: 'up' | 'down';
|
||||
tolerance: string;
|
||||
value: UnknownObject;
|
||||
}
|
||||
|
||||
export interface AmendLiquidityProvisionOrder {
|
||||
blockHeight: string;
|
||||
nonce: string;
|
||||
liquidityProvisionAmendment: {
|
||||
marketId: string;
|
||||
commitmentAmount: string;
|
||||
fee: string;
|
||||
sells: LiquidityProvisionOrderChange[];
|
||||
buys: LiquidityProvisionOrderChange[];
|
||||
reference: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LiquidityProvisionOrderChange {
|
||||
string: Reference;
|
||||
proportion: number;
|
||||
offset: string;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const defaultOptions = {} as const;
|
||||
export type ExplorerNodesQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, status: Types.NodeStatus, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } };
|
||||
export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, status: Types.NodeStatus, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } };
|
||||
|
||||
|
||||
export const ExplorerNodesDocument = gql`
|
||||
@@ -22,6 +22,7 @@ export const ExplorerNodesDocument = gql`
|
||||
tmPubkey
|
||||
ethereumAddress
|
||||
location
|
||||
status
|
||||
stakedByOperator
|
||||
stakedByDelegates
|
||||
stakedTotal
|
||||
@@ -31,8 +32,6 @@ export const ExplorerNodesDocument = gql`
|
||||
offline
|
||||
online
|
||||
}
|
||||
status
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ query ExplorerNodes {
|
||||
tmPubkey
|
||||
ethereumAddress
|
||||
location
|
||||
status
|
||||
stakedByOperator
|
||||
stakedByDelegates
|
||||
stakedTotal
|
||||
@@ -19,8 +20,6 @@ query ExplorerNodes {
|
||||
offline
|
||||
online
|
||||
}
|
||||
status
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user