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}`; const link = `${DATA_SOURCES.ethExplorerUrl}/${type}/${id}`;
return ( return (
<a <a
className="underline external" className="underline external font-mono"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
{...props} {...props}

View File

@ -14,6 +14,7 @@ import { TxDetailsNodeVote } from './tx-node-vote';
import { TxDetailsOrderCancel } from './tx-order-cancel'; import { TxDetailsOrderCancel } from './tx-order-cancel';
import get from 'lodash/get'; import get from 'lodash/get';
import { TxDetailsOrderAmend } from './tx-order-amend'; import { TxDetailsOrderAmend } from './tx-order-amend';
import { TxDetailsWithdrawSubmission } from './tx-withdraw-submission';
interface TxDetailsWrapperProps { interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@ -88,6 +89,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsChainEvent; return TxDetailsChainEvent;
case 'Node Vote': case 'Node Vote':
return TxDetailsNodeVote; return TxDetailsNodeVote;
case 'Withdraw':
return TxDetailsWithdrawSubmission;
default: default:
return TxDetailsGeneric; 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 type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared'; import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table'; import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { ExplorerNodeVoteQueryResult } from './__generated___/node-vote'; import type { ExplorerNodeVoteQueryResult } from './__generated___/Node-vote';
import { useExplorerNodeVoteQuery } from './__generated___/node-vote'; import { useExplorerNodeVoteQuery } from './__generated___/Node-vote';
import { PartyLink } from '../../links'; import { PartyLink } from '../../links';
import { Time } from '../../time'; import { Time } from '../../time';
import {
EthExplorerLink,
EthExplorerLinkTypes,
} from '../../links/eth-explorer-link/eth-explorer-link';
interface TxDetailsNodeVoteProps { interface TxDetailsNodeVoteProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@ -80,10 +84,10 @@ export function TxDetailsNodeVoteDeposit({
<TableCell> <TableCell>
<Time date={deposit?.deposit?.creditedTimestamp} /> <Time date={deposit?.deposit?.creditedTimestamp} />
</TableCell> </TableCell>
{deposit?.deposit?.txHash ? (
<TxHash hash={deposit?.deposit?.txHash} />
) : null}
</TableRow> </TableRow>
{deposit?.deposit?.txHash ? (
<TxHash hash={deposit?.deposit?.txHash} />
) : null}
</> </>
); );
} }
@ -119,10 +123,10 @@ export function TxDetailsNodeVoteWithdrawal({
<TableCell> <TableCell>
<Time date={withdrawal?.withdrawal?.withdrawnTimestamp} /> <Time date={withdrawal?.withdrawal?.withdrawnTimestamp} />
</TableCell> </TableCell>
{withdrawal?.withdrawal?.txHash ? (
<TxHash hash={withdrawal?.withdrawal?.txHash} />
) : null}
</TableRow> </TableRow>
{withdrawal?.withdrawal?.txHash ? (
<TxHash hash={withdrawal?.withdrawal?.txHash} />
) : null}
</> </>
); );
} }
@ -138,7 +142,9 @@ export function TxHash({ hash }: TxDetailsEthTxHashProps) {
return ( return (
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>Ethereum TX:</TableCell> <TableCell>Ethereum TX:</TableCell>
<TableCell>{hash}</TableCell> <TableCell>
<EthExplorerLink id={hash} type={EthExplorerLinkTypes.tx} />
</TableCell>
</TableRow> </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 { RouteTitle } from '../../components/route-title';
import { SubHeading } from '../../components/sub-heading'; import { SubHeading } from '../../components/sub-heading';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit'; import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { useExplorerAssetsQuery } from './__generated__/assets'; import { useExplorerAssetsQuery } from './__generated__/Assets';
import type { AssetsFieldsFragment } 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 Assets = () => {
const { data } = useExplorerAssetsQuery(); const { data } = useExplorerAssetsQuery();
useDocumentTitle(['Assets']);
useScrollToLocation();
const assets = getNodes<AssetsFieldsFragment>(data?.assetsConnection); const assets = getNodes<AssetsFieldsFragment>(data?.assetsConnection);
@ -25,7 +30,7 @@ const Assets = () => {
return ( return (
<React.Fragment key={a.id}> <React.Fragment key={a.id}>
<SubHeading data-testid="asset-header"> <SubHeading data-testid="asset-header" id={a.id}>
{a.name} ({a.symbol}) {a.name} ({a.symbol})
</SubHeading> </SubHeading>
<SyntaxHighlighter data={a} /> <SyntaxHighlighter data={a} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,8 @@ import {
import { RouteTitle } from '../../components/route-title'; import { RouteTitle } from '../../components/route-title';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import type { NetworkParamsQuery } from '@vegaprotocol/react-helpers'; import type { NetworkParamsQuery } from '@vegaprotocol/react-helpers';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
const PERCENTAGE_PARAMS = [ const PERCENTAGE_PARAMS = [
'governance.proposal.asset.requiredMajority', 'governance.proposal.asset.requiredMajority',
@ -58,6 +60,7 @@ export const NetworkParameterRow = ({
row: { key: string; value: string }; row: { key: string; value: string };
}) => { }) => {
const isSyntaxRow = suitableForSyntaxHighlighter(value); const isSyntaxRow = suitableForSyntaxHighlighter(value);
useDocumentTitle(['Network Parameters']);
return ( return (
<KeyValueTableRow <KeyValueTableRow
@ -65,7 +68,7 @@ export const NetworkParameterRow = ({
inline={!isSyntaxRow} inline={!isSyntaxRow}
id={key} id={key}
className={ 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} {key}
@ -125,5 +128,6 @@ export const NetworkParametersTable = ({
export const NetworkParameters = () => { export const NetworkParameters = () => {
const { data, loading, error } = useNetworkParamsQuery(); const { data, loading, error } = useNetworkParamsQuery();
useScrollToLocation();
return <NetworkParametersTable data={data} error={error} loading={loading} />; 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 { useNavigate } from 'react-router-dom';
import { Routes } from '../../route-names'; import { Routes } from '../../route-names';
import { t } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
import { useDocumentTitle } from '../../../hooks/use-document-title';
export const JumpToParty = () => { export const JumpToParty = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useDocumentTitle(['Parties']);
const handleSubmit = (e: React.SyntheticEvent) => { const handleSubmit = (e: React.SyntheticEvent) => {
e.preventDefault(); e.preventDefault();

View File

@ -15,6 +15,7 @@ import { PageHeader } from '../../../components/page-header';
import { useExplorerPartyAssetsQuery } from './__generated__/party-assets'; import { useExplorerPartyAssetsQuery } from './__generated__/party-assets';
import type * as Schema from '@vegaprotocol/types'; import type * as Schema from '@vegaprotocol/types';
import get from 'lodash/get'; import get from 'lodash/get';
import { useDocumentTitle } from '../../../hooks/use-document-title';
const accountTypeString: Record<Schema.AccountType, string> = { const accountTypeString: Record<Schema.AccountType, string> = {
ACCOUNT_TYPE_BOND: t('Bond'), ACCOUNT_TYPE_BOND: t('Bond'),
@ -37,6 +38,8 @@ const accountTypeString: Record<Schema.AccountType, string> = {
const Party = () => { const Party = () => {
const { party } = useParams<{ party: string }>(); const { party } = useParams<{ party: string }>();
useDocumentTitle(['Parties', party || '-']);
const partyId = toNonHex(party ? party : ''); const partyId = toNonHex(party ? party : '');
const { isMobile } = useScreenDimensions(); const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]); 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 { TxList } from '../../components/txs';
import { RouteTitle } from '../../components/route-title'; import { RouteTitle } from '../../components/route-title';
import { t, useFetch } from '@vegaprotocol/react-helpers'; import { t, useFetch } from '@vegaprotocol/react-helpers';
import { useDocumentTitle } from '../../hooks/use-document-title';
const PendingTxs = () => { const PendingTxs = () => {
const { const {
@ -12,6 +13,8 @@ const PendingTxs = () => {
`${DATA_SOURCES.tendermintUrl}/unconfirmed_txs` `${DATA_SOURCES.tendermintUrl}/unconfirmed_txs`
); );
useDocumentTitle(['Pending transactions']);
return ( return (
<section> <section>
<RouteTitle data-testid="unconfirmed-transactions-header"> <RouteTitle data-testid="unconfirmed-transactions-header">

View File

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

View File

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

View File

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