feat(explorer): add withdrawsubmission view (#2366)

* feat(explorer): add withdraw view
* fix(explorer): regenerate types after rebasing develop
This commit is contained in:
Edd 2022-12-09 11:02:01 +00:00 committed by GitHub
parent fa8868d42a
commit 13c9dffc3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 579 additions and 16 deletions

View File

@ -0,0 +1,31 @@
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { AssetLink } from '../links';
import { useExplorerAssetQuery } from '../links/asset-link/__generated__/Asset';
export type AssetBalanceProps = {
assetId: string;
price: string;
};
/**
* Given a market ID and a price it will fetch the market
* and format the price in that market's decimal places.
*/
const AssetBalance = ({ assetId, price }: AssetBalanceProps) => {
const { data } = useExplorerAssetQuery({
variables: { id: assetId },
});
const label =
data && data.asset?.decimals
? addDecimalsFormatNumber(price, data.asset.decimals)
: price;
return (
<div className="inline-block">
<span>{label}</span> <AssetLink id={data?.asset?.id || ''} />
</div>
);
};
export default AssetBalance;

View File

@ -21,7 +21,7 @@ export const EthExplorerLink = ({
const link = `${DATA_SOURCES.ethExplorerUrl}/${type}/${id}`;
return (
<a
className="underline external"
className="underline external font-mono"
target="_blank"
rel="noopener noreferrer"
{...props}

View File

@ -14,6 +14,7 @@ import { TxDetailsNodeVote } from './tx-node-vote';
import { TxDetailsOrderCancel } from './tx-order-cancel';
import get from 'lodash/get';
import { TxDetailsOrderAmend } from './tx-order-amend';
import { TxDetailsWithdrawSubmission } from './tx-withdraw-submission';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@ -88,6 +89,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsChainEvent;
case 'Node Vote':
return TxDetailsNodeVote;
case 'Withdraw':
return TxDetailsWithdrawSubmission;
default:
return TxDetailsGeneric;
}

View File

@ -3,10 +3,14 @@ import type { BlockExplorerTransactionResult } from '../../../routes/types/block
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { ExplorerNodeVoteQueryResult } from './__generated___/node-vote';
import { useExplorerNodeVoteQuery } from './__generated___/node-vote';
import type { ExplorerNodeVoteQueryResult } from './__generated___/Node-vote';
import { useExplorerNodeVoteQuery } from './__generated___/Node-vote';
import { PartyLink } from '../../links';
import { Time } from '../../time';
import {
EthExplorerLink,
EthExplorerLinkTypes,
} from '../../links/eth-explorer-link/eth-explorer-link';
interface TxDetailsNodeVoteProps {
txData: BlockExplorerTransactionResult | undefined;
@ -80,10 +84,10 @@ export function TxDetailsNodeVoteDeposit({
<TableCell>
<Time date={deposit?.deposit?.creditedTimestamp} />
</TableCell>
{deposit?.deposit?.txHash ? (
<TxHash hash={deposit?.deposit?.txHash} />
) : null}
</TableRow>
{deposit?.deposit?.txHash ? (
<TxHash hash={deposit?.deposit?.txHash} />
) : null}
</>
);
}
@ -119,10 +123,10 @@ export function TxDetailsNodeVoteWithdrawal({
<TableCell>
<Time date={withdrawal?.withdrawal?.withdrawnTimestamp} />
</TableCell>
{withdrawal?.withdrawal?.txHash ? (
<TxHash hash={withdrawal?.withdrawal?.txHash} />
) : null}
</TableRow>
{withdrawal?.withdrawal?.txHash ? (
<TxHash hash={withdrawal?.withdrawal?.txHash} />
) : null}
</>
);
}
@ -138,7 +142,9 @@ export function TxHash({ hash }: TxDetailsEthTxHashProps) {
return (
<TableRow modifier="bordered">
<TableCell>Ethereum TX:</TableCell>
<TableCell>{hash}</TableCell>
<TableCell>
<EthExplorerLink id={hash} type={EthExplorerLinkTypes.tx} />
</TableCell>
</TableRow>
);
}

View File

@ -0,0 +1,72 @@
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import {
EthExplorerLink,
EthExplorerLinkTypes,
} from '../../links/eth-explorer-link/eth-explorer-link';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import AssetBalance from '../../asset-balance/asset-balance';
import { useScrollToLocation } from '../../../hooks/scroll-to-location';
import { WithdrawalProgress } from '../../withdrawal/withdrawal-progress';
interface TxDetailsOrderCancelProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* The first part of a withdrawal. If the validators all approve this request (i.e. the user has the
* required funds), they will produce a multisig bundle that can be submitted to the ERC20 bridge to
* execute the withdrawal.
*/
export const TxDetailsWithdrawSubmission = ({
txData,
pubKey,
blockData,
}: TxDetailsOrderCancelProps) => {
useScrollToLocation();
if (!txData || !txData.command.withdrawSubmission) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const w = txData.command.withdrawSubmission;
const id = txData?.signature?.value
? txSignatureToDeterministicId(txData.signature.value)
: '-';
return (
<>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Amount')}</TableCell>
<TableCell>
<AssetBalance price={w.amount} assetId={w.asset} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Recipient')}</TableCell>
<TableCell>
<EthExplorerLink
id={w.ext.erc20.receiverAddress}
type={EthExplorerLinkTypes.address}
/>
</TableCell>
</TableRow>
</TableWithTbody>
{id !== '-' ? (
<WithdrawalProgress id={id} txStatus={txData.code} />
) : null}
</>
);
};

View File

@ -0,0 +1,19 @@
fragment ExplorerWithdrawalProperties on Withdrawal {
id
status
createdTimestamp
withdrawnTimestamp
ref
txHash
details {
... on Erc20WithdrawalDetails {
receiverAddress
}
}
}
query ExplorerWithdrawal($id: ID!) {
withdrawal(id: $id) {
...ExplorerWithdrawalProperties
}
}

View File

@ -0,0 +1,64 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerWithdrawalPropertiesFragment = { __typename?: 'Withdrawal', id: string, status: Types.WithdrawalStatus, createdTimestamp: any, withdrawnTimestamp?: any | null, ref: string, txHash?: string | null, details?: { __typename?: 'Erc20WithdrawalDetails', receiverAddress: string } | null };
export type ExplorerWithdrawalQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerWithdrawalQuery = { __typename?: 'Query', withdrawal?: { __typename?: 'Withdrawal', id: string, status: Types.WithdrawalStatus, createdTimestamp: any, withdrawnTimestamp?: any | null, ref: string, txHash?: string | null, details?: { __typename?: 'Erc20WithdrawalDetails', receiverAddress: string } | null } | null };
export const ExplorerWithdrawalPropertiesFragmentDoc = gql`
fragment ExplorerWithdrawalProperties on Withdrawal {
id
status
createdTimestamp
withdrawnTimestamp
ref
txHash
details {
... on Erc20WithdrawalDetails {
receiverAddress
}
}
}
`;
export const ExplorerWithdrawalDocument = gql`
query ExplorerWithdrawal($id: ID!) {
withdrawal(id: $id) {
...ExplorerWithdrawalProperties
}
}
${ExplorerWithdrawalPropertiesFragmentDoc}`;
/**
* __useExplorerWithdrawalQuery__
*
* To run a query within a React component, call `useExplorerWithdrawalQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerWithdrawalQuery` 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 } = useExplorerWithdrawalQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerWithdrawalQuery(baseOptions: Apollo.QueryHookOptions<ExplorerWithdrawalQuery, ExplorerWithdrawalQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerWithdrawalQuery, ExplorerWithdrawalQueryVariables>(ExplorerWithdrawalDocument, options);
}
export function useExplorerWithdrawalLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerWithdrawalQuery, ExplorerWithdrawalQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerWithdrawalQuery, ExplorerWithdrawalQueryVariables>(ExplorerWithdrawalDocument, options);
}
export type ExplorerWithdrawalQueryHookResult = ReturnType<typeof useExplorerWithdrawalQuery>;
export type ExplorerWithdrawalLazyQueryHookResult = ReturnType<typeof useExplorerWithdrawalLazyQuery>;
export type ExplorerWithdrawalQueryResult = Apollo.QueryResult<ExplorerWithdrawalQuery, ExplorerWithdrawalQueryVariables>;

View File

@ -0,0 +1,125 @@
import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import WithdrawalProgress from './withdrawal-progress';
import { ExplorerWithdrawalDocument } from './__generated__/Withdrawal';
import { ApolloError } from '@apollo/client';
function renderComponent(id: string, status: number, mock: MockedResponse[]) {
return (
<MockedProvider mocks={mock}>
<WithdrawalProgress id={id} txStatus={status} />
</MockedProvider>
);
}
describe('Withdrawal Progress component', () => {
it('Renders success for the first indicator if txStatus is 0', () => {
const res = render(renderComponent('123', 0, []));
expect(res.getByText('Requested')).toBeInTheDocument();
// Steps 2 and three should not be complete also
expect(res.getByText('Not prepared')).toBeInTheDocument();
expect(res.getByText('Not complete')).toBeInTheDocument();
});
it('Renders success for the first indicator if txStatus is anything except 0', () => {
const res = render(renderComponent('123', 20, []));
expect(res.getByText('Rejected')).toBeInTheDocument();
// Steps 2 and three should not be complete also
expect(res.getByText('Not prepared')).toBeInTheDocument();
expect(res.getByText('Not complete')).toBeInTheDocument();
});
it('Renders success for the second indicator if a date can be fetched', async () => {
const mock = {
request: {
query: ExplorerWithdrawalDocument,
variables: {
id: '123',
},
},
result: {
data: {
withdrawal: {
__typename: 'Withdrawal',
id: '123',
status: 'STATUS_OPEN',
createdTimestamp: '2022-03-24T11:03:40.026173466Z',
withdrawnTimestamp: null,
ref: 'irrelevant',
txHash: '0x123456890',
details: {
__typename: 'Erc20WithdrawalDetails',
receiverAddress: '0x5435345432342423',
},
},
},
},
};
const res = render(renderComponent('123', 0, [mock]));
// Step 1 should be filled in
expect(res.getByText('Requested')).toBeInTheDocument();
// Step 2
expect(await res.findByText('Prepared')).toBeInTheDocument();
// Step 3
expect(await res.findByText('Not complete')).toBeInTheDocument();
});
it('Renders success for the third indicator if the withdrawal has been executed', async () => {
const mock = {
request: {
query: ExplorerWithdrawalDocument,
variables: {
id: '123',
},
},
result: {
data: {
withdrawal: {
__typename: 'Withdrawal',
id: '123',
status: 'STATUS_FINALIZED',
createdTimestamp: '2022-03-24T11:03:40.026173466Z',
withdrawnTimestamp: '2022-03-24T11:03:40.026173466Z',
ref: 'irrelevant',
txHash: '0x123456890',
details: {
__typename: 'Erc20WithdrawalDetails',
receiverAddress: '0x5435345432342423',
},
},
},
},
};
const res = render(renderComponent('123', 0, [mock]));
// Step 1 should be filled in
expect(res.getByText('Requested')).toBeInTheDocument();
// Step 2
expect(await res.findByText('Prepared')).toBeInTheDocument();
// Step 3
expect(await res.findByText('Complete')).toBeInTheDocument();
});
it('Renders an error under step 2 if the tx status code is ok but there is a graphql error', async () => {
const mock = {
request: {
query: ExplorerWithdrawalDocument,
variables: {
id: '123',
},
},
error: new ApolloError({ errorMessage: 'No such withdrawal' }),
};
const res = render(renderComponent('123', 0, [mock]));
expect(await res.findByText('No such withdrawal')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,149 @@
import { t } from '@vegaprotocol/react-helpers';
import type { ReactNode } from 'react';
import { Time } from '../time';
import { useExplorerWithdrawalQuery } from './__generated__/Withdrawal';
interface TxsStatsInfoProps {
id: string;
txStatus: number;
className?: string;
}
const OK = 0;
/**
* Shows a user the *current* state of a proposal. Which sound easy, but.. strap in
* 1. Step one is the submission and acceptance of the WithdrawalSubmission. If it is not rejected, it is accepted and we can mark it as complete
* 2. Is complete when there is a multisig bundle available to the user. This means that their funds have been moved to an account locked for
* withdrawal. To move out of this phase, the user (any user - but realistically the owner of the recipient address) needs to execute the withdrawal
* bundle on Ethereum, then we progress to.
* 3. The funds have left the ERC20 bridge and been sent to the user. This is complete.
*
* There could actually be some extra complexity:
* - If a deposit is above { asset { withdrawalThreshold } } then the multisig bundle is still produced (step 2) but cannot be submitted for a period
* of time. The period of time is defined on the ERC20 bridge. Attempts to submit the multisig bundle before that would be rejected.
* - The bridge could be paused, which would cause attempts to complete step three to be rejected
*
* Also:
* - 1 -> 2 is completed automatically by the system
* - 2 -> 3 requires action from the user
* - Any user with an Ethereum wallet can complete 2 -> 3 as above, but it will likely be the person who owns the vega public key of the source and
* the ethereum public key of the recipient. Whoever is looking at this page may be none of those people.
* - This renders fine if the useExplorerWithdrawalQuery fails or returns nothing. It will render an error under the rejection if it failed to fetch
*/
export const WithdrawalProgress = ({ id, txStatus }: TxsStatsInfoProps) => {
const { data, error } = useExplorerWithdrawalQuery({ variables: { id } });
const step2Date = data?.withdrawal?.createdTimestamp || undefined;
const step3Date = data?.withdrawal?.withdrawnTimestamp || undefined;
const isStep1Complete = txStatus === OK;
const isStep2Complete = isStep1Complete && !!step2Date;
const isStep3Complete = isStep2Complete && !!step3Date;
const hasApiError = !!error?.message;
return (
<div className="p-5 mb-12 max-w-xl">
<div className="mx-4 p-4">
<div className="flex items-center">
<WithdrawalProgressIndicator
step={isStep1Complete ? 1 : '✕'}
isComplete={isStep1Complete}
statusDescription={isStep1Complete ? t('Requested') : t('Rejected')}
/>
<WithdrawalProgressSeparator isComplete={isStep1Complete} />
<WithdrawalProgressIndicator
step={2}
isComplete={isStep2Complete}
statusDescription={
isStep2Complete ? t('Prepared') : t('Not prepared')
}
>
{isStep2Complete ? (
<Time date={step2Date} />
) : hasApiError ? (
<span>{error?.message}</span>
) : (
''
)}
</WithdrawalProgressIndicator>
<WithdrawalProgressSeparator isComplete={isStep3Complete} />
<WithdrawalProgressIndicator
step={3}
isComplete={isStep3Complete}
statusDescription={
isStep3Complete ? t('Complete') : t('Not complete')
}
>
{isStep3Complete ? <Time date={step3Date} /> : ''}
</WithdrawalProgressIndicator>
</div>
</div>
</div>
);
};
const classes = {
indicatorFailed:
'rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2 border-red-600 bg-red-600 text-center text-white font-bold leading-5',
textFailed:
'absolute top-0 -ml-10 text-center mt-16 w-32 text-xs font-medium uppercase text-red-600',
indicatorComplete:
'rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2 border-vega-green-dark bg-vega-green-dark text-center text-white leading-5',
textComplete:
'absolute top-0 -ml-10 text-center mt-16 w-32 text-xs font-medium uppercase text-vega-green-dark',
indicatorIncomplete:
'rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2 border-gray-300 text-center leading-5',
textIncomplete:
'absolute top-0 -ml-10 text-center mt-16 w-32 text-xs font-medium uppercase text-gray-500',
};
interface WithdrawalProgressIndicatorProps {
step: string | number;
isComplete: boolean;
statusDescription: string;
children?: ReactNode;
}
export function WithdrawalProgressIndicator({
isComplete,
statusDescription,
step,
children,
}: WithdrawalProgressIndicatorProps) {
return (
<div className="flex items-center relative">
<div
className={
isComplete ? classes.indicatorComplete : classes.indicatorIncomplete
}
>
{step}
</div>
<div
className={isComplete ? classes.textComplete : classes.textIncomplete}
>
{statusDescription}
<br />
{children}
</div>
</div>
);
}
interface WithdrawalProgressSeparatorProps {
isComplete: boolean;
}
export function WithdrawalProgressSeparator({
isComplete,
}: WithdrawalProgressSeparatorProps) {
return (
<div
className={`flex-auto border-t-2 transition duration-500 ease-in-out ${
isComplete ? 'border-vega-green-dark' : 'border-gray-300'
}`}
></div>
);
}
export default WithdrawalProgress;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
// Source: https://scribe.rip/scrolling-to-an-anchor-in-react-when-your-elements-are-rendered-asynchronously-8c64f77b5f34
export const useScrollToLocation = () => {
const scrolledRef = React.useRef(false);
const { hash } = useLocation();
const hashRef = React.useRef(hash);
React.useEffect(() => {
if (hash) {
// We want to reset if the hash has changed
if (hashRef.current !== hash) {
hashRef.current = hash;
scrolledRef.current = false;
}
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
scrolledRef.current = true;
}
}
});
};

View File

@ -0,0 +1,20 @@
import React from 'react';
/**
* Set the document title
* @param segments string array of segments. Will be reversed.
*/
export function useDocumentTitle(segments?: string[]) {
const base = 'VEGA explorer';
const split = ' | ';
React.useEffect(() => {
const segmentsOrdered = segments?.reverse() || [];
if (segments && segments.length > 0) {
document.title = `${segmentsOrdered.join(split)}${split}${base}`;
} else {
document.title = base;
}
}, [segments]);
}

View File

@ -3,11 +3,16 @@ import React from 'react';
import { RouteTitle } from '../../components/route-title';
import { SubHeading } from '../../components/sub-heading';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { useExplorerAssetsQuery } from './__generated__/assets';
import type { AssetsFieldsFragment } from './__generated__/assets';
import { useExplorerAssetsQuery } from './__generated__/Assets';
import type { AssetsFieldsFragment } from './__generated__/Assets';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
const Assets = () => {
const { data } = useExplorerAssetsQuery();
useDocumentTitle(['Assets']);
useScrollToLocation();
const assets = getNodes<AssetsFieldsFragment>(data?.assetsConnection);
@ -25,7 +30,7 @@ const Assets = () => {
return (
<React.Fragment key={a.id}>
<SubHeading data-testid="asset-header">
<SubHeading data-testid="asset-header" id={a.id}>
{a.name} ({a.symbol})
</SubHeading>
<SyntaxHighlighter data={a} />

View File

@ -9,6 +9,7 @@ import { BlocksRefetch } from '../../../components/blocks';
import { BlocksInfiniteList } from '../../../components/blocks/blocks-infinite-list';
import { JumpToBlock } from '../../../components/jump-to-block';
import { t, useFetch } from '@vegaprotocol/react-helpers';
import { useDocumentTitle } from '../../../hooks/use-document-title';
// This constant should only be changed if Tendermint API changes the max blocks returned
const TM_BLOCKS_PER_REQUEST = 20;
@ -22,6 +23,7 @@ interface BlocksStateProps {
}
const Blocks = () => {
useDocumentTitle(['Blocks']);
const [
{
areBlocksLoading,

View File

@ -17,9 +17,11 @@ import { Routes } from '../../route-names';
import { RenderFetched } from '../../../components/render-fetched';
import { t, useFetch } from '@vegaprotocol/react-helpers';
import { NodeLink } from '../../../components/links';
import { useDocumentTitle } from '../../../hooks/use-document-title';
const Block = () => {
const { block } = useParams<{ block: string }>();
useDocumentTitle(['Blocks', `Block #${block}`]);
const {
state: { data: blockData, loading, error },
} = useFetch<TendermintBlocksResponse>(

View File

@ -3,8 +3,11 @@ import { RouteTitle } from '../../components/route-title';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { DATA_SOURCES } from '../../config';
import type { TendermintGenesisResponse } from './tendermint-genesis-response';
import { useDocumentTitle } from '../../hooks/use-document-title';
const Genesis = () => {
useDocumentTitle(['Genesis']);
const {
state: { data: genesis },
} = useFetch<TendermintGenesisResponse>(

View File

@ -4,12 +4,15 @@ import { RouteTitle } from '../../components/route-title';
import { SubHeading } from '../../components/sub-heading';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalsQuery } from './__generated__/proposals';
import { useDocumentTitle } from '../../hooks/use-document-title';
const Governance = () => {
const { data } = useExplorerProposalsQuery({
errorPolicy: 'ignore',
});
useDocumentTitle();
if (!data || !data.proposalsConnection || !data.proposalsConnection.edges) {
return <section></section>;
}

View File

@ -1,7 +1,11 @@
import { StatsManager } from '@vegaprotocol/network-stats';
import { useDocumentTitle } from '../../hooks/use-document-title';
const Home = () => {
const classnames = 'mt-4 grid grid-cols-1 lg:grid-cols-2 lg:gap-4';
useDocumentTitle();
return (
<section>
<StatsManager className={classnames} />

View File

@ -3,11 +3,16 @@ import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { RouteTitle } from '../../components/route-title';
import { SubHeading } from '../../components/sub-heading';
import { t } from '@vegaprotocol/react-helpers';
import { useExplorerMarketsQuery } from './__generated__/markets';
import { useExplorerMarketsQuery } from './__generated__/Markets';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
const Markets = () => {
const { data } = useExplorerMarketsQuery();
useScrollToLocation();
useDocumentTitle(['Markets']);
const m = data?.marketsConnection?.edges;
return (
@ -17,7 +22,7 @@ const Markets = () => {
{m
? m.map((e) => (
<React.Fragment key={e.node.id}>
<SubHeading data-testid="markets-header">
<SubHeading data-testid="markets-header" id={e.node.id}>
{e.node.tradableInstrument.instrument.name}
</SubHeading>
<SyntaxHighlighter data={e.node} />

View File

@ -15,6 +15,8 @@ import {
import { RouteTitle } from '../../components/route-title';
import orderBy from 'lodash/orderBy';
import type { NetworkParamsQuery } from '@vegaprotocol/react-helpers';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
const PERCENTAGE_PARAMS = [
'governance.proposal.asset.requiredMajority',
@ -58,6 +60,7 @@ export const NetworkParameterRow = ({
row: { key: string; value: string };
}) => {
const isSyntaxRow = suitableForSyntaxHighlighter(value);
useDocumentTitle(['Network Parameters']);
return (
<KeyValueTableRow
@ -65,7 +68,7 @@ export const NetworkParameterRow = ({
inline={!isSyntaxRow}
id={key}
className={
'group target:bg-vega-pink target:text-white dark:target:bg-vega-yellow dark:target:text-black'
'group focus:bg-vega-pink focus:text-white dark:focus:bg-vega-yellow dark:focus:text-black'
}
>
{key}
@ -125,5 +128,6 @@ export const NetworkParametersTable = ({
export const NetworkParameters = () => {
const { data, loading, error } = useNetworkParamsQuery();
useScrollToLocation();
return <NetworkParametersTable data={data} error={error} loading={loading} />;
};

View File

@ -5,10 +5,13 @@ import { JumpTo } from '../../../components/jump-to';
import { useNavigate } from 'react-router-dom';
import { Routes } from '../../route-names';
import { t } from '@vegaprotocol/react-helpers';
import { useDocumentTitle } from '../../../hooks/use-document-title';
export const JumpToParty = () => {
const navigate = useNavigate();
useDocumentTitle(['Parties']);
const handleSubmit = (e: React.SyntheticEvent) => {
e.preventDefault();

View File

@ -15,6 +15,7 @@ import { PageHeader } from '../../../components/page-header';
import { useExplorerPartyAssetsQuery } from './__generated__/party-assets';
import type * as Schema from '@vegaprotocol/types';
import get from 'lodash/get';
import { useDocumentTitle } from '../../../hooks/use-document-title';
const accountTypeString: Record<Schema.AccountType, string> = {
ACCOUNT_TYPE_BOND: t('Bond'),
@ -37,6 +38,8 @@ const accountTypeString: Record<Schema.AccountType, string> = {
const Party = () => {
const { party } = useParams<{ party: string }>();
useDocumentTitle(['Parties', party || '-']);
const partyId = toNonHex(party ? party : '');
const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);

View File

@ -4,6 +4,7 @@ import type { TendermintUnconfirmedTransactionsResponse } from '../txs/tendermin
import { TxList } from '../../components/txs';
import { RouteTitle } from '../../components/route-title';
import { t, useFetch } from '@vegaprotocol/react-helpers';
import { useDocumentTitle } from '../../hooks/use-document-title';
const PendingTxs = () => {
const {
@ -12,6 +13,8 @@ const PendingTxs = () => {
`${DATA_SOURCES.tendermintUrl}/unconfirmed_txs`
);
useDocumentTitle(['Pending transactions']);
return (
<section>
<RouteTitle data-testid="unconfirmed-transactions-header">

View File

@ -3,10 +3,13 @@ import { RouteTitle } from '../../../components/route-title';
import { BlocksRefetch } from '../../../components/blocks';
import { TxsInfiniteList, TxsStatsInfo } from '../../../components/txs';
import { useTxsData } from '../../../hooks/use-txs-data';
import { useDocumentTitle } from '../../../hooks/use-document-title';
const BE_TXS_PER_REQUEST = 20;
export const TxsList = () => {
useDocumentTitle(['Transactions']);
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
useTxsData({ limit: BE_TXS_PER_REQUEST });
return (

View File

@ -10,12 +10,15 @@ import { PageHeader } from '../../../components/page-header';
import { Routes } from '../../../routes/route-names';
import { IconNames } from '@blueprintjs/icons';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { useDocumentTitle } from '../../../hooks/use-document-title';
const Tx = () => {
const { txHash } = useParams<{ txHash: string }>();
const hash = txHash ? toNonHex(txHash) : '';
let errorMessage: string | undefined = undefined;
useDocumentTitle(['Transactions', `TX ${txHash}`]);
const {
state: { data, loading, error },
} = useFetch<BlockExplorerTransaction>(

View File

@ -7,6 +7,7 @@ import { DATA_SOURCES } from '../../config';
import { useFetch } from '@vegaprotocol/react-helpers';
import type { TendermintValidatorsResponse } from './tendermint-validator-response';
import { useExplorerNodesQuery } from './__generated__/nodes';
import { useDocumentTitle } from '../../hooks/use-document-title';
const Validators = () => {
const {
@ -14,6 +15,9 @@ const Validators = () => {
} = useFetch<TendermintValidatorsResponse>(
`${DATA_SOURCES.tendermintUrl}/validators`
);
useDocumentTitle(['Validators']);
const { data } = useExplorerNodesQuery();
return (