feat(explorer): add specific link types, start specific tx views (#2152)
* feat(explorer): add specific link types, start specific tx views * feat(explorer): add raw tx view * chore(explorer): remove default nx vega url * fix(explorer): improve prop typing for link components * fix(explorer): remove console log * fix(explorer): remove unused import * fix(explorer): refactor txdetails component selection
This commit is contained in:
parent
8e5012891c
commit
8f8a727b4d
@ -2,7 +2,6 @@
|
|||||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
||||||
NX_TENDERMINT_URL=http://localhost:26617
|
NX_TENDERMINT_URL=http://localhost:26617
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||||
NX_VEGA_URL=http://localhost:3028/query
|
|
||||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=CUSTOM
|
NX_VEGA_ENV=CUSTOM
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
|
@ -3,7 +3,6 @@ NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-exp
|
|||||||
NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
|
NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
||||||
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
|
|
||||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=DEVNET
|
NX_VEGA_ENV=DEVNET
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
|
@ -3,7 +3,6 @@ NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-exp
|
|||||||
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
|
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
|
||||||
NX_VEGA_URL=https://api.vega.xyz/query
|
|
||||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=MAINNET
|
NX_VEGA_ENV=MAINNET
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_VEGA_URL=https://api.sandbox.vega.xyz/graphql
|
|
||||||
NX_VEGA_ENV=SANDBOX
|
NX_VEGA_ENV=SANDBOX
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json
|
||||||
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
||||||
|
@ -6,7 +6,6 @@ NX_VEGA_ENV=STAGNET1
|
|||||||
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
|
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
|
||||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
|
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
|
||||||
NX_VEGA_URL=https://api.n00.stagnet1.vega.xyz/graphql
|
|
||||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
|
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||||
|
@ -3,7 +3,6 @@ NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-exp
|
|||||||
NX_TENDERMINT_URL=https://n01.stagnet3.vega.xyz/tm
|
NX_TENDERMINT_URL=https://n01.stagnet3.vega.xyz/tm
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://n01.stagnet3.vega.xyz/tm/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://n01.stagnet3.vega.xyz/tm/websocket
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||||
NX_VEGA_URL=https://api.n01.stagnet3.vega.xyz/graphql
|
|
||||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=STAGNET3
|
NX_VEGA_ENV=STAGNET3
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
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_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_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_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=TESTNET
|
NX_VEGA_ENV=TESTNET
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
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_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||||
|
NX_VEGA_NETWORKS={}
|
@ -2,7 +2,6 @@
|
|||||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
||||||
NX_TENDERMINT_URL=http://localhost:26607/
|
NX_TENDERMINT_URL=http://localhost:26607/
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
|
||||||
NX_VEGA_URL=http://localhost:3003/query
|
|
||||||
NX_VEGA_ENV=CUSTOM
|
NX_VEGA_ENV=CUSTOM
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_BLOCK_EXPLORER=
|
NX_BLOCK_EXPLORER=
|
||||||
|
@ -45,22 +45,21 @@ yarn nx run explorer:serve --env={env} # e.g. stagnet3
|
|||||||
|
|
||||||
There are a few different configuration options offered for this app:
|
There are a few different configuration options offered for this app:
|
||||||
|
|
||||||
| **Flag** | **Purpose** |
|
| **Flag** | **Purpose** |
|
||||||
| -------------------------------- | ---------------------------------------------------------------------------------------------------- | --- |
|
| -------------------------------- | --------------------------------------------------------------- | --- |
|
||||||
| `NX_CHAIN_EXPLORER_URL` | The URL of the chain explorer service for decoding transactions |
|
| `NX_CHAIN_EXPLORER_URL` | The URL of the chain explorer service for decoding transactions |
|
||||||
| `NX_TENDERMINT_URL` | The Tendermint REST URL for the Vega consesus engine |
|
| `NX_TENDERMINT_URL` | The Tendermint REST URL for the Vega consesus engine |
|
||||||
| `NX_TENDERMINT_WEBSOCKET_URL` | The Tendermint Websocket URL for the Vega consensus engine |
|
| `NX_TENDERMINT_WEBSOCKET_URL` | The Tendermint Websocket URL for the Vega consensus engine |
|
||||||
| `NX_VEGA_URL` | The GraphQl query endpoint of a [Vega data node](https://github.com/vegaprotocol/networks#data-node) |
|
| `NX_VEGA_ENV` | The name of the currently connected vega environment | |
|
||||||
| `NX_VEGA_ENV` | The name of the currently connected vega environment | |
|
| `NX_EXPLORER_ASSETS` | Enable the assets page for the explorer |
|
||||||
| `NX_EXPLORER_ASSETS` | Enable the assets page for the explorer |
|
| `NX_EXPLORER_GENESIS` | Enable the genesis page for the explorer |
|
||||||
| `NX_EXPLORER_GENESIS` | Enable the genesis page for the explorer |
|
| `NX_EXPLORER_GOVERNANCE` | Enable the governance page for the explorer |
|
||||||
| `NX_EXPLORER_GOVERNANCE` | Enable the governance page for the explorer |
|
| `NX_EXPLORER_MARKETS` | Enable the markets page for the explorer |
|
||||||
| `NX_EXPLORER_MARKETS` | Enable the markets page for the explorer |
|
| `NX_EXPLORER_ORACLES` | Enable the oracles page for the explorer |
|
||||||
| `NX_EXPLORER_ORACLES` | Enable the oracles page for the explorer |
|
| `NX_EXPLORER_TXS_LIST` | Enable the transactions list page for the explorer |
|
||||||
| `NX_EXPLORER_TXS_LIST` | Enable the transactions list page for the explorer |
|
| `NX_EXPLORER_NETWORK_PARAMETERS` | Enable the network parameters page for the explorer |
|
||||||
| `NX_EXPLORER_NETWORK_PARAMETERS` | Enable the network parameters page for the explorer |
|
| `NX_EXPLORER_PARTIES` | Enable the parties page for the explorer |
|
||||||
| `NX_EXPLORER_PARTIES` | Enable the parties page for the explorer |
|
| `NX_EXPLORER_VALIDATORS` | Enable the validators page for the explorer |
|
||||||
| `NX_EXPLORER_VALIDATORS` | Enable the validators page for the explorer |
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
query ExplorerAsset($id: ID!) {
|
||||||
|
asset(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
status
|
||||||
|
decimals
|
||||||
|
}
|
||||||
|
}
|
51
apps/explorer/src/app/components/links/asset-link/__generated__/Asset.ts
generated
Normal file
51
apps/explorer/src/app/components/links/asset-link/__generated__/Asset.ts
generated
Normal file
@ -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,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Routes } from '../../../routes/route-names';
|
||||||
|
import { useExplorerAssetQuery } from './__generated__/Asset';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
|
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" {...props} to={`/${Routes.MARKETS}#${id}`}>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssetLink;
|
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Routes } from '../../../routes/route-names';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type BlockLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
|
height: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BlockLink = ({ height, ...props }: BlockLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link className="underline" {...props} to={`/${Routes.BLOCKS}/${height}`}>
|
||||||
|
{height}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockLink;
|
5
apps/explorer/src/app/components/links/index.ts
Normal file
5
apps/explorer/src/app/components/links/index.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
54
apps/explorer/src/app/components/links/market-link/__generated__/Market.ts
generated
Normal file
54
apps/explorer/src/app/components/links/market-link/__generated__/Market.ts
generated
Normal file
@ -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,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Routes } from '../../../routes/route-names';
|
||||||
|
import { useExplorerMarketQuery } from './__generated__/Market';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
|
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" {...props} to={`/${Routes.MARKETS}#${id}`}>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketLink;
|
@ -0,0 +1,7 @@
|
|||||||
|
query ExplorerNode($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
50
apps/explorer/src/app/components/links/node-link/__generated__/Node.ts
generated
Normal file
50
apps/explorer/src/app/components/links/node-link/__generated__/Node.ts
generated
Normal file
@ -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,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Routes } from '../../../routes/route-names';
|
||||||
|
import { useExplorerNodeQuery } from './__generated__/Node';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type NodeLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
|
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" {...props} to={`/${Routes.VALIDATORS}#${id}`}>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NodeLink;
|
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Routes } from '../../../routes/route-names';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link className="underline" {...props} to={`/${Routes.PARTIES}/${id}`}>
|
||||||
|
{id}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PartyLink;
|
@ -41,10 +41,10 @@ describe('NestedDataList', () => {
|
|||||||
const parent = getAllByRole('listitem', { name: 'Validator Heartbeat' });
|
const parent = getAllByRole('listitem', { name: 'Validator Heartbeat' });
|
||||||
const nestedContainer = parent[0].querySelector('[aria-hidden]');
|
const nestedContainer = parent[0].querySelector('[aria-hidden]');
|
||||||
const expandBtn = parent[0].querySelector('button');
|
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 user.click(expandBtn as HTMLButtonElement);
|
||||||
await waitFor(() => nestedContainer);
|
await waitFor(() => nestedContainer);
|
||||||
expect(nestedContainer).toHaveAttribute('aria-hidden', 'false');
|
expect(nestedContainer).toHaveAttribute('aria-hidden', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('add border to the title of the parent', () => {
|
it('add border to the title of the parent', () => {
|
||||||
|
@ -55,7 +55,7 @@ const NestedDataListItem = ({
|
|||||||
value,
|
value,
|
||||||
index,
|
index,
|
||||||
}: NestedDataListItemProps) => {
|
}: NestedDataListItemProps) => {
|
||||||
const [isCollapsed, setCollapsed] = useState(true);
|
const [isCollapsed, setCollapsed] = useState(false);
|
||||||
const toggleVisible = useCallback(
|
const toggleVisible = useCallback(
|
||||||
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { TableRow, TableCell } from '../../../table';
|
||||||
|
import { BlockLink, PartyLink } from '../../../links/';
|
||||||
|
import { TimeAgo } from '../../../time-ago';
|
||||||
|
|
||||||
|
import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
|
||||||
|
interface TxDetailsSharedProps {
|
||||||
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
blockData: TendermintBlocksResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These rows are shown for every transaction type, providing a consistent set of rows for the top
|
||||||
|
* of a transaction details row. The order is relatively arbitrary but felt right - it might need to
|
||||||
|
* change as the views get more bespoke.
|
||||||
|
*/
|
||||||
|
export const TxDetailsShared = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxDetailsSharedProps) => {
|
||||||
|
if (!txData) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time: string = blockData?.result.block.header.time || '';
|
||||||
|
const height: string = blockData?.result.block.header.height || '';
|
||||||
|
let timeFormatted = '';
|
||||||
|
|
||||||
|
if (time) {
|
||||||
|
timeFormatted = new Date(time).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Hash')}</TableCell>
|
||||||
|
<TableCell>{txData.hash}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Submitter')}</TableCell>
|
||||||
|
<TableCell>{pubKey ? <PartyLink id={pubKey} /> : '-'}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Block')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<BlockLink height={height} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Time')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{time ? (
|
||||||
|
<div>
|
||||||
|
<span className="mr-5">{timeFormatted} </span>
|
||||||
|
<span>
|
||||||
|
<TimeAgo date={time} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
75
apps/explorer/src/app/components/txs/details/tx-batch.tsx
Normal file
75
apps/explorer/src/app/components/txs/details/tx-batch.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
BlockExplorerTransactionResult,
|
||||||
|
BatchMarketInstructions,
|
||||||
|
} from '../../../routes/types/block-explorer-response';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||||
|
import { TableWithTbody, TableRow, TableCell } from '../../table';
|
||||||
|
|
||||||
|
interface TxDetailsBatchProps {
|
||||||
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
blockData: TendermintBlocksResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BatchMarketInstructions are sets of changes in three categories: new orders (submissions),
|
||||||
|
* order change s(amendments) and cancellations.
|
||||||
|
*
|
||||||
|
* Design considerations for batch:
|
||||||
|
* - So far it's very basic details about the size of the batch
|
||||||
|
*/
|
||||||
|
export const TxDetailsBatch = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxDetailsBatchProps) => {
|
||||||
|
if (!txData) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = txData.command as BatchMarketInstructions;
|
||||||
|
|
||||||
|
const batchSubmissions = cmd.batchMarketInstructions.submissions.length;
|
||||||
|
const batchAmendments = cmd.batchMarketInstructions.amendments.length;
|
||||||
|
const batchCancellations = cmd.batchMarketInstructions.cancellations.length;
|
||||||
|
const batchTotal = batchSubmissions + batchAmendments + batchCancellations;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableWithTbody>
|
||||||
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Batch size')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span>{batchTotal}</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>
|
||||||
|
<span className="ml-5">{t('Submissions')}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span>{batchSubmissions}</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>
|
||||||
|
<span className="ml-5">{t('Amendments')}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span>{batchAmendments}</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>
|
||||||
|
<span className="ml-5">{t('Cancellations')}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span>{batchCancellations}</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableWithTbody>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
BlockExplorerTransactionResult,
|
||||||
|
ChainEvent,
|
||||||
|
} from '../../../routes/types/block-explorer-response';
|
||||||
|
import { AssetLink, PartyLink } from '../../links';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||||
|
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||||
|
|
||||||
|
interface TxDetailsChainEventProps {
|
||||||
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
blockData: TendermintBlocksResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chain events are external blockchain events (e.g. Ethereum) reported by bridge
|
||||||
|
* Multiple events will relay the same data, from each validator, so that the
|
||||||
|
* deposit/withdrawal can be verified independently.
|
||||||
|
*
|
||||||
|
* Design considerations so far:
|
||||||
|
* - The ethereum address should be a link to an Ethereum explorer
|
||||||
|
* - Sender and recipient are shown because they are easy
|
||||||
|
* - Amount is not shown because there is no formatter by asset component
|
||||||
|
*/
|
||||||
|
export const TxDetailsChainEvent = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxDetailsChainEventProps) => {
|
||||||
|
if (!txData) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
const cmd = txData.command as ChainEvent;
|
||||||
|
const assetId = cmd.chainEvent.erc20.deposit.vegaAssetId;
|
||||||
|
const sender = cmd.chainEvent.erc20.deposit.sourceEthereumAddress;
|
||||||
|
const recipient = cmd.chainEvent.erc20.deposit.targetPartyId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableWithTbody>
|
||||||
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Asset')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<AssetLink id={assetId} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Sender')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span>{sender}</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Recipient')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<PartyLink id={recipient} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableWithTbody>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,83 @@
|
|||||||
|
import React, { useMemo } 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';
|
||||||
|
import { TxDetailsGeneric } from './tx-generic';
|
||||||
|
import { TxDetailsBatch } from './tx-batch';
|
||||||
|
import { TxDetailsChainEvent } from './tx-chain-event';
|
||||||
|
import { TxContent } from '../../../routes/txs/id/tx-content';
|
||||||
|
|
||||||
|
type resultOrNull = BlockExplorerTransactionResult | undefined;
|
||||||
|
|
||||||
|
interface TxDetailsWrapperProps {
|
||||||
|
txData: resultOrNull;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
height: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TxDetailsWrapper = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
height,
|
||||||
|
}: TxDetailsWrapperProps) => {
|
||||||
|
const {
|
||||||
|
state: { data: blockData },
|
||||||
|
} = useFetch<TendermintBlocksResponse>(
|
||||||
|
`${DATA_SOURCES.tendermintUrl}/block?height=${height}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const child = useMemo(() => getTransactionComponent(txData), [txData]);
|
||||||
|
|
||||||
|
if (!txData) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>{child({ txData, pubKey, blockData })}</section>
|
||||||
|
|
||||||
|
<details title={t('Decoded transaction')} className="mt-3">
|
||||||
|
<summary className="cursor-pointer">{t('Decoded transaction')}</summary>
|
||||||
|
<TxContent data={txData} />
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details title={t('Raw transaction')} className="mt-3">
|
||||||
|
<summary className="cursor-pointer">{t('Raw transaction')}</summary>
|
||||||
|
<code className="break-all font-mono text-xs">
|
||||||
|
{blockData?.result.block.data.txs[txData.index]}
|
||||||
|
</code>
|
||||||
|
</details>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses the appropriate component to render the full details of a transaction
|
||||||
|
*
|
||||||
|
* @param txData
|
||||||
|
* @returns JSX.Element
|
||||||
|
*/
|
||||||
|
function getTransactionComponent(txData: resultOrNull) {
|
||||||
|
if (!txData) {
|
||||||
|
return TxDetailsGeneric;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (txData.type) {
|
||||||
|
case 'Submit Order':
|
||||||
|
return TxDetailsOrder;
|
||||||
|
case 'Validator Heartbeat':
|
||||||
|
return TxDetailsHeartbeat;
|
||||||
|
case 'Amend LiquidityProvision Order':
|
||||||
|
return TxDetailsLPAmend;
|
||||||
|
case 'Batch Market Instructions':
|
||||||
|
return TxDetailsBatch;
|
||||||
|
case 'Chain Event':
|
||||||
|
return TxDetailsChainEvent;
|
||||||
|
default:
|
||||||
|
return TxDetailsGeneric;
|
||||||
|
}
|
||||||
|
}
|
32
apps/explorer/src/app/components/txs/details/tx-generic.tsx
Normal file
32
apps/explorer/src/app/components/txs/details/tx-generic.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
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 { TableWithTbody } from '../../table';
|
||||||
|
|
||||||
|
interface TxDetailsGenericProps {
|
||||||
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
blockData: TendermintBlocksResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is not yet a custom component for a transaction, just display
|
||||||
|
* the basic details. This allows someone to view the decoded transaction.
|
||||||
|
*/
|
||||||
|
export const TxDetailsGeneric = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxDetailsGenericProps) => {
|
||||||
|
if (!txData) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableWithTbody>
|
||||||
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
|
</TableWithTbody>
|
||||||
|
);
|
||||||
|
};
|
86
apps/explorer/src/app/components/txs/details/tx-hearbeat.tsx
Normal file
86
apps/explorer/src/app/components/txs/details/tx-hearbeat.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
BlockExplorerTransactionResult,
|
||||||
|
ValidatorHeartbeat,
|
||||||
|
} from '../../../routes/types/block-explorer-response';
|
||||||
|
import { BlockLink, NodeLink } from '../../links/';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||||
|
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableWithTbody>
|
||||||
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Node')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<NodeLink id={cmd.validatorHeartbeat.nodeId} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Signed block height')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<BlockLink height={cmd.blockHeight} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Freshness (lower is better)')}</TableCell>
|
||||||
|
<TableCell>{scoreFreshness(txData.block, cmd.blockHeight)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableWithTbody>
|
||||||
|
);
|
||||||
|
};
|
45
apps/explorer/src/app/components/txs/details/tx-lp-amend.tsx
Normal file
45
apps/explorer/src/app/components/txs/details/tx-lp-amend.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
AmendLiquidityProvisionOrder,
|
||||||
|
BlockExplorerTransactionResult,
|
||||||
|
} from '../../../routes/types/block-explorer-response';
|
||||||
|
import { MarketLink } from '../../links/';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||||
|
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||||
|
|
||||||
|
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. So far this only displays the market, which is only
|
||||||
|
* because it's very easy to do so.
|
||||||
|
*/
|
||||||
|
export const TxDetailsLPAmend = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxDetailsOrderProps) => {
|
||||||
|
if (!txData) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = txData.command as AmendLiquidityProvisionOrder;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableWithTbody>
|
||||||
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Market')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<MarketLink id={cmd.liquidityProvisionAmendment.marketId} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableWithTbody>
|
||||||
|
);
|
||||||
|
};
|
46
apps/explorer/src/app/components/txs/details/tx-order.tsx
Normal file
46
apps/explorer/src/app/components/txs/details/tx-order.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
BlockExplorerTransactionResult,
|
||||||
|
SubmitOrder,
|
||||||
|
} from '../../../routes/types/block-explorer-response';
|
||||||
|
import { MarketLink } from '../../links/';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||||
|
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableWithTbody>
|
||||||
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Market')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<MarketLink id={cmd.orderSubmission.marketId} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableWithTbody>
|
||||||
|
);
|
||||||
|
};
|
@ -3,6 +3,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||||
import { Routes as RouteNames } from '../../route-names';
|
import { Routes as RouteNames } from '../../route-names';
|
||||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/react-helpers', () => {
|
jest.mock('@vegaprotocol/react-helpers', () => {
|
||||||
const original = jest.requireActual('@vegaprotocol/react-helpers');
|
const original = jest.requireActual('@vegaprotocol/react-helpers');
|
||||||
@ -121,11 +122,13 @@ const createBlockResponse = (id: number = blockId) => {
|
|||||||
|
|
||||||
const renderComponent = (id: number = blockId) => {
|
const renderComponent = (id: number = blockId) => {
|
||||||
return (
|
return (
|
||||||
<MemoryRouter initialEntries={[`/${RouteNames.BLOCKS}/${id}`]}>
|
<MockedProvider>
|
||||||
<Routes>
|
<MemoryRouter initialEntries={[`/${RouteNames.BLOCKS}/${id}`]}>
|
||||||
<Route path={`/${RouteNames.BLOCKS}/:block`} element={<Block />} />
|
<Routes>
|
||||||
</Routes>
|
<Route path={`/${RouteNames.BLOCKS}/:block`} element={<Block />} />
|
||||||
</MemoryRouter>
|
</Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -168,11 +171,13 @@ describe('Block', () => {
|
|||||||
expect(screen.getByTestId('block-header')).toHaveTextContent(
|
expect(screen.getByTestId('block-header')).toHaveTextContent(
|
||||||
`BLOCK ${blockId}`
|
`BLOCK ${blockId}`
|
||||||
);
|
);
|
||||||
|
const expectedValidator = '1C9B6E2708F8217F8D5BFC8D8734ED9A5BC19B21';
|
||||||
const proposer = screen.getByTestId('block-validator');
|
const proposer = screen.getByTestId('block-validator');
|
||||||
expect(proposer).toHaveTextContent(
|
expect(proposer).toHaveTextContent(expectedValidator);
|
||||||
'1C9B6E2708F8217F8D5BFC8D8734ED9A5BC19B21'
|
expect(proposer).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
`/${RouteNames.VALIDATORS}#${expectedValidator}`
|
||||||
);
|
);
|
||||||
expect(proposer).toHaveAttribute('href', `/${RouteNames.VALIDATORS}`);
|
|
||||||
expect(screen.getByTestId('block-time')).toHaveTextContent(
|
expect(screen.getByTestId('block-time')).toHaveTextContent(
|
||||||
'59 minutes ago'
|
'59 minutes ago'
|
||||||
);
|
);
|
||||||
|
@ -15,8 +15,8 @@ import { TxsPerBlock } from '../../../components/txs/txs-per-block';
|
|||||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
import { Routes } from '../../route-names';
|
import { Routes } from '../../route-names';
|
||||||
import { RenderFetched } from '../../../components/render-fetched';
|
import { RenderFetched } from '../../../components/render-fetched';
|
||||||
import { HighlightedLink } from '../../../components/highlighted-link';
|
|
||||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||||
|
import { NodeLink } from '../../../components/links';
|
||||||
|
|
||||||
const Block = () => {
|
const Block = () => {
|
||||||
const { block } = useParams<{ block: string }>();
|
const { block } = useParams<{ block: string }>();
|
||||||
@ -60,9 +60,8 @@ const Block = () => {
|
|||||||
<TableRow modifier="bordered">
|
<TableRow modifier="bordered">
|
||||||
<TableHeader scope="row">Mined by</TableHeader>
|
<TableHeader scope="row">Mined by</TableHeader>
|
||||||
<TableCell modifier="bordered">
|
<TableCell modifier="bordered">
|
||||||
<HighlightedLink
|
<NodeLink
|
||||||
to={`/${Routes.VALIDATORS}`}
|
id={blockData.result.block.header.proposer_address}
|
||||||
text={blockData.result.block.header.proposer_address}
|
|
||||||
data-testid="block-validator"
|
data-testid="block-validator"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -80,6 +79,12 @@ const Block = () => {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableHeader scope="row">Transactions</TableHeader>
|
||||||
|
<TableCell modifier="bordered">
|
||||||
|
<span>{blockData.result.block.data.txs.length}</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
</TableWithTbody>
|
</TableWithTbody>
|
||||||
{blockData.result.block.data.txs.length > 0 ? (
|
{blockData.result.block.data.txs.length > 0 ? (
|
||||||
<TxsPerBlock blockHeight={block} />
|
<TxsPerBlock blockHeight={block} />
|
||||||
|
@ -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`
|
export const ExplorerPartyAssetsDocument = gql`
|
||||||
@ -49,6 +49,9 @@ export const ExplorerPartyAssetsDocument = gql`
|
|||||||
}
|
}
|
||||||
type
|
type
|
||||||
balance
|
balance
|
||||||
|
market {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,9 +69,11 @@ const Party = () => {
|
|||||||
return (
|
return (
|
||||||
<InfoPanel title={account.asset.name} id={account.asset.id}>
|
<InfoPanel title={account.asset.name} id={account.asset.id}>
|
||||||
<section>
|
<section>
|
||||||
<dl className="flex gap-2">
|
<dl className="flex gap-2 flex-wrap">
|
||||||
<dt className="text-zinc-500 dark:text-zinc-400 text-md">
|
<dt className="text-zinc-500 dark:text-zinc-400 text-md">
|
||||||
{t('Balance')} ({account.asset.symbol})
|
<p>
|
||||||
|
{t('Balance')} ({account.asset.symbol})
|
||||||
|
</p>
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="text-md">
|
<dd className="text-md">
|
||||||
{addDecimalsFormatNumber(
|
{addDecimalsFormatNumber(
|
||||||
|
@ -35,6 +35,9 @@ query ExplorerPartyAssets($partyId: ID!) {
|
|||||||
}
|
}
|
||||||
type
|
type
|
||||||
balance
|
balance
|
||||||
|
market {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { Link, useParams } from 'react-router-dom';
|
|||||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||||
import { DATA_SOURCES } from '../../../config';
|
import { DATA_SOURCES } from '../../../config';
|
||||||
import { RenderFetched } from '../../../components/render-fetched';
|
import { RenderFetched } from '../../../components/render-fetched';
|
||||||
import { TxContent } from './tx-content';
|
|
||||||
import { TxDetails } from './tx-details';
|
import { TxDetails } from './tx-details';
|
||||||
import type { BlockExplorerTransaction } from '../../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransaction } from '../../../routes/types/block-explorer-response';
|
||||||
import { toNonHex } from '../../../components/search/detect-search';
|
import { toNonHex } from '../../../components/search/detect-search';
|
||||||
@ -36,24 +35,18 @@ const Tx = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={hash}
|
title="transaction"
|
||||||
prefix="Transaction"
|
|
||||||
copy
|
|
||||||
truncateStart={5}
|
truncateStart={5}
|
||||||
truncateEnd={9}
|
truncateEnd={9}
|
||||||
className="mb-5"
|
className="mb-5"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RenderFetched error={tTxError} loading={tTxLoading}>
|
<RenderFetched error={tTxError} loading={tTxLoading}>
|
||||||
<>
|
<TxDetails
|
||||||
<TxDetails
|
className="mb-28"
|
||||||
className="mb-28"
|
txData={data?.transaction}
|
||||||
txData={data?.transaction}
|
pubKey={data?.transaction.submitter}
|
||||||
pubKey={data?.transaction.submitter}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<TxContent data={data?.transaction} />
|
|
||||||
</>
|
|
||||||
</RenderFetched>
|
</RenderFetched>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { StatusMessage } from '../../../components/status-message';
|
import { StatusMessage } from '../../../components/status-message';
|
||||||
import { NestedDataList } from '../../../components/nested-data-list';
|
import { NestedDataList } from '../../../components/nested-data-list';
|
||||||
|
import type { UnknownObject } from '../../../components/nested-data-list';
|
||||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||||
|
|
||||||
interface TxContentProps {
|
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,9 +1,14 @@
|
|||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { TxDetails } from './tx-details';
|
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';
|
// Note: Long enough that there is a truncated output and a full output
|
||||||
|
const pubKey =
|
||||||
|
'67755549e43e95f0697f83b2bf419c6ccc18eee32a8a61b8ba6f59471b86fbef';
|
||||||
const hash = '7416753A30622A9E24A06F0172D6C33A95186B36806D96345C6DC5A23FA3F283';
|
const hash = '7416753A30622A9E24A06F0172D6C33A95186B36806D96345C6DC5A23FA3F283';
|
||||||
const height = '52987';
|
const height = '52987';
|
||||||
|
|
||||||
@ -15,7 +20,7 @@ const txData: BlockExplorerTransactionResult = {
|
|||||||
code: 0,
|
code: 0,
|
||||||
cursor: `${height}.0`,
|
cursor: `${height}.0`,
|
||||||
type: 'type',
|
type: 'type',
|
||||||
command: {},
|
command: {} as ValidatorHeartbeat,
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderComponent = (txData: BlockExplorerTransactionResult) => (
|
const renderComponent = (txData: BlockExplorerTransactionResult) => (
|
||||||
@ -25,13 +30,9 @@ const renderComponent = (txData: BlockExplorerTransactionResult) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe('Transaction details', () => {
|
describe('Transaction details', () => {
|
||||||
it('Renders the pubKey', () => {
|
it('Renders the details common to all txs', () => {
|
||||||
render(renderComponent(txData));
|
render(renderComponent(txData));
|
||||||
expect(screen.getByText(pubKey)).toBeInTheDocument();
|
expect(screen.getByText(pubKey)).toBeInTheDocument();
|
||||||
});
|
expect(screen.getByText(hash)).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 React from 'react';
|
||||||
import { TruncateInline } from '../../../components/truncate/truncate';
|
import { TruncateInline } from '../../../components/truncate/truncate';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { TxDetailsWrapper } from '../../../components/txs/details/tx-details-wrapper';
|
||||||
|
|
||||||
interface TxDetailsProps {
|
interface TxDetailsProps {
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
@ -24,7 +25,7 @@ export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mb-10">
|
<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{' '}
|
{txData.type} by{' '}
|
||||||
<Link
|
<Link
|
||||||
className="font-bold underline"
|
className="font-bold underline"
|
||||||
@ -33,17 +34,7 @@ export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
|
|||||||
{truncatedSubmitter}
|
{truncatedSubmitter}
|
||||||
</Link>
|
</Link>
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xl xl:text-2xl uppercase font-alpha">
|
<TxDetailsWrapper height={txData.block} txData={txData} pubKey={pubKey} />
|
||||||
Block{' '}
|
|
||||||
<Link
|
|
||||||
className="font-bold underline"
|
|
||||||
to={`/${Routes.BLOCKS}/${txData.block}`}
|
|
||||||
>
|
|
||||||
{txData.block}
|
|
||||||
</Link>
|
|
||||||
{', '}
|
|
||||||
Index {txData.index}
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { UnknownObject } from '../../components/nested-data-list';
|
||||||
|
|
||||||
export interface BlockExplorerTransactionResult {
|
export interface BlockExplorerTransactionResult {
|
||||||
block: string;
|
block: string;
|
||||||
index: number;
|
index: number;
|
||||||
@ -6,7 +8,13 @@ export interface BlockExplorerTransactionResult {
|
|||||||
type: string;
|
type: string;
|
||||||
code: number;
|
code: number;
|
||||||
cursor: string;
|
cursor: string;
|
||||||
command: Record<string, unknown>;
|
command:
|
||||||
|
| ValidatorHeartbeat
|
||||||
|
| SubmitOrder
|
||||||
|
| StateVariableProposal
|
||||||
|
| AmendLiquidityProvisionOrder
|
||||||
|
| BatchMarketInstructions
|
||||||
|
| ChainEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockExplorerTransactions {
|
export interface BlockExplorerTransactions {
|
||||||
@ -16,3 +24,95 @@ export interface BlockExplorerTransactions {
|
|||||||
export interface BlockExplorerTransaction {
|
export interface BlockExplorerTransaction {
|
||||||
transaction: BlockExplorerTransactionResult;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchMarketInstructions {
|
||||||
|
blockHeight: string;
|
||||||
|
nonce: string;
|
||||||
|
batchMarketInstructions: {
|
||||||
|
amendments: BatchInstruction[];
|
||||||
|
submissions: BatchInstruction[];
|
||||||
|
cancellations: BatchCancellationInstruction[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchInstruction {
|
||||||
|
orderId: string;
|
||||||
|
marketId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchCancellationInstruction {
|
||||||
|
orderId: string;
|
||||||
|
marketId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChainEvent {
|
||||||
|
blockHeight: string;
|
||||||
|
nonce: string;
|
||||||
|
chainEvent: {
|
||||||
|
erc20: {
|
||||||
|
deposit: ERC20Deposit;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ERC20Deposit {
|
||||||
|
vegaAssetId: string;
|
||||||
|
sourceEthereumAddress: string;
|
||||||
|
targetPartyId: string;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@ const defaultOptions = {} as const;
|
|||||||
export type ExplorerNodesQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
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`
|
export const ExplorerNodesDocument = gql`
|
||||||
@ -22,6 +22,7 @@ export const ExplorerNodesDocument = gql`
|
|||||||
tmPubkey
|
tmPubkey
|
||||||
ethereumAddress
|
ethereumAddress
|
||||||
location
|
location
|
||||||
|
status
|
||||||
stakedByOperator
|
stakedByOperator
|
||||||
stakedByDelegates
|
stakedByDelegates
|
||||||
stakedTotal
|
stakedTotal
|
||||||
@ -31,8 +32,6 @@ export const ExplorerNodesDocument = gql`
|
|||||||
offline
|
offline
|
||||||
online
|
online
|
||||||
}
|
}
|
||||||
status
|
|
||||||
name
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ query ExplorerNodes {
|
|||||||
tmPubkey
|
tmPubkey
|
||||||
ethereumAddress
|
ethereumAddress
|
||||||
location
|
location
|
||||||
|
status
|
||||||
stakedByOperator
|
stakedByOperator
|
||||||
stakedByDelegates
|
stakedByDelegates
|
||||||
stakedTotal
|
stakedTotal
|
||||||
@ -19,8 +20,6 @@ query ExplorerNodes {
|
|||||||
offline
|
offline
|
||||||
online
|
online
|
||||||
}
|
}
|
||||||
status
|
|
||||||
name
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user