Feat/1682 fix api endpoints (#1690)

* chore(explorer): replace tendermint api with block explorer api for transaction related searches

* feat(explorer): replace detect search type to be by process of elimination as temp measure

* fix(explorer): fix broken tests
This commit is contained in:
Elmar 2022-10-11 14:17:10 +01:00 committed by GitHub
parent 0b8c9c836c
commit a7d100bce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 199 additions and 234 deletions

View File

@ -89,8 +89,8 @@ describe('Detect Search', () => {
expect(actual).toStrictEqual(expected);
});
it("detectTypeByFetching should call fetch with hex query it's a transaction", async () => {
const query = 'abc';
it("detectTypeByFetching should call fetch with non-hex query it's a transaction", async () => {
const query = '0xabc';
const type = SearchTypes.Transaction;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
@ -99,45 +99,21 @@ describe('Detect Search', () => {
ok: true,
json: () =>
Promise.resolve({
result: {
tx: query,
transaction: {
hash: query,
},
}),
})
)
);
const result = await detectTypeByFetching(query, type);
const result = await detectTypeByFetching(query);
expect(fetch).toHaveBeenCalledWith(
`${DATA_SOURCES.tendermintUrl}/tx?hash=0x${query}`
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(query)}`
);
expect(result).toBe(type);
});
it("detectTypeByFetching should call fetch with non-hex query it's a party", async () => {
const query = 'abc';
const type = SearchTypes.Party;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
result: {
txs: [query],
},
}),
})
)
);
const result = await detectTypeByFetching(query, type);
expect(fetch).toHaveBeenCalledWith(
`${DATA_SOURCES.tendermintUrl}/tx_search?query="tx.submitter='${query}'"`
);
expect(result).toBe(type);
});
it('detectTypeByFetching should return undefined if no matches', async () => {
const query = 'abc';
const type = SearchTypes.Party;
// @ts-ignore issue related to polyfill
@ -148,11 +124,8 @@ describe('Detect Search', () => {
})
)
);
const result = await detectTypeByFetching(query, type);
expect(fetch).toHaveBeenCalledWith(
`${DATA_SOURCES.tendermintUrl}/tx_search?query="tx.submitter='${query}'"`
);
expect(result).toBe(undefined);
const result = await detectTypeByFetching(query);
expect(result).toBe(type);
});
it('getSearchType should return party from fetch response', async () => {
@ -163,13 +136,7 @@ describe('Detect Search', () => {
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
result: {
txs: [query],
},
}),
ok: false,
})
)
);
@ -177,7 +144,7 @@ describe('Detect Search', () => {
expect(result).toBe(expected);
});
it('getSearchType should return party from transaction response', async () => {
it('getSearchType should return transaction from fetch response', async () => {
const query =
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const expected = SearchTypes.Transaction;
@ -188,8 +155,8 @@ describe('Detect Search', () => {
ok: true,
json: () =>
Promise.resolve({
result: {
tx: query,
transaction: {
hash: query,
},
}),
})
@ -200,17 +167,8 @@ describe('Detect Search', () => {
});
it('getSearchType should return undefined from transaction response', async () => {
const query =
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const query = 'u';
const expected = undefined;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: false,
})
)
);
const result = await getSearchType(query);
expect(result).toBe(expected);
});

View File

@ -1,4 +1,5 @@
import { DATA_SOURCES } from '../../config';
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
export enum SearchTypes {
Transaction = 'transaction',
@ -42,46 +43,88 @@ export const detectTypeFromQuery = (
};
export const detectTypeByFetching = async (
query: string,
type: SearchTypes
query: string
): Promise<SearchTypes | undefined> => {
const TYPES = [SearchTypes.Party, SearchTypes.Transaction];
const hash = toNonHex(query);
const request = await fetch(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
);
if (!TYPES.includes(type)) {
throw new Error('Search type provided not recognised');
}
if (request?.ok) {
const body: BlockExplorerTransaction = await request.json();
if (type === SearchTypes.Transaction) {
const hash = toHex(query);
const request = await fetch(
`${DATA_SOURCES.tendermintUrl}/tx?hash=${hash}`
);
if (request?.ok) {
const body = await request.json();
if (body?.result?.tx) {
return SearchTypes.Transaction;
}
}
} else if (type === SearchTypes.Party) {
const party = toNonHex(query);
const request = await fetch(
`${DATA_SOURCES.tendermintUrl}/tx_search?query="tx.submitter='${party}'"`
);
if (request.ok) {
const body = await request.json();
if (body?.result?.txs?.length) {
return SearchTypes.Party;
}
if (body?.transaction) {
return SearchTypes.Transaction;
}
}
return undefined;
return SearchTypes.Party;
};
// Code commented out because the current solution to detect a hex is temporary (by process of elimination)
// export const detectTypeByFetching = async (
// query: string,
// type: SearchTypes
// ): Promise<SearchTypes | undefined> => {
// const TYPES = [SearchTypes.Party, SearchTypes.Transaction];
//
// if (!TYPES.includes(type)) {
// throw new Error('Search type provided not recognised');
// }
//
// if (type === SearchTypes.Transaction) {
// const hash = toNonHex(query);
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
// );
//
// if (request?.ok) {
// const body: BlockExplorerTransaction = await request.json();
//
// if (body?.transaction) {
// return SearchTypes.Transaction;
// }
// }
// } else if (type === SearchTypes.Party) {
// const party = toNonHex(query);
//
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${party}`
// );
//
// if (request.ok) {
// const body: BlockExplorerTransactions = await request.json();
//
// if (body?.transactions?.length) {
// return SearchTypes.Party;
// }
// }
// }
//
// return undefined;
// };
// export const getSearchType = async (
// query: string
// ): Promise<SearchTypes | undefined> => {
// const searchTypes = detectTypeFromQuery(query);
// const hasResults = searchTypes?.length;
//
// if (hasResults) {
// if (hasResults > 1) {
// const promises = searchTypes.map((type) =>
// detectTypeByFetching(query, type)
// );
// const results = await Promise.all(promises);
// return results.find((result) => result !== undefined);
// }
//
// return searchTypes[0];
// }
//
// return undefined;
// };
export const getSearchType = async (
query: string
): Promise<SearchTypes | undefined> => {
@ -90,11 +133,7 @@ export const getSearchType = async (
if (hasResults) {
if (hasResults > 1) {
const promises = searchTypes.map((type) =>
detectTypeByFetching(query, type)
);
const results = await Promise.all(promises);
return results.find((type) => type !== undefined);
return await detectTypeByFetching(query);
}
return searchTypes[0];

View File

@ -22,32 +22,29 @@ export const Search = () => {
const query = fields.search;
if (!query) {
setError(new Error(t('Search query required')));
} else {
const result = await getSearchType(query);
const urlAsHex = toHex(query);
const unrecognisedError = new Error(
t('Transaction type is not recognised')
);
if (result) {
switch (result) {
case SearchTypes.Party:
navigate(`${Routes.PARTIES}/${urlAsHex}`);
break;
case SearchTypes.Transaction:
navigate(`${Routes.TX}/${urlAsHex}`);
break;
case SearchTypes.Block:
navigate(`${Routes.BLOCKS}/${Number(query)}`);
break;
default:
setError(unrecognisedError);
}
}
setError(unrecognisedError);
return setError(new Error(t('Search query required')));
}
const result = await getSearchType(query);
const urlAsHex = toHex(query);
const unrecognisedError = new Error(
t('Transaction type is not recognised')
);
if (result) {
switch (result) {
case SearchTypes.Party:
return navigate(`${Routes.PARTIES}/${urlAsHex}`);
case SearchTypes.Transaction:
return navigate(`${Routes.TX}/${urlAsHex}`);
case SearchTypes.Block:
return navigate(`${Routes.BLOCKS}/${Number(query)}`);
default:
return setError(unrecognisedError);
}
}
return setError(unrecognisedError);
},
[navigate]
);
@ -61,22 +58,24 @@ export const Search = () => {
{t('Search by block number or transaction hash')}
</label>
<div className="flex items-stretch gap-2">
<Input
{...register('search')}
id="search"
data-testid="search"
className="text-white"
hasError={Boolean(error?.message)}
type="text"
placeholder={t('Enter block number, party id or transaction hash')}
/>
{error?.message && (
<div className="absolute top-[100%] flex-1 w-full">
<InputError data-testid="search-error" intent="danger">
{error.message}
</InputError>
</div>
)}
<div className="flex grow relative">
<Input
{...register('search')}
id="search"
data-testid="search"
className="text-white"
hasError={Boolean(error?.message)}
type="text"
placeholder={t('Enter block number, party id or transaction hash')}
/>
{error?.message && (
<div className="bg-white border border-t-0 border-accent absolute top-[100%] flex-1 w-full pb-2 px-2 rounded-b">
<InputError data-testid="search-error" intent="danger">
{error.message}
</InputError>
</div>
)}
</div>
<Button type="submit" size="sm" data-testid="search-button">
{t('Search')}
</Button>

View File

@ -2,7 +2,7 @@ import React from 'react';
import { TruncatedLink } from '../truncate/truncated-link';
import { Routes } from '../../routes/route-names';
import { TxOrderType } from './tx-order-type';
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import { toHex } from '../search/detect-search';
const TRUNCATE_LENGTH = 14;
@ -13,7 +13,7 @@ export const TxsInfiniteListItem = ({
type,
block,
index,
}: Partial<BlockExplorerTransaction>) => {
}: Partial<BlockExplorerTransactionResult>) => {
if (
!hash ||
!submitter ||

View File

@ -1,9 +1,9 @@
import { TxsInfiniteList } from './txs-infinite-list';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
const generateTxs = (number: number): BlockExplorerTransaction[] => {
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
return Array.from(Array(number)).map((_) => ({
block: '87901',
index: 2,

View File

@ -3,19 +3,19 @@ import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { t } from '@vegaprotocol/react-helpers';
import { TxsInfiniteListItem } from './txs-infinite-list-item';
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
interface TxsInfiniteListProps {
hasMoreTxs: boolean;
areTxsLoading: boolean | undefined;
txs: BlockExplorerTransaction[] | undefined;
txs: BlockExplorerTransactionResult[] | undefined;
loadMoreTxs: () => void;
error: Error | undefined;
className?: string;
}
interface ItemProps {
index: BlockExplorerTransaction;
index: BlockExplorerTransactionResult;
style: React.CSSProperties;
isLoading: boolean;
error: Error | undefined;

View File

@ -13,12 +13,13 @@ import { SubHeading } from '../../../components/sub-heading';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { Panel } from '../../../components/panel';
import { InfoPanel } from '../../../components/info-panel';
import { toNonHex } from '../../../components/search/detect-search';
import { DATA_SOURCES } from '../../../config';
import type { TendermintSearchTransactionResponse } from '../tendermint-transaction-response';
import type {
PartyAssetsQuery,
PartyAssetsQueryVariables,
} from './__generated__/PartyAssetsQuery';
import type { BlockExplorerTransactions } from '../../../routes/types/block-explorer-response';
const PARTY_ASSETS_QUERY = gql`
query PartyAssetsQuery($partyId: ID!) {
@ -57,13 +58,12 @@ const PARTY_ASSETS_QUERY = gql`
const Party = () => {
const { party } = useParams<{ party: string }>();
const partyId = party ? toNonHex(party) : '';
const {
state: { data: partyData },
} = useFetch<TendermintSearchTransactionResponse>(
`${
DATA_SOURCES.tendermintUrl
}/tx_search?query="tx.submitter='${party?.replace('0x', '')}'"`
} = useFetch<BlockExplorerTransactions>(
`${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${partyId}`
);
const { data } = useQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(
@ -71,7 +71,7 @@ const Party = () => {
{
// Don't cache data for this query, party information can move quite quickly
fetchPolicy: 'network-only',
variables: { partyId: party?.replace('0x', '') || '' },
variables: { partyId },
skip: !party,
}
);

View File

@ -5,12 +5,12 @@ import { RouteTitle } from '../../../components/route-title';
import { BlocksRefetch } from '../../../components/blocks';
import { TxsInfiniteList } from '../../../components/txs';
import type {
BlockExplorerTransaction,
BlockExplorerTransactionResult,
BlockExplorerTransactions,
} from '../../../routes/types/block-explorer-response';
interface TxsStateProps {
txsData: BlockExplorerTransaction[];
txsData: BlockExplorerTransactionResult[];
hasMoreTxs: boolean;
lastCursor: string;
}

View File

@ -1,49 +1,43 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { useFetch } from '@vegaprotocol/react-helpers';
import type { TendermintTransactionResponse } from '../tendermint-transaction-response.d';
import type { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
import { DATA_SOURCES } from '../../../config';
import { RouteTitle } from '../../../components/route-title';
import { RenderFetched } from '../../../components/render-fetched';
import { TxContent } from './tx-content';
import { TxDetails } from './tx-details';
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransaction } from '../../../routes/types/block-explorer-response';
import { toNonHex } from '../../../components/search/detect-search';
const Tx = () => {
const { txHash } = useParams<{ txHash: string }>();
const hash = txHash ? toNonHex(txHash) : '';
const {
state: { data: tTxData, loading: tTxLoading, error: tTxError },
} = useFetch<TendermintTransactionResponse>(
`${DATA_SOURCES.tendermintUrl}/tx?hash=${txHash}`
state: { data, loading: tTxLoading, error: tTxError },
} = useFetch<BlockExplorerTransaction>(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(hash)}`
);
const {
state: { data: ceTxData, loading: ceTxLoading, error: ceTxError },
} = useFetch<ChainExplorerTxResponse>(DATA_SOURCES.chainExplorerUrl, {
method: 'POST',
body: JSON.stringify({
tx_hash: txHash,
node_url: `${DATA_SOURCES.tendermintUrl}/`,
}),
});
return (
<section>
<RouteTitle>{t('Transaction details')}</RouteTitle>
<RenderFetched error={tTxError} loading={tTxLoading}>
<TxDetails
className="mb-28"
txData={tTxData?.result}
pubKey={ceTxData?.PubKey}
/>
</RenderFetched>
<>
<TxDetails
className="mb-28"
txData={data?.transaction}
pubKey={data?.transaction.submitter}
/>
<h2 className="text-2xl uppercase mb-4">{t('Transaction content')}</h2>
<RenderFetched error={ceTxError} loading={ceTxLoading}>
<TxContent data={ceTxData} />
<h2 className="text-2xl uppercase mb-4">
{t('Transaction content')}
</h2>
<TxContent data={data?.transaction} />
</>
</RenderFetched>
</section>
);

View File

@ -8,14 +8,14 @@ import {
TableRow,
} from '../../../components/table';
import { TxOrderType } from '../../../components/txs';
import type { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
interface TxContentProps {
data: ChainExplorerTxResponse | undefined;
data: BlockExplorerTransactionResult | undefined;
}
export const TxContent = ({ data }: TxContentProps) => {
if (!data?.Command) {
if (!data?.command) {
return (
<StatusMessage>
{t('Could not retrieve transaction content')}
@ -31,13 +31,13 @@ export const TxContent = ({ data }: TxContentProps) => {
{t('Type')}
</TableHeader>
<TableCell modifier="bordered">
<TxOrderType orderType={data.Type} />
<TxOrderType orderType={data.type} />
</TableCell>
</TableRow>
</TableWithTbody>
<h3 className="font-mono mb-8">{t('Decoded transaction content')}</h3>
<SyntaxHighlighter data={JSON.parse(data.Command)} />
<SyntaxHighlighter data={data.command} />
</>
);
};

View File

@ -1,33 +1,24 @@
import { BrowserRouter as Router } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { truncateByChars } from '@vegaprotocol/react-helpers';
import { TxDetails, txDetailsTruncateLength } from './tx-details';
import type { Result } from '../tendermint-transaction-response.d';
import { TxDetails } from './tx-details';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
const pubKey = 'test';
const hash = '7416753A30622A9E24A06F0172D6C33A95186B36806D96345C6DC5A23FA3F283';
const height = '52987';
const tx =
'ClMIpeWjmKnn77FNEPedA5J9QhJAOGI3YjQzMWNlMmNhNzc4MWMzMTQ1M2IyYjc0MWYwMTJlNzQ1MzBhZDhjMDgzODVkMWQ1YjRiY2VkMTJiNDc1MhKTAQqAATM5NDFlNmExMzQ3MGVhNTlhNGExNmQzMjRiYzlkZjI5YWZkMzYxMDRiZjQ5MzEwZWMxM2ZiOTMxNTM2NGY3ZjU2ZTQyOTJmYTAyZDlhNTBlZDc0OWE0ZjExMzJiNjM2ZTZmMzQ3YzQ2NjdkYmM5OThmYzcyZjYzYzQxMzU4ZTAzEgx2ZWdhL2VkMjU1MTkYAYB9AtI+QDA5MzFhOGZkOGNjOTM1NDU4ZjQ3MGU0MzVhMDU0MTQzODdjZWE2ZjMyOWQ2NDhiZTg5NGZjZDQ0YmQ1MTdhMmI=';
const txData = {
const txData: BlockExplorerTransactionResult = {
hash,
height,
tx,
block: height,
index: 0,
tx_result: {
code: 0,
data: null,
log: '',
info: '',
events: [],
gas_wanted: '0',
gas_used: '0',
codespace: '',
},
submitter: pubKey,
code: 0,
cursor: `${height}.0`,
type: 'type',
command: {},
};
const renderComponent = (txData: Result) => (
const renderComponent = (txData: BlockExplorerTransactionResult) => (
<Router>
<TxDetails txData={txData} pubKey={pubKey} />
</Router>
@ -49,15 +40,6 @@ describe('Transaction details', () => {
expect(screen.getByText(height)).toBeInTheDocument();
});
it('Renders the truncated tx text', () => {
render(renderComponent(txData));
expect(
screen.getByText(
truncateByChars(tx, txDetailsTruncateLength, txDetailsTruncateLength)
)
).toBeInTheDocument();
});
it('Renders a copy button', () => {
render(renderComponent(txData));
expect(screen.getByTestId('copy-tx-to-clipboard')).toBeInTheDocument();

View File

@ -1,18 +1,18 @@
import { Routes } from '../../route-names';
import { ButtonLink, CopyWithTooltip } from '@vegaprotocol/ui-toolkit';
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
import {
TableWithTbody,
TableCell,
TableHeader,
TableRow,
} from '../../../components/table';
import { TruncateInline } from '../../../components/truncate/truncate';
import { t } from '@vegaprotocol/react-helpers';
import { HighlightedLink } from '../../../components/highlighted-link';
import type { Result } from '../tendermint-transaction-response.d';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import React from 'react';
interface TxDetailsProps {
txData: Result | undefined;
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
className?: string;
}
@ -21,7 +21,7 @@ export const txDetailsTruncateLength = 30;
export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
if (!txData) {
return <>{t('Awaiting Tendermint transaction details')}</>;
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
return (
@ -30,6 +30,15 @@ export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
<TableCell>{t('Hash')}</TableCell>
<TableCell modifier="bordered" data-testid="hash">
{txData.hash}
<CopyWithTooltip text={txData.hash}>
<button
title={t('Copy tx to clipboard')}
data-testid="copy-tx-to-clipboard"
className="underline"
>
<Icon name="duplicate" className="ml-2" />
</button>
</CopyWithTooltip>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
@ -44,31 +53,11 @@ export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
<TableCell>{t('Block')}</TableCell>
<TableCell modifier="bordered" data-testid="block">
<HighlightedLink
to={`/${Routes.BLOCKS}/${txData.height}`}
text={txData.height}
to={`/${Routes.BLOCKS}/${txData.block}`}
text={txData.block}
/>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Encoded txn')}</TableCell>
<TableCell
modifier="bordered"
data-testid="encoded-tnx"
className="flex justify-between"
>
<TruncateInline
text={txData.tx}
startChars={txDetailsTruncateLength}
endChars={txDetailsTruncateLength}
/>
<CopyWithTooltip text={txData.tx}>
<ButtonLink
title={t('Copy tx to clipboard')}
data-testid="copy-tx-to-clipboard"
/>
</CopyWithTooltip>
</TableCell>
</TableRow>
</TableWithTbody>
);
};

View File

@ -1,4 +1,4 @@
export interface BlockExplorerTransaction {
export interface BlockExplorerTransactionResult {
block: string;
index: number;
hash: string;
@ -10,5 +10,9 @@ export interface BlockExplorerTransaction {
}
export interface BlockExplorerTransactions {
transactions: BlockExplorerTransaction[];
transactions: BlockExplorerTransactionResult[];
}
export interface BlockExplorerTransaction {
transaction: BlockExplorerTransactionResult;
}