feat(explorer): add withdrawsubmission view (#2366)
* feat(explorer): add withdraw view * fix(explorer): regenerate types after rebasing develop
This commit is contained in:
parent
fa8868d42a
commit
13c9dffc3d
@ -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;
|
@ -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}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
64
apps/explorer/src/app/components/withdrawal/__generated__/Withdrawal.ts
generated
Normal file
64
apps/explorer/src/app/components/withdrawal/__generated__/Withdrawal.ts
generated
Normal 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>;
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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;
|
27
apps/explorer/src/app/hooks/scroll-to-location.ts
Normal file
27
apps/explorer/src/app/hooks/scroll-to-location.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
20
apps/explorer/src/app/hooks/use-document-title.ts
Normal file
20
apps/explorer/src/app/hooks/use-document-title.ts
Normal 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]);
|
||||||
|
}
|
@ -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} />
|
||||||
|
@ -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,
|
||||||
|
@ -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>(
|
||||||
|
@ -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>(
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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} />
|
||||||
|
@ -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} />
|
||||||
|
@ -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} />;
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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]);
|
||||||
|
@ -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">
|
||||||
|
@ -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 (
|
||||||
|
@ -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>(
|
||||||
|
@ -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 (
|
||||||
|
Loading…
Reference in New Issue
Block a user