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}`;
|
||||
return (
|
||||
<a
|
||||
className="underline external"
|
||||
className="underline external font-mono"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 { 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} />
|
||||
|
@ -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,
|
||||
|
@ -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>(
|
||||
|
@ -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>(
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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} />
|
||||
|
@ -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} />
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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]);
|
||||
|
@ -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">
|
||||
|
@ -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 (
|
||||
|
@ -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>(
|
||||
|
@ -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 (
|
||||
|
Loading…
Reference in New Issue
Block a user