feat(explorer): cancel and order transaction view (#2289)
* feat(explorer): cancel & submit tx view
This commit is contained in:
parent
8639cf0ff6
commit
8b74e63195
@ -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
|
||||
}
|
||||
}
|
78
apps/explorer/src/app/components/deterministic-order-details/__generated__/Order.ts
generated
Normal file
78
apps/explorer/src/app/components/deterministic-order-details/__generated__/Order.ts
generated
Normal 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>;
|
@ -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;
|
@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Panel } from '../panel';
|
||||
import {
|
||||
|
@ -1,9 +1,15 @@
|
||||
query ExplorerMarket($id: ID!) {
|
||||
market(id: $id) {
|
||||
id
|
||||
decimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
product {
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
state
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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>
|
||||
{id}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -22,7 +22,7 @@ const NodeLink = ({ id, ...props }: NodeLinkProps) => {
|
||||
|
||||
return (
|
||||
<Link className="underline" {...props} to={`/${Routes.VALIDATORS}#${id}`}>
|
||||
{label}
|
||||
<code>{label}</code>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -32,7 +32,7 @@ export const TxDetailsChainEvent = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<TableWithTbody>
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
<ChainEvent txData={txData} />
|
||||
</TableWithTbody>
|
||||
|
@ -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':
|
||||
|
@ -24,7 +24,7 @@ export const TxDetailsGeneric = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<TableWithTbody>
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
</TableWithTbody>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 })
|
||||
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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]);
|
||||
});
|
@ -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();
|
||||
}
|
@ -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"
|
||||
|
@ -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: ✅');
|
||||
});
|
||||
});
|
||||
|
@ -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: </span>
|
||||
{index}
|
||||
<span className="xl:hidden uppercase text-zinc-500">
|
||||
Success:
|
||||
</span>
|
||||
{isNumber(code) ? (
|
||||
<ChainResponseCode code={code} hideLabel={true} />
|
||||
) : (
|
||||
code
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -11,6 +11,9 @@ const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
||||
submitter:
|
||||
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
|
||||
type: 'Submit Order',
|
||||
signature: {
|
||||
value: '123',
|
||||
},
|
||||
code: 0,
|
||||
cursor: '87901.2',
|
||||
command: {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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">
|
||||
|
@ -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 = () => {
|
||||
|
@ -21,6 +21,9 @@ const txData: BlockExplorerTransactionResult = {
|
||||
cursor: `${height}.0`,
|
||||
type: 'type',
|
||||
command: {} as ValidatorHeartbeat,
|
||||
signature: {
|
||||
value: '123',
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (txData: BlockExplorerTransactionResult) => (
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user