Feat/397 list transactions (#1770)

* chore(explorer): add stagnet 1 env vars

* feat(explorer): add info block component

* feat(explorer): change tx order type lozenge

* feat(explorer): change truncated-link.tsx style

* feat(explorer): add stats info component for txs

* feat(explorer): change page title size

* feat(explorer): update txs list and add txs for party

* fix(explorer): remove unused var

* fix(explorer): change copy and remove unused vars

* fix(explorer): pr review fixes
This commit is contained in:
Elmar 2022-10-19 14:08:17 +01:00 committed by GitHub
parent 033ee14009
commit cc8f052a5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 333 additions and 102 deletions

View File

@ -0,0 +1,10 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://stagnet1.console.vega.xyz\",\"STAGNET3\":\"https://stagnet3.console.vega.xyz\"}
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
NX_VEGA_URL=https://api.n00.stagnet1.vega.xyz/graphql
NX_VEGA_WALLET_URL=http://localhost:1789

View File

@ -0,0 +1 @@
export * from './info-block';

View File

@ -0,0 +1,29 @@
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import React from 'react';
export interface InfoBlockProps {
title: string;
subtitle: string;
tooltipInfo: string;
}
export const InfoBlock = ({ title, subtitle, tooltipInfo }: InfoBlockProps) => {
return (
<div className="flex flex-col text-center ">
<h3 className="text-4xl">{title}</h3>
<p className="text-zinc-800 dark:text-zinc-300">
{subtitle}
{tooltipInfo ? (
<Tooltip description={tooltipInfo} align="center">
<span>
<Icon
name="info-sign"
className="ml-2 text-zinc-400 dark:text-zinc-600"
/>
</span>
</Tooltip>
) : undefined}
</p>
</div>
);
};

View File

@ -14,7 +14,7 @@ export const RouteTitle = ({
}: RouteTitleProps) => {
const classes = classnames(
'font-alpha',
'text-2xl',
'text-4xl',
'uppercase',
'mb-8',
className

View File

@ -20,7 +20,7 @@ export const TruncatedLink = ({
text={text}
startChars={startChars}
endChars={endChars}
className="font-mono font-bold underline"
className="underline uppercase underline-offset-2"
/>
</Link>
);

View File

@ -0,0 +1,35 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: TxsStats
// ====================================================
export interface TxsStats_statistics {
__typename: "Statistics";
/**
* Average number of orders added per blocks
*/
averageOrdersPerBlock: string;
/**
* Number of orders per seconds
*/
ordersPerSecond: string;
/**
* Number of transaction processed per block
*/
txPerBlock: string;
/**
* Number of the trades per seconds
*/
tradesPerSecond: string;
}
export interface TxsStats {
/**
* Get statistics about the Vega node
*/
statistics: TxsStats_statistics;
}

View File

@ -3,3 +3,4 @@ export { BlockTxsData } from './block-txs-data';
export { TxOrderType } from './tx-order-type';
export { TxsInfiniteList } from './txs-infinite-list';
export { TxsInfiniteListItem } from './txs-infinite-list-item';
export { TxsStatsInfo } from './txs-stats-info';

View File

@ -1,5 +1,3 @@
import { Lozenge, Intent } from '@vegaprotocol/ui-toolkit';
interface TxOrderTypeProps {
orderType: string;
className?: string;
@ -34,10 +32,13 @@ const displayString: StringMap = {
ValidatorHeartbeat: 'Validator Heartbeat',
};
export const TxOrderType = ({ orderType, className }: TxOrderTypeProps) => {
export const TxOrderType = ({ orderType }: TxOrderTypeProps) => {
return (
<Lozenge data-testid="tx-type" variant={Intent.None} className={className}>
<div
data-testid="tx-type"
className="text-sm rounded-md leading-none px-2 py-2 inline-block text-white dark:text-white bg-zinc-800 dark:bg-zinc-800 "
>
{displayString[orderType] || orderType}
</Lozenge>
</div>
);
};

View File

@ -83,8 +83,7 @@ describe('Txs infinite list item', () => {
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
expect(screen.getByTestId('tx-block')).toHaveTextContent(
'Block 1 (index 1)'
);
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
expect(screen.getByTestId('tx-index')).toHaveTextContent('1');
});
});

View File

@ -27,20 +27,13 @@ export const TxsInfiniteListItem = ({
return (
<div
data-testid="transaction-row"
className="grid grid-flow-col auto-cols-auto border-t border-neutral-600 dark:border-neutral-800 py-2 txs-infinite-list-item"
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2"
>
<div className="whitespace-nowrap" data-testid="tx-type">
<TxOrderType orderType={type} />
</div>
<div className="whitespace-nowrap" data-testid="pub-key">
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div className="whitespace-nowrap" data-testid="tx-hash">
<div
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="tx-hash"
>
<span className="xl:hidden uppercase text-zinc-500">ID:&nbsp;</span>
<TruncatedLink
to={`/${Routes.TX}/${toHex(hash)}`}
text={hash}
@ -48,14 +41,40 @@ export const TxsInfiniteListItem = ({
endChars={TRUNCATE_LENGTH}
/>
</div>
<div className="whitespace-nowrap" data-testid="tx-block">
<div
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="pub-key"
>
<span className="xl:hidden uppercase text-zinc-500">By:&nbsp;</span>
<TruncatedLink
to={`/${Routes.BLOCKS}/${block}`}
text={`Block ${block} (index ${index})`}
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div className="text-sm col-span-5 xl:col-span-2 leading-none flex items-center">
<TxOrderType orderType={type} />
</div>
<div
className="text-sm col-span-3 xl:col-span-1 leading-none flex items-center"
data-testid="tx-block"
>
<span className="xl:hidden uppercase text-zinc-500">Block:&nbsp;</span>
<TruncatedLink
to={`/${Routes.BLOCKS}/${block}`}
text={block}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div
className="text-sm col-span-2 xl:col-span-1 leading-none flex items-center"
data-testid="tx-index"
>
<span className="xl:hidden uppercase text-zinc-500">Index:&nbsp;</span>
{index}
</div>
</div>
);
};

View File

@ -65,7 +65,7 @@ describe('Txs infinite list', () => {
it('item renders data of n length into list of n length', () => {
// Provided the number of items doesn't exceed the 30 it initially
// desires, all txs will initially render
const txs = generateTxs(10);
const txs = generateTxs(7);
render(
<MemoryRouter>
<TxsInfiniteList
@ -82,7 +82,7 @@ describe('Txs infinite list', () => {
screen
.getByTestId('infinite-scroll-wrapper')
.querySelectorAll('.txs-infinite-list-item')
).toHaveLength(10);
).toHaveLength(7);
});
it('tries to load more items when required to initially fill the list', () => {
@ -126,7 +126,7 @@ describe('Txs infinite list', () => {
});
it('loads more items is called when scrolled', () => {
const txs = generateTxs(20);
const txs = generateTxs(14);
const callback = jest.fn();
render(
@ -143,7 +143,7 @@ describe('Txs infinite list', () => {
act(() => {
fireEvent.scroll(screen.getByTestId('infinite-scroll-wrapper'), {
target: { scrollY: 600 },
target: { scrollY: 2000 },
});
});

View File

@ -1,7 +1,7 @@
import React from 'react';
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { t } from '@vegaprotocol/react-helpers';
import { t, useScreenDimensions } from '@vegaprotocol/react-helpers';
import { TxsInfiniteListItem } from './txs-infinite-list-item';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
@ -55,6 +55,9 @@ export const TxsInfiniteList = ({
error,
className,
}: TxsInfiniteListProps) => {
const { screenSize } = useScreenDimensions();
const isStacked = ['xs', 'sm', 'md', 'lg'].includes(screenSize);
if (!txs) {
return <div>No items</div>;
}
@ -71,11 +74,15 @@ export const TxsInfiniteList = ({
return (
<div className={className} data-testid="transactions-list">
<div className="grid grid-flow-col auto-cols-auto w-full mb-8">
<div className="text-lg font-bold pl-2">Type</div>
<div className="text-lg font-bold">Submitted By</div>
<div className="text-lg font-bold">Transaction ID</div>
<div className="text-lg font-bold">Block</div>
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-zinc-500 uppercase">
<div className="col-span-3">
<span className="hidden xl:inline">Transaction &nbsp;</span>
<span>ID</span>
</div>
<div className="col-span-3">Submitted By</div>
<div className="col-span-2">Type</div>
<div className="col-span-1">Block</div>
<div className="col-span-1">Index</div>
</div>
<div data-testid="infinite-scroll-wrapper">
<InfiniteLoader
@ -88,7 +95,7 @@ export const TxsInfiniteList = ({
className="List"
height={595}
itemCount={itemCount}
itemSize={41}
itemSize={isStacked ? 134 : 72}
onItemsRendered={onItemsRendered}
ref={ref}
width={'100%'}

View File

@ -0,0 +1,79 @@
import { gql, useQuery } from '@apollo/client';
import { t } from '@vegaprotocol/react-helpers';
import { useEffect } from 'react';
import { InfoBlock } from '../../components/info-block';
import type { TxsStats, TxsStats_statistics } from './__generated__/TxsStats';
import classNames from 'classnames';
const STATS_QUERY = gql`
query TxsStats {
statistics {
averageOrdersPerBlock
ordersPerSecond
txPerBlock
tradesPerSecond
}
}
`;
interface StatsMap {
field: keyof TxsStats_statistics;
label: string;
info: string;
}
export const TXS_STATS_MAP: StatsMap[] = [
{
field: 'averageOrdersPerBlock',
label: t('Orders per block'),
info: t(
'Number of new orders processed in the last block. All orders derived from pegged orders and liquidity commitments count as a single order'
),
},
{
field: 'txPerBlock',
label: t('Transactions per block'),
info: t('Number of transactions processed in the last block'),
},
{
field: 'tradesPerSecond',
label: t('Trades per second'),
info: t('Number of trades processed in the last second'),
},
{
field: 'ordersPerSecond',
label: t('Order per second'),
info: t(
'Number of orders processed in the last second. All orders derived from pegged orders and liquidity commitments count as a single order'
),
},
];
interface TxsStatsInfoProps {
className?: string;
}
export const TxsStatsInfo = ({ className }: TxsStatsInfoProps) => {
const { data, startPolling, stopPolling } = useQuery<TxsStats>(STATS_QUERY);
useEffect(() => {
startPolling(1000);
return () => stopPolling();
});
const gridStyles =
'grid grid-rows-2 gap-4 grid-cols-2 xl:gap-8 xl:grid-rows-1 xl:grid-cols-4';
const containerStyles = 'p-5 border rounded-lg border-zinc-500';
return (
<aside className={classNames(gridStyles, containerStyles, className)}>
{TXS_STATS_MAP.map((field) => (
<InfoBlock
subtitle={field.label}
tooltipInfo={field.info}
title={data?.statistics[field.field] || ''}
/>
))}
</aside>
);
};

View File

@ -0,0 +1,82 @@
import { useCallback, useEffect, useState } from 'react';
import { useFetch } from '@vegaprotocol/react-helpers';
import type {
BlockExplorerTransactionResult,
BlockExplorerTransactions,
} from '../routes/types/block-explorer-response';
import { DATA_SOURCES } from '../config';
export interface TxsStateProps {
txsData: BlockExplorerTransactionResult[];
hasMoreTxs: boolean;
lastCursor: string;
}
export interface IUseTxsData {
limit?: number;
filters?: string;
}
export const getTxsDataUrl = ({ limit = 10, filters = '' }) => {
let url = `${DATA_SOURCES.blockExplorerUrl}/transactions?limit=${limit}`;
if (filters) {
url = `${url}&${filters}`;
}
return url;
};
export const useTxsData = ({ limit = 10, filters }: IUseTxsData) => {
const [{ txsData, hasMoreTxs, lastCursor }, setTxsState] =
useState<TxsStateProps>({
txsData: [],
hasMoreTxs: true,
lastCursor: '',
});
const url = getTxsDataUrl({ limit, filters });
const {
state: { data, error, loading },
refetch,
} = useFetch<BlockExplorerTransactions>(url, {}, false);
useEffect(() => {
if (data?.transactions?.length) {
setTxsState((prev) => ({
txsData: [...prev.txsData, ...data.transactions],
hasMoreTxs: true,
lastCursor:
data.transactions[data.transactions.length - 1].cursor || '',
}));
}
}, [setTxsState, data?.transactions]);
const loadTxs = useCallback(() => {
return refetch({
limit: limit,
before: lastCursor,
});
}, [lastCursor, limit, refetch]);
const refreshTxs = useCallback(async () => {
setTxsState((prev) => ({
...prev,
lastCursor: '',
hasMoreTxs: true,
txsData: [],
}));
}, [setTxsState]);
return {
data,
loading,
error,
txsData,
hasMoreTxs,
lastCursor,
refreshTxs,
loadTxs,
};
};

View File

@ -10,7 +10,7 @@ import React from 'react';
import { useParams } from 'react-router-dom';
import { RouteTitle } from '../../../components/route-title';
import { SubHeading } from '../../../components/sub-heading';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { AsyncRenderer, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { Panel } from '../../../components/panel';
import { InfoPanel } from '../../../components/info-panel';
import { toNonHex } from '../../../components/search/detect-search';
@ -19,7 +19,9 @@ import type {
PartyAssetsQuery,
PartyAssetsQueryVariables,
} from './__generated__/PartyAssetsQuery';
import type { BlockExplorerTransactions } from '../../../routes/types/block-explorer-response';
import type { TendermintSearchTransactionResponse } from '../tendermint-transaction-response';
import { useTxsData } from '../../../hooks/use-txs-data';
import { TxsInfiniteList } from '../../../components/txs';
const PARTY_ASSETS_QUERY = gql`
query PartyAssetsQuery($partyId: ID!) {
@ -59,11 +61,16 @@ const PARTY_ASSETS_QUERY = gql`
const Party = () => {
const { party } = useParams<{ party: string }>();
const partyId = party ? toNonHex(party) : '';
const filters = `filters[tx.submitter]=${partyId}`;
const { hasMoreTxs, loadTxs, error, txsData, loading } = useTxsData({
limit: 10,
filters,
});
const {
state: { data: partyData },
} = useFetch<BlockExplorerTransactions>(
`${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${partyId}`
} = useFetch<TendermintSearchTransactionResponse>(
`${DATA_SOURCES.tendermintUrl}/tx_search?query="tx.submitter='${partyId}'"`
);
const { data } = useQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(
@ -149,6 +156,21 @@ const Party = () => {
{accounts}
<SubHeading>{t('Staking')}</SubHeading>
{staking}
<SubHeading>{t('Transactions')}</SubHeading>
<AsyncRenderer
loading={loading as boolean}
error={error}
data={txsData}
>
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}
txs={txsData}
loadMoreTxs={loadTxs}
error={error}
className="mb-28"
/>
</AsyncRenderer>
<SubHeading>{t('JSON')}</SubHeading>
<section data-testid="parties-json">
<SyntaxHighlighter data={data} />

View File

@ -1,73 +1,19 @@
import { DATA_SOURCES } from '../../../config';
import { useCallback, useEffect, useState } from 'react';
import { t, useFetch } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/react-helpers';
import { RouteTitle } from '../../../components/route-title';
import { BlocksRefetch } from '../../../components/blocks';
import { TxsInfiniteList } from '../../../components/txs';
import type {
BlockExplorerTransactionResult,
BlockExplorerTransactions,
} from '../../../routes/types/block-explorer-response';
interface TxsStateProps {
txsData: BlockExplorerTransactionResult[];
hasMoreTxs: boolean;
lastCursor: string;
}
import { TxsInfiniteList, TxsStatsInfo } from '../../../components/txs';
import { useTxsData } from '../../../hooks/use-txs-data';
const BE_TXS_PER_REQUEST = 100;
export const TxsList = () => {
const [{ txsData, hasMoreTxs, lastCursor }, setTxsState] =
useState<TxsStateProps>({
txsData: [],
hasMoreTxs: true,
lastCursor: '',
});
const {
state: { data, error, loading },
refetch,
} = useFetch<BlockExplorerTransactions>(
`${DATA_SOURCES.blockExplorerUrl}/transactions?` +
new URLSearchParams({
limit: BE_TXS_PER_REQUEST.toString(10),
}),
{},
false
);
useEffect(() => {
if (data?.transactions?.length) {
setTxsState((prev) => ({
txsData: [...prev.txsData, ...data.transactions],
hasMoreTxs: true,
lastCursor:
data.transactions[data.transactions.length - 1].cursor || '',
}));
}
}, [data?.transactions]);
const loadTxs = useCallback(() => {
return refetch({
limit: BE_TXS_PER_REQUEST,
before: lastCursor,
});
}, [lastCursor, refetch]);
const refreshTxs = useCallback(async () => {
setTxsState((prev) => ({
...prev,
lastCursor: '',
hasMoreTxs: true,
txsData: [],
}));
}, [setTxsState]);
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
useTxsData({ limit: BE_TXS_PER_REQUEST });
return (
<section>
<section className="md:p-2 lg:p-4 xl:p-6">
<RouteTitle>{t('Transactions')}</RouteTitle>
<BlocksRefetch refetch={refreshTxs} />
<TxsStatsInfo className="my-8 py-8" />
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}