feat(explorer): cancel and order transaction view (#2289)

* feat(explorer): cancel & submit tx view
This commit is contained in:
Edd 2022-12-02 14:58:33 +00:00 committed by GitHub
parent 8639cf0ff6
commit 8b74e63195
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 866 additions and 120 deletions

View File

@ -0,0 +1,33 @@
fragment ExplorerDeterministicOrderFields on Order {
id
type
reference
status
version
createdAt
expiresAt
timeInForce
price
side
remaining
size
rejectionReason
party {
id
}
market {
id
decimalPlaces
tradableInstrument {
instrument {
name
}
}
}
}
query ExplorerDeterministicOrder($orderId: ID!) {
orderByID(id: $orderId) {
...ExplorerDeterministicOrderFields
}
}

View File

@ -0,0 +1,78 @@
import { Schema as Types } from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } };
export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
orderId: Types.Scalars['ID'];
}>;
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } } };
export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
fragment ExplorerDeterministicOrderFields on Order {
id
type
reference
status
version
createdAt
expiresAt
timeInForce
price
side
remaining
size
rejectionReason
party {
id
}
market {
id
decimalPlaces
tradableInstrument {
instrument {
name
}
}
}
}
`;
export const ExplorerDeterministicOrderDocument = gql`
query ExplorerDeterministicOrder($orderId: ID!) {
orderByID(id: $orderId) {
...ExplorerDeterministicOrderFields
}
}
${ExplorerDeterministicOrderFieldsFragmentDoc}`;
/**
* __useExplorerDeterministicOrderQuery__
*
* To run a query within a React component, call `useExplorerDeterministicOrderQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerDeterministicOrderQuery` 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 } = useExplorerDeterministicOrderQuery({
* variables: {
* orderId: // value for 'orderId'
* },
* });
*/
export function useExplorerDeterministicOrderQuery(baseOptions: Apollo.QueryHookOptions<ExplorerDeterministicOrderQuery, ExplorerDeterministicOrderQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerDeterministicOrderQuery, ExplorerDeterministicOrderQueryVariables>(ExplorerDeterministicOrderDocument, options);
}
export function useExplorerDeterministicOrderLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerDeterministicOrderQuery, ExplorerDeterministicOrderQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerDeterministicOrderQuery, ExplorerDeterministicOrderQueryVariables>(ExplorerDeterministicOrderDocument, options);
}
export type ExplorerDeterministicOrderQueryHookResult = ReturnType<typeof useExplorerDeterministicOrderQuery>;
export type ExplorerDeterministicOrderLazyQueryHookResult = ReturnType<typeof useExplorerDeterministicOrderLazyQuery>;
export type ExplorerDeterministicOrderQueryResult = Apollo.QueryResult<ExplorerDeterministicOrderQuery, ExplorerDeterministicOrderQueryVariables>;

View File

@ -0,0 +1,157 @@
import { t } from '@vegaprotocol/react-helpers';
import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
import type { Schema } from '@vegaprotocol/types';
import { MarketLink } from '../links';
import PriceInMarket from '../price-in-market/price-in-market';
import { Time } from '../time';
export interface DeterministicOrderDetailsProps {
id: string;
// Version to fetch, with 0 being 'latest' and 1 being 'first'. Defaults to 0
version?: number;
}
const statusText: Record<Schema.OrderStatus, string> = {
STATUS_ACTIVE: t('Active'),
STATUS_CANCELLED: t('Cancelled'),
STATUS_EXPIRED: t('Expired'),
STATUS_FILLED: t('Filled'),
STATUS_PARKED: t('Parked'),
// Intentionally vague - table shows partial fills
STATUS_PARTIALLY_FILLED: t('Active'),
STATUS_REJECTED: t('Rejected'),
STATUS_STOPPED: t('Stopped'),
};
const sideText: Record<Schema.Side, string> = {
SIDE_BUY: t('Buy'),
SIDE_SELL: t('Sell'),
};
const tifShort: Record<Schema.OrderTimeInForce, string> = {
TIME_IN_FORCE_FOK: t('FOK'),
TIME_IN_FORCE_GFA: t('GFA'),
TIME_IN_FORCE_GFN: t('GFN'),
TIME_IN_FORCE_GTC: t('GTC'),
TIME_IN_FORCE_GTT: t('GTT'),
TIME_IN_FORCE_IOC: t('IOC'),
};
const tifFull: Record<Schema.OrderTimeInForce, string> = {
TIME_IN_FORCE_FOK: t('Fill or Kill'),
TIME_IN_FORCE_GFA: t('Good for Auction'),
TIME_IN_FORCE_GFN: t('Good for Normal'),
TIME_IN_FORCE_GTC: t("Good 'til Cancel"),
TIME_IN_FORCE_GTT: t("Good 'til Time"),
TIME_IN_FORCE_IOC: t('Immediate or Cancel'),
};
const wrapperClasses =
'grid lg:grid-cols-1 flex items-center max-w-xl border border-zinc-200 dark:border-zinc-800 rounded-md pv-2 ph-5 mb-5';
/**
* This component renders the *current* details for an order
*
* An important part of this component is that unlike most of the rest of the Explorer,
* it is displaying 'live' data. With the current APIs it's impossible to get the state
* of the order at a specific point in time. While one day that might be possible, this
* order component is not built with that in mind.
*
* @param param0
* @returns
*/
const DeterministicOrderDetails = ({
id,
version = 0,
}: DeterministicOrderDetailsProps) => {
const { data, error } = useExplorerDeterministicOrderQuery({
variables: { orderId: id },
});
if (error || (data && !data.orderByID)) {
return (
<div className={wrapperClasses}>
<div className="mb-12 lg:mb-0">
<div className="relative block rounded-lg px-3 py-6 md:px-6 lg:-mr-7">
<h2 className="text-3xl font-bold mb-4 display-5">
{t('Order not found')}
</h2>
<p className="text-gray-500 mb-12">
{t('No order created from this transaction')}
</p>
</div>
</div>
</div>
);
}
if (!data || !data.orderByID) {
return null;
}
const o = data.orderByID;
return (
<div className={wrapperClasses}>
<div className="mb-12 lg:mb-0">
<div className="relative block px-3 py-6 md:px-6 lg:-mr-7">
<h2 className="text-3xl font-bold mb-4 display-5">
<abbr title={tifFull[o.timeInForce]} className="bb-dotted mr-2">
{tifShort[o.timeInForce]}
</abbr>
{sideText[o.side]}
<span className="mx-5 text-base">@</span>
<PriceInMarket price={o.price} marketId={o.market.id} />
</h2>
<p className="text-gray-500 mb-4">
In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />.
</p>
{o.reference ? (
<p className="text-gray-500 mb-4">
<span>{t('Reference')}</span>: {o.reference}
</p>
) : null}
<div className="grid md:grid-cols-4 gap-x-6">
{version !== 0 ? null : (
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Status')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
{statusText[o.status]}
</h5>
</div>
)}
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
{o.size}
</h5>
</div>
{version !== 0 ? null : (
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Remaining')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
{o.remaining}
</h5>
</div>
)}
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Version')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
{o.version}
</h5>
</div>
</div>
</div>
</div>
</div>
);
};
export default DeterministicOrderDetails;

View File

@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import { useMemo } from 'react';
import type { ReactNode } from 'react';
import { Panel } from '../panel';
import {

View File

@ -1,9 +1,15 @@
query ExplorerMarket($id: ID!) {
market(id: $id) {
id
decimalPlaces
tradableInstrument {
instrument {
name
product {
... on Future {
quoteName
}
}
}
}
state

View File

@ -8,16 +8,22 @@ export type ExplorerMarketQueryVariables = Types.Exact<{
}>;
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } | null };
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null };
export const ExplorerMarketDocument = gql`
query ExplorerMarket($id: ID!) {
market(id: $id) {
id
decimalPlaces
tradableInstrument {
instrument {
name
product {
... on Future {
quoteName
}
}
}
}
state

View File

@ -4,10 +4,11 @@ import type { MockedResponse } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import MarketLink from './market-link';
import { ExplorerMarketDocument } from './__generated__/Market';
import { GraphQLError } from 'graphql';
function renderComponent(id: string, mock: MockedResponse[]) {
function renderComponent(id: string, mocks: MockedResponse[]) {
return (
<MockedProvider mocks={mock}>
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<MarketLink id={id} />
</MemoryRouter>
@ -21,6 +22,26 @@ describe('Market link component', () => {
expect(res.getByText('123')).toBeInTheDocument();
});
it('Renders the ID with an emoji on error', async () => {
const mock = {
request: {
query: ExplorerMarketDocument,
variables: {
id: '456',
},
},
result: {
errors: [new GraphQLError('No such market')],
},
};
const res = render(renderComponent('456', [mock]));
// The ID
expect(res.getByText('456')).toBeInTheDocument();
// The emoji
expect(await res.findByRole('img')).toBeInTheDocument();
});
it('Renders the market name when the query returns a result', async () => {
const mock = {
request: {
@ -33,10 +54,14 @@ describe('Market link component', () => {
data: {
market: {
id: '123',
decimalPlaces: 5,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
name: 'test-label',
product: {
quoteName: 'dai',
},
},
},
},

View File

@ -4,6 +4,7 @@ import { useExplorerMarketQuery } from './__generated__/Market';
import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import { t } from '@vegaprotocol/react-helpers';
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
@ -15,14 +16,25 @@ export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
* it will use the ID instead
*/
const MarketLink = ({ id, ...props }: MarketLinkProps) => {
const { data } = useExplorerMarketQuery({
const { data, error, loading } = useExplorerMarketQuery({
variables: { id },
});
let label = id;
let label = <span>{id}</span>;
if (data?.market?.tradableInstrument.instrument.name) {
label = data.market.tradableInstrument.instrument.name;
if (!loading) {
if (data?.market?.tradableInstrument.instrument.name) {
label = <span>{data.market.tradableInstrument.instrument.name}</span>;
} else if (error) {
label = (
<div title={t('Unknown market')}>
<span role="img" aria-label="Unknown market" className="img">
</span>
&nbsp;{id}
</div>
);
}
}
return (

View File

@ -22,7 +22,7 @@ const NodeLink = ({ id, ...props }: NodeLinkProps) => {
return (
<Link className="underline" {...props} to={`/${Routes.VALIDATORS}#${id}`}>
{label}
<code>{label}</code>
</Link>
);
};

View File

@ -10,7 +10,11 @@ export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
return (
<Link className="underline" {...props} to={`/${Routes.PARTIES}/${id}`}>
<Link
className="underline font-mono"
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
{id}
</Link>
);

View File

@ -0,0 +1,92 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import PriceInMarket from './price-in-market';
import { ExplorerMarketDocument } from '../links/market-link/__generated__/Market';
function renderComponent(
price: string,
marketId: string,
mocks: MockedResponse[]
) {
return (
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<PriceInMarket marketId={marketId} price={price} />
</MemoryRouter>
</MockedProvider>
);
}
describe('Price in Market component', () => {
it('Renders the raw price when there is no market data', () => {
const res = render(renderComponent('100', '123', []));
expect(res.getByText('100')).toBeInTheDocument();
});
it('Renders the formatted price when market data is fetched', async () => {
const mock = {
request: {
query: ExplorerMarketDocument,
variables: {
id: '123',
},
},
result: {
data: {
market: {
id: '123',
decimalPlaces: 2,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
name: 'test dai',
product: {
__typename: 'Future',
quoteName: 'dai',
},
},
},
},
},
},
};
const res = render(renderComponent('100', '123', [mock]));
expect(await res.findByText('1.00')).toBeInTheDocument();
expect(await res.findByText('dai')).toBeInTheDocument();
});
it('Leaves the market id when the market is not found', async () => {
const mock = {
request: {
query: ExplorerMarketDocument,
variables: {
id: '123',
},
},
error: new Error('No such market'),
};
const res = render(renderComponent('100', '123', [mock]));
expect(await res.findByText('100')).toBeInTheDocument();
});
it('Renders `Market` instead of a price for market orders: 0 price', () => {
const res = render(renderComponent('0', '123', []));
expect(res.getByText('Market')).toBeInTheDocument();
});
it('Renders `Market` instead of a price for market orders: undefined price', () => {
const res = render(
renderComponent(undefined as unknown as string, '123', [])
);
expect(res.getByText('Market')).toBeInTheDocument();
});
it('Renders `Market` instead of a price for market orders: empty price', () => {
const res = render(renderComponent('', '123', []));
expect(res.getByText('Market')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,47 @@
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import isUndefined from 'lodash/isUndefined';
import { useExplorerMarketQuery } from '../links/market-link/__generated__/Market';
import get from 'lodash/get';
export type PriceInMarketProps = {
marketId: 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 PriceInMarket = ({ marketId, price }: PriceInMarketProps) => {
const { data } = useExplorerMarketQuery({
variables: { id: marketId },
});
let label = price;
if (data && data.market?.decimalPlaces) {
label = addDecimalsFormatNumber(price, data.market.decimalPlaces);
}
const suffix = get(
data,
'market.tradableInstrument.instrument.product.quoteName',
''
);
if (isUndefined(price) || price === '' || price === '0') {
return (
<span>
<abbr title={'Best available price'}>{t('Market')}</abbr> {suffix}
</span>
);
} else {
return (
<div className="inline-block">
<span>{label}</span> <span>{suffix}</span>
</div>
);
}
};
export default PriceInMarket;

View File

@ -0,0 +1,44 @@
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
export const ErrorCodes = new Map([
[51, 'Transaction failed validation'],
[60, 'Transaction could not be decoded'],
[70, 'Internal error'],
[80, 'Unknown command'],
[89, 'Rejected as spam'],
[0, 'Success'],
]);
export const successCodes = new Set([0]);
interface ChainResponseCodeProps {
code: number;
hideLabel?: boolean;
}
/**
* Someone deposited some of a builtin asset. Builtin assets
* have no value outside the Vega chain and should appear only
* on Test networks.
*/
export const ChainResponseCode = ({
code,
hideLabel = false,
}: ChainResponseCodeProps) => {
const isSuccess = successCodes.has(code);
const icon = isSuccess ? '✅' : '❌';
const label = ErrorCodes.get(code) || 'Unknown response code';
return (
<div title={`Response code: ${code} - ${label}`}>
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
role="img"
>
{icon}
</span>
{hideLabel ? null : <span>{label}</span>}
</div>
);
};

View File

@ -6,6 +6,7 @@ import { TimeAgo } from '../../../time-ago';
import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../../routes/blocks/tendermint-blocks-response';
import { Time } from '../../../time';
import { ChainResponseCode } from '../chain-response-code/chain-reponse.code';
interface TxDetailsSharedProps {
txData: BlockExplorerTransactionResult | undefined;
@ -28,13 +29,19 @@ export const TxDetailsShared = ({
}
const time: string = blockData?.result.block.header.time || '';
const height: string = blockData?.result.block.header.height || '';
const height: string = blockData?.result.block.header.height || txData.block;
return (
<>
<TableRow modifier="bordered">
<TableCell>{t('Type')}</TableCell>
<TableCell>{txData.type}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Hash')}</TableCell>
<TableCell>{txData.hash}</TableCell>
<TableCell>
<code>{txData.hash}</code>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Submitter')}</TableCell>
@ -63,6 +70,12 @@ export const TxDetailsShared = ({
)}
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Response code')}</TableCell>
<TableCell>
<ChainResponseCode code={txData.code} />
</TableCell>
</TableRow>
</>
);
};

View File

@ -35,7 +35,7 @@ export const TxDetailsBatch = ({
const countTotal = countSubmissions + countAmendments + countCancellations;
return (
<TableWithTbody>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Batch size')}</TableCell>

View File

@ -32,7 +32,7 @@ export const TxDetailsChainEvent = ({
}
return (
<TableWithTbody>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<ChainEvent txData={txData} />
</TableWithTbody>

View File

@ -11,6 +11,8 @@ import { TxDetailsBatch } from './tx-batch';
import { TxDetailsChainEvent } from './tx-chain-event';
import { TxContent } from '../../../routes/txs/id/tx-content';
import { TxDetailsNodeVote } from './tx-node-vote';
import { TxDetailsOrderCancel } from './tx-order-cancel';
import get from 'lodash/get';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@ -36,6 +38,8 @@ export const TxDetailsWrapper = ({
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const raw = get(blockData, `result.block.data.txs[${txData.index}]`);
return (
<>
<section>{child({ txData, pubKey, blockData })}</section>
@ -45,12 +49,12 @@ export const TxDetailsWrapper = ({
<TxContent data={txData} />
</details>
<details title={t('Raw transaction')} className="mt-3">
<summary className="cursor-pointer">{t('Raw transaction')}</summary>
<code className="break-all font-mono text-xs">
{blockData?.result.block.data.txs[txData.index]}
</code>
</details>
{raw ? (
<details title={t('Raw transaction')} className="mt-3">
<summary className="cursor-pointer">{t('Raw transaction')}</summary>
<code className="break-all font-mono text-xs">{raw}</code>
</details>
) : null}
</>
);
};
@ -69,6 +73,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
switch (txData.type) {
case 'Submit Order':
return TxDetailsOrder;
case 'Cancel Order':
return TxDetailsOrderCancel;
case 'Validator Heartbeat':
return TxDetailsHeartbeat;
case 'Amend LiquidityProvision Order':

View File

@ -24,7 +24,7 @@ export const TxDetailsGeneric = ({
}
return (
<TableWithTbody>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
</TableWithTbody>
);

View File

@ -60,7 +60,7 @@ export const TxDetailsHeartbeat = ({
const blockHeight = txData.command.blockHeight || '';
return (
<TableWithTbody>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Node')}</TableCell>

View File

@ -28,7 +28,7 @@ export const TxDetailsLPAmend = ({
const marketId = txData.command.liquidityProvisionAmendment?.marketId || '';
return (
<TableWithTbody>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>

View File

@ -38,7 +38,7 @@ export const TxDetailsNodeVote = ({
}
return (
<TableWithTbody>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{data && !!data.deposit
? TxDetailsNodeVoteDeposit({ deposit: data })

View File

@ -0,0 +1,49 @@
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import { MarketLink } from '../../links/';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import DeterministicOrderDetails from '../../deterministic-order-details/deterministic-order-details';
interface TxDetailsOrderCancelProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Someone cancelled an order
*/
export const TxDetailsOrderCancel = ({
txData,
pubKey,
blockData,
}: TxDetailsOrderCancelProps) => {
if (!txData || !txData.command.orderCancellation) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const marketId: string = txData.command.orderCancellation.marketId || '-';
const orderId: string = txData.command.orderCancellation.orderId || '-';
return (
<>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
</TableCell>
</TableRow>
</TableWithTbody>
{orderId !== '-' ? <DeterministicOrderDetails id={orderId} /> : null}
</>
);
};

View File

@ -4,6 +4,8 @@ import { MarketLink } from '../../links/';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import DeterministicOrderDetails from '../../deterministic-order-details/deterministic-order-details';
interface TxDetailsOrderProps {
txData: BlockExplorerTransactionResult | undefined;
@ -27,15 +29,32 @@ export const TxDetailsOrder = ({
}
const marketId = txData.command.orderSubmission.marketId || '-';
let deterministicId = '';
const sig = txData.signature.value as string;
if (sig) {
deterministicId = txSignatureToDeterministicId(sig);
}
return (
<TableWithTbody>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
</TableCell>
</TableRow>
</TableWithTbody>
<>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
</TableCell>
</TableRow>
</TableWithTbody>
{deterministicId.length > 0 ? (
<DeterministicOrderDetails id={deterministicId} version={1} />
) : null}
</>
);
};

View File

@ -0,0 +1,22 @@
import { hexToString, txSignatureToDeterministicId } from './deterministic-ids';
it('txSignatureToDeterministicId Turns a known signature in to a known deterministic ID', () => {
const signature =
'0f34fc11ffb7513295d8545a96f9628c388ef1aee028e94f0399fc5a4a7d867e5c5516ea3eec1d4ec4b2e80d8bc69ccdbde4af4494d7c9fe18450cb3a442e50e';
const id = txSignatureToDeterministicId(signature);
expect(id).toStrictEqual(
'9df52e506304bf2efb0220506af688ffadbc8f52b6072b73c3694513b410137d'
);
});
it('hexToString only accepts a hex string', () => {
expect(() => {
hexToString('test');
}).toThrowError('is not a hex string');
});
it('hexToString encodes a known good value as bytes', () => {
const hex = 'edd';
const res = hexToString(hex);
expect(res).toEqual([14, 221]);
});

View File

@ -0,0 +1,39 @@
import { sha3_256 } from 'js-sha3';
/**
* Encodes a string as bytes
* @param hex
* @returns number[]
*/
export function hexToString(hex: string) {
if (!hex.match(/^[0-9a-fA-F]+$/)) {
throw new Error('is not a hex string.');
}
const paddedHex = hex.length % 2 !== 0 ? `0${hex}` : hex;
const bytes = [];
for (let n = 0; n < paddedHex.length; n += 2) {
const code = parseInt(paddedHex.substring(n, n + 2), 16);
bytes.push(code);
}
return bytes;
}
/**
* Given a transaction signature string, returns the deterministic
* ID that the transaction will get. This works for:
* - orders
* - proposals
*
* @param signature
* @return string deterministic id
*/
export function txSignatureToDeterministicId(signature: string): string {
const bytes = hexToString(signature);
const hash = sha3_256.create();
hash.update(bytes);
return hash.hex();
}

View File

@ -3,7 +3,7 @@ import type { components } from '../../../types/explorer';
interface TxOrderTypeProps {
orderType: string;
chainEvent?: components['schemas']['v1ChainEvent'];
command?: components['schemas']['v1InputData'];
className?: string;
}
@ -18,6 +18,7 @@ const displayString: StringMap = {
OrderAmendment: 'Order Amendment',
VoteSubmission: 'Vote Submission',
WithdrawSubmission: 'Withdraw Submission',
Withdraw: 'Withdraw Request',
LiquidityProvisionSubmission: 'Liquidity Provision',
LiquidityProvisionCancellation: 'Liquidity Cancellation',
LiquidityProvisionAmendment: 'Liquidity Amendment',
@ -36,6 +37,31 @@ const displayString: StringMap = {
ValidatorHeartbeat: 'Validator Heartbeat',
};
/**
* Given a proposal, will return a specific label
* @param chainEvent
* @returns
*/
export function getLabelForProposal(
proposal: components['schemas']['v1ProposalSubmission']
): string {
if (proposal.terms?.newAsset) {
return t('Proposal: New asset');
} else if (proposal.terms?.updateAsset) {
return t('Proposal: Update asset');
} else if (proposal.terms?.newMarket) {
return t('Proposal: New market');
} else if (proposal.terms?.updateMarket) {
return t('Proposal: Update market');
} else if (proposal.terms?.updateNetworkParameter) {
return t('Proposal: Network parameter');
} else if (proposal.terms?.newFreeform) {
return t('Proposal: Freeform');
} else {
return t('Proposal');
}
}
/**
* Given a chain event, will try to provide a more useful label
* @param chainEvent
@ -86,17 +112,44 @@ export function getLabelForChainEvent(
return t('Chain Event');
}
export const TxOrderType = ({ orderType, chainEvent }: TxOrderTypeProps) => {
/**
* Actually it's a transaction type, rather than an order type - this just
* hasn't been refactored yet.
*
* There's no logic to the colours used -
* - Chain events are white text on pink background
* - Proposals are black text on yellow background
*
* Both of these were opted as they're easy to pick out when scrolling
* the infinite transaction list
*
* The multiple paths on this one are different types from the old chain
* explorer and the new one. When there are no longer two different APIs
* in use, these should be consistent. For now, the view on a block page
* can have a different label to the transaction list - but the colours
* are consistent.
*/
export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
let type = displayString[orderType] || orderType;
let colours = 'text-white dark:text-white bg-zinc-800 dark:bg-zinc-800';
// This will get unwieldy and should probably produce a different colour of tag
if (type === 'Chain Event' && !!chainEvent) {
type = getLabelForChainEvent(chainEvent);
if (type === 'Chain Event' && !!command?.chainEvent) {
type = getLabelForChainEvent(command.chainEvent);
colours =
'text-white dark-text-white bg-vega-pink-dark dark:bg-vega-pink-dark';
} else if (type === 'Proposal' || type === 'Governance Proposal') {
if (command && !!command.proposalSubmission) {
type = getLabelForProposal(command.proposalSubmission);
}
colours = 'text-black bg-vega-yellow';
}
if (type === 'Vote on Proposal' || type === 'Vote Submission') {
colours = 'text-black bg-vega-yellow';
}
return (
<div
data-testid="tx-type"

View File

@ -5,65 +5,78 @@ import { MemoryRouter } from 'react-router-dom';
describe('Txs infinite list item', () => {
it('should display "missing vital data" if "type" data missing', () => {
render(
<TxsInfiniteListItem
type={undefined}
submitter="test"
hash=""
index={0}
block="1"
/>
<MemoryRouter>
<TxsInfiniteListItem
type={undefined}
submitter="test"
hash=""
code={0}
block="1"
/>
</MemoryRouter>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('should display "missing vital data" if "hash" data missing', () => {
render(
<TxsInfiniteListItem
type="test"
submitter="test"
hash={undefined}
index={0}
block="1"
/>
<MemoryRouter>
<TxsInfiniteListItem
type="test"
submitter="test"
hash={undefined}
code={0}
block="1"
command={{}}
/>
</MemoryRouter>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('should display "missing vital data" if "submitter" data missing', () => {
render(
<TxsInfiniteListItem
type="test"
submitter={undefined}
hash="test"
index={0}
block="1"
/>
<MemoryRouter>
<TxsInfiniteListItem
type="test"
submitter={undefined}
hash="test"
code={0}
block="1"
command={{}}
/>
</MemoryRouter>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('should display "missing vital data" if "block" data missing', () => {
render(
<TxsInfiniteListItem
type="test"
submitter="test"
hash="test"
index={0}
block={undefined}
/>
<MemoryRouter>
<TxsInfiniteListItem
type="test"
submitter="test"
hash="test"
code={0}
block={undefined}
command={{}}
/>
</MemoryRouter>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('should display "missing vital data" if "index" data missing', () => {
it('should display "missing vital data" if "code" data missing', () => {
render(
<TxsInfiniteListItem
type="test"
submitter="test"
hash="test"
index={undefined}
block="1"
/>
<MemoryRouter>
<TxsInfiniteListItem
type="test"
submitter="test"
hash="test"
block="1"
command={{}}
/>
</MemoryRouter>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
@ -75,8 +88,9 @@ describe('Txs infinite list item', () => {
type="testType"
submitter="testPubKey"
hash="testTxHash"
index={1}
block="1"
code={0}
command={{}}
/>
</MemoryRouter>
);
@ -84,6 +98,6 @@ describe('Txs infinite list item', () => {
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
expect(screen.getByTestId('tx-index')).toHaveTextContent('1');
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success: ✅');
});
});

View File

@ -4,23 +4,26 @@ import { Routes } from '../../routes/route-names';
import { TxOrderType } from './tx-order-type';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import { toHex } from '../search/detect-search';
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
import isNumber from 'lodash/isNumber';
const TRUNCATE_LENGTH = 14;
const TRUNCATE_LENGTH = 5;
export const TxsInfiniteListItem = ({
hash,
code,
submitter,
type,
block,
index,
command,
}: Partial<BlockExplorerTransactionResult>) => {
if (
!hash ||
!submitter ||
!type ||
code === undefined ||
block === undefined ||
index === undefined
command === undefined
) {
return <div>Missing vital data</div>;
}
@ -55,7 +58,7 @@ export const TxsInfiniteListItem = ({
/>
</div>
<div className="text-sm col-span-5 xl:col-span-2 leading-none flex items-center">
<TxOrderType orderType={type} chainEvent={command?.chainEvent} />
<TxOrderType orderType={type} command={command} />
</div>
<div
className="text-sm col-span-3 xl:col-span-1 leading-none flex items-center"
@ -71,10 +74,16 @@ export const TxsInfiniteListItem = ({
</div>
<div
className="text-sm col-span-2 xl:col-span-1 leading-none flex items-center"
data-testid="tx-index"
data-testid="tx-success"
>
<span className="xl:hidden uppercase text-zinc-500">Index:&nbsp;</span>
{index}
<span className="xl:hidden uppercase text-zinc-500">
Success:&nbsp;
</span>
{isNumber(code) ? (
<ChainResponseCode code={code} hideLabel={true} />
) : (
code
)}
</div>
</div>
);

View File

@ -11,6 +11,9 @@ const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
submitter:
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
type: 'Submit Order',
signature: {
value: '123',
},
code: 0,
cursor: '87901.2',
command: {

View File

@ -31,10 +31,19 @@ const Item = ({ index, style, isLoading, error }: ItemProps) => {
} else if (isLoading) {
content = t('Loading...');
} else {
const { hash, submitter, type, command, block, index: blockIndex } = index;
const {
hash,
submitter,
type,
command,
block,
code,
index: blockIndex,
} = index;
content = (
<TxsInfiniteListItem
type={type}
code={code}
command={command}
submitter={submitter}
hash={hash}
@ -82,7 +91,7 @@ export const TxsInfiniteList = ({
<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 className="col-span-1">Success</div>
</div>
<div data-testid="infinite-scroll-wrapper">
<InfiniteLoader

View File

@ -61,7 +61,7 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
/>
</TableCell>
<TableCell modifier="bordered">
<TxOrderType orderType={type} chainEvent={command} />
<TxOrderType orderType={type} command={command} />
</TableCell>
</TableRow>
);

View File

@ -58,6 +58,24 @@ const Block = () => {
{blockData && (
<>
<TableWithTbody className="mb-8">
<TableRow modifier="bordered">
<TableHeader scope="row">{t('Block hash')}</TableHeader>
<TableCell modifier="bordered">
<code>{blockData.result.block_id.hash}</code>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableHeader scope="row">{t('Data hash')}</TableHeader>
<TableCell modifier="bordered">
<code>{blockData.result.block.header.data_hash}</code>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableHeader scope="row">{t('Consensus hash')}</TableHeader>
<TableCell modifier="bordered">
<code>{blockData.result.block.header.consensus_hash}</code>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableHeader scope="row">Mined by</TableHeader>
<TableCell modifier="bordered">

View File

@ -17,22 +17,22 @@ import type { Schema } from '@vegaprotocol/types';
import get from 'lodash/get';
const accountTypeString: Record<Schema.AccountType, string> = {
ACCOUNT_TYPE_BOND: 'Bond',
ACCOUNT_TYPE_EXTERNAL: 'External',
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: 'Fees (Infrastructure)',
ACCOUNT_TYPE_FEES_LIQUIDITY: 'Fees (Liquidity)',
ACCOUNT_TYPE_FEES_MAKER: 'Fees (Maker)',
ACCOUNT_TYPE_GENERAL: 'General',
ACCOUNT_TYPE_GLOBAL_INSURANCE: 'Global Insurance Pool',
ACCOUNT_TYPE_GLOBAL_REWARD: 'Global Reward Pool',
ACCOUNT_TYPE_INSURANCE: 'Insurance',
ACCOUNT_TYPE_MARGIN: 'Margin',
ACCOUNT_TYPE_PENDING_TRANSFERS: 'Pending Transfers',
ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: 'Reward - LP Fees received',
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES: 'Reward - Maker fees paid',
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES: 'Reward - Maker fees received',
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: 'Reward - Market proposers',
ACCOUNT_TYPE_SETTLEMENT: 'Settlement',
ACCOUNT_TYPE_BOND: t('Bond'),
ACCOUNT_TYPE_EXTERNAL: t('External'),
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: t('Fees (Infrastructure)'),
ACCOUNT_TYPE_FEES_LIQUIDITY: t('Fees (Liquidity)'),
ACCOUNT_TYPE_FEES_MAKER: t('Fees (Maker)'),
ACCOUNT_TYPE_GENERAL: t('General'),
ACCOUNT_TYPE_GLOBAL_INSURANCE: t('Global Insurance Pool'),
ACCOUNT_TYPE_GLOBAL_REWARD: t('Global Reward Pool'),
ACCOUNT_TYPE_INSURANCE: t('Insurance'),
ACCOUNT_TYPE_MARGIN: t('Margin'),
ACCOUNT_TYPE_PENDING_TRANSFERS: t('Pending Transfers'),
ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: t('Reward - LP Fees received'),
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES: t('Reward - Maker fees paid'),
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES: t('Reward - Maker fees received'),
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: t('Reward - Market proposers'),
ACCOUNT_TYPE_SETTLEMENT: t('Settlement'),
};
const Party = () => {

View File

@ -21,6 +21,9 @@ const txData: BlockExplorerTransactionResult = {
cursor: `${height}.0`,
type: 'type',
command: {} as ValidatorHeartbeat,
signature: {
value: '123',
},
};
const renderComponent = (txData: BlockExplorerTransactionResult) => (

View File

@ -1,9 +1,5 @@
import { Routes } from '../../route-names';
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import React from 'react';
import { TruncateInline } from '../../../components/truncate/truncate';
import { Link } from 'react-router-dom';
import { TxDetailsWrapper } from '../../../components/txs/details/tx-details-wrapper';
interface TxDetailsProps {
@ -18,22 +14,8 @@ export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const truncatedSubmitter = (
<TruncateInline text={pubKey || ''} startChars={5} endChars={5} />
);
return (
<section className="mb-10">
<h3 className="text-l xl:text-l uppercase mb-4">
{txData.type} by{' '}
<Link
className="font-bold underline"
to={`/${Routes.PARTIES}/${pubKey}`}
>
{truncatedSubmitter}
</Link>
</h3>
<TxDetailsWrapper height={txData.block} txData={txData} pubKey={pubKey} />
</section>
);

View File

@ -9,7 +9,10 @@ export interface BlockExplorerTransactionResult {
type: string;
code: number;
cursor: string;
command: components['schemas']['v1InputData'];
command: components['schemas']['blockexplorerv1transaction'];
signature: {
value: string;
};
}
export interface BlockExplorerTransactions {