feat(explorer): batch market instructions (#2617)
* feat(explorer): add basic table for all types in a batch * feat(explorer): use marketlink for market ids in batch * feat(explorer): add ordersummary * feat(explorer): improve caching with some slight overfetching and cache config
This commit is contained in:
parent
cd0ed05eb8
commit
ff53e5e841
@ -1,23 +1,15 @@
|
||||
import classnames from 'classnames';
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
EnvironmentProvider,
|
||||
NetworkLoader,
|
||||
useEnvironment,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
||||
import { NetworkInfo } from '@vegaprotocol/network-info';
|
||||
import { Nav } from './components/nav';
|
||||
import { Header } from './components/header';
|
||||
import { Main } from './components/main';
|
||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||
import { ENV } from './config/env';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
|
||||
function App() {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
@ -26,18 +18,9 @@ function App() {
|
||||
setMenuOpen(false);
|
||||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
Sentry.init({
|
||||
dsn: ENV.dsn,
|
||||
integrations: [new BrowserTracing()],
|
||||
tracesSampleRate: 1,
|
||||
environment: VEGA_ENV,
|
||||
});
|
||||
}, [VEGA_ENV]);
|
||||
|
||||
const cacheConfig: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
Node: {
|
||||
statistics: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
|
@ -18,7 +18,6 @@ const AssetBalance = ({
|
||||
showAssetLink = true,
|
||||
}: AssetBalanceProps) => {
|
||||
const { data } = useExplorerAssetQuery({
|
||||
fetchPolicy: 'cache-first',
|
||||
variables: { id: assetId },
|
||||
});
|
||||
|
||||
|
@ -16,7 +16,6 @@ export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
*/
|
||||
const AssetLink = ({ id, ...props }: AssetLinkProps) => {
|
||||
const { data } = useExplorerAssetQuery({
|
||||
fetchPolicy: 'cache-first',
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
|
@ -29,7 +29,7 @@ const MarketLink = ({ id, ...props }: MarketLinkProps) => {
|
||||
label = (
|
||||
<div title={t('Unknown market')}>
|
||||
<span role="img" aria-label="Unknown market" className="img">
|
||||
⚠️
|
||||
⚠️ {t('Invalid market')}
|
||||
</span>
|
||||
{id}
|
||||
</div>
|
||||
|
@ -22,8 +22,14 @@ fragment ExplorerDeterministicOrderFields on Order {
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
product {
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import * 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, updatedAt?: any | null, 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 ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, 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, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
|
||||
|
||||
export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
|
||||
orderId: Types.Scalars['ID'];
|
||||
@ -11,7 +11,7 @@ export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, 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 ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, 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, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
|
||||
|
||||
export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
|
||||
fragment ExplorerDeterministicOrderFields on Order {
|
||||
@ -38,8 +38,14 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
product {
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import type { components } from '../../../../types/explorer';
|
||||
|
||||
export interface DeterministicOrderDetailsProps {
|
||||
id: string;
|
||||
@ -19,7 +20,8 @@ export const statusText: Record<Schema.OrderStatus, string> = {
|
||||
STATUS_STOPPED: t('Stopped'),
|
||||
};
|
||||
|
||||
export const sideText: Record<Schema.Side, string> = {
|
||||
export const sideText: Record<components['schemas']['vegaSide'], string> = {
|
||||
SIDE_UNSPECIFIED: t('Unspecified'),
|
||||
SIDE_BUY: t('Buy'),
|
||||
SIDE_SELL: t('Sell'),
|
||||
};
|
||||
|
@ -0,0 +1,80 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import OrderSummary from './order-summary';
|
||||
import type { OrderSummaryModifier } from './order-summary';
|
||||
import { render } from '@testing-library/react';
|
||||
import { ExplorerDeterministicOrderDocument } from '../order-details/__generated__/Order';
|
||||
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerDeterministicOrderDocument,
|
||||
variables: {
|
||||
orderId: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orderByID: {
|
||||
__typename: 'Order',
|
||||
id: '123',
|
||||
type: 'GTC',
|
||||
status: 'OPEN',
|
||||
version: '1',
|
||||
createdAt: 'Tue, Jan 10, 2023 3:35',
|
||||
expiresAt: 'Tue, Jan 10, 2023 3:35',
|
||||
updatedAt: 'Tue, Jan 10, 2023 3:35',
|
||||
rejectionReason: null,
|
||||
reference: null,
|
||||
timeInForce: 'GTC',
|
||||
price: '333',
|
||||
side: 'SIDE_BUY',
|
||||
remaining: '100',
|
||||
size: '100',
|
||||
party: {
|
||||
id: '456',
|
||||
},
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '789',
|
||||
decimalPlaces: 2,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'TEST',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
function renderComponent(
|
||||
id: string,
|
||||
mocks?: MockedResponse[],
|
||||
modifier?: OrderSummaryModifier
|
||||
) {
|
||||
return render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<OrderSummary id={id} modifier={modifier} />
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Order Summary component', () => {
|
||||
it('side, size are present', async () => {
|
||||
const res = renderComponent(mock.result.data.orderByID.id, [mock]);
|
||||
expect(await res.findByText('Buy')).toBeInTheDocument();
|
||||
expect(await res.findByText('100')).toBeInTheDocument();
|
||||
// Note: Market is not mocked so the PriceInMarket component is not rendering in this test - hence no price formatting
|
||||
});
|
||||
|
||||
it('Cancelled modifier add strikethrough', async () => {
|
||||
const res = renderComponent(
|
||||
mock.result.data.orderByID.id,
|
||||
[mock],
|
||||
'cancelled'
|
||||
);
|
||||
const buy = await res.findByText('Buy');
|
||||
expect(buy.parentElement).toHaveClass('line-through');
|
||||
});
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
import { useExplorerDeterministicOrderQuery } from '../order-details/__generated__/Order';
|
||||
import PriceInMarket from '../price-in-market/price-in-market';
|
||||
import { sideText } from '../order-details/lib/order-labels';
|
||||
|
||||
// Note: Edited has no style currently
|
||||
export type OrderSummaryModifier = 'cancelled' | 'edited';
|
||||
|
||||
/**
|
||||
* Provides Tailwind classnames to apply to order
|
||||
* @param modifier
|
||||
* @returns string
|
||||
*/
|
||||
export function getClassName(modifier?: OrderSummaryModifier) {
|
||||
if (modifier === 'cancelled') {
|
||||
return 'line-through';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface OrderSummaryProps {
|
||||
id: string;
|
||||
modifier?: OrderSummaryModifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component renders the *current* details for an order, like OrderDetails but much
|
||||
* more compact. It is equivalent to OrderTxSummary, but operates on an order rather than
|
||||
* the transaction that creates an order
|
||||
*/
|
||||
const OrderSummary = ({ id, modifier }: OrderSummaryProps) => {
|
||||
const { data, error } = useExplorerDeterministicOrderQuery({
|
||||
variables: { orderId: id },
|
||||
});
|
||||
|
||||
if (error || !data || (data && !data.orderByID)) {
|
||||
return <div data-testid="order-summary">-</div>;
|
||||
}
|
||||
|
||||
const order = data.orderByID;
|
||||
return (
|
||||
<div data-testid="order-summary" className={getClassName(modifier)}>
|
||||
<span>{sideText[order.side]}</span>
|
||||
<span>{order.size}</span> <i>@</i>
|
||||
<PriceInMarket marketId={order.market.id} price={order.price} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderSummary;
|
@ -0,0 +1,119 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import type { components } from '../../../types/explorer';
|
||||
import type { UnknownObject } from '../nested-data-list';
|
||||
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import OrderTxSummary from './order-tx-summary';
|
||||
import { render } from '@testing-library/react';
|
||||
import { ExplorerMarketDocument } from '../links/market-link/__generated__/Market';
|
||||
|
||||
type Order = components['schemas']['v1OrderSubmission'];
|
||||
|
||||
function renderComponent(order: Order, mocks?: MockedResponse[]) {
|
||||
return render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<OrderTxSummary order={order} />
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
describe('Order TX Summary component', () => {
|
||||
it('Renders nothing if the order passed is somehow null', () => {
|
||||
const res = renderComponent({} as UnknownObject as Order);
|
||||
expect(res.queryByTestId('order-summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders nothing if the order passed lacks a side', () => {
|
||||
const o: Order = {
|
||||
marketId: '123',
|
||||
price: '100',
|
||||
type: 'TYPE_LIMIT',
|
||||
size: '10',
|
||||
};
|
||||
const res = renderComponent(o);
|
||||
expect(res.queryByTestId('order-summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders nothing if the order passed lacks a price', () => {
|
||||
const o: Order = {
|
||||
marketId: '123',
|
||||
side: 'SIDE_BUY',
|
||||
type: 'TYPE_LIMIT',
|
||||
size: '10',
|
||||
};
|
||||
const res = renderComponent(o);
|
||||
expect(res.queryByTestId('order-summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders nothing if the order has an unspecified side', () => {
|
||||
const o: Order = {
|
||||
marketId: '123',
|
||||
side: 'SIDE_UNSPECIFIED',
|
||||
type: 'TYPE_LIMIT',
|
||||
size: '10',
|
||||
price: '10',
|
||||
};
|
||||
|
||||
const res = renderComponent(o);
|
||||
expect(res.queryByTestId('order-summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders nothing if the order has an unspecified market', () => {
|
||||
const o: Order = {
|
||||
side: 'SIDE_BUY',
|
||||
type: 'TYPE_LIMIT',
|
||||
size: '10',
|
||||
price: '10',
|
||||
};
|
||||
|
||||
const res = renderComponent(o);
|
||||
expect(res.queryByTestId('order-summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('side, size and price in market if all details are present', async () => {
|
||||
const o: Order = {
|
||||
marketId: '123',
|
||||
side: 'SIDE_BUY',
|
||||
type: 'TYPE_LIMIT',
|
||||
size: '10',
|
||||
price: '333',
|
||||
};
|
||||
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerMarketDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
market: {
|
||||
id: '123',
|
||||
decimalPlaces: 2,
|
||||
state: 'irrelevant-test-data',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'TEST',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'TEST',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = renderComponent(o, [mock]);
|
||||
expect(res.queryByTestId('order-summary')).toBeInTheDocument();
|
||||
expect(res.getByText('Buy')).toBeInTheDocument();
|
||||
expect(res.getByText('10')).toBeInTheDocument();
|
||||
|
||||
// Initially renders price alone
|
||||
expect(res.getByText('333')).toBeInTheDocument();
|
||||
|
||||
// After fetch renders formatted price and asset quotename
|
||||
expect(await res.findByText('3.33')).toBeInTheDocument();
|
||||
expect(await res.findByText('TEST')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,41 @@
|
||||
import type { components } from '../../../types/explorer';
|
||||
|
||||
import PriceInMarket from '../price-in-market/price-in-market';
|
||||
import { sideText } from '../order-details/lib/order-labels';
|
||||
|
||||
export type OrderSummaryProps = {
|
||||
order: components['schemas']['v1OrderSubmission'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a brief, relatively plaintext summary of an order transaction
|
||||
* showing the price, correctly formatted. Created for the Batch Submission
|
||||
* list, should be kept compact.
|
||||
*
|
||||
* Market name is expected to be listed elsewhere, so is not included
|
||||
*/
|
||||
const OrderTxSummary = ({ order }: OrderSummaryProps) => {
|
||||
// Render nothing if the Order Submission doesn't look right
|
||||
if (
|
||||
!order ||
|
||||
!order.marketId ||
|
||||
!order.price ||
|
||||
!order.side ||
|
||||
order.side === 'SIDE_UNSPECIFIED'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="order-summary">
|
||||
<span>{sideText[order.side]}</span>
|
||||
<span>{order.size}</span> <i className="text-xs">@</i>
|
||||
<PriceInMarket
|
||||
marketId={order.marketId}
|
||||
price={order.price}
|
||||
></PriceInMarket>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderTxSummary;
|
@ -15,6 +15,7 @@ export type PriceInMarketProps = {
|
||||
const PriceInMarket = ({ marketId, price }: PriceInMarketProps) => {
|
||||
const { data } = useExplorerMarketQuery({
|
||||
variables: { id: marketId },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
let label = price;
|
||||
@ -37,9 +38,9 @@ const PriceInMarket = ({ marketId, price }: PriceInMarketProps) => {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="inline-block">
|
||||
<label>
|
||||
<span>{label}</span> <span>{suffix}</span>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -0,0 +1,29 @@
|
||||
import type { BatchInstruction } from '../../../../routes/types/block-explorer-response';
|
||||
import { TxOrderType } from '../../tx-order-type';
|
||||
import { MarketLink } from '../../../links';
|
||||
import OrderSummary from '../../../order-summary/order-summary';
|
||||
|
||||
interface BatchAmendProps {
|
||||
index: number;
|
||||
submission: BatchInstruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table row for a single amendment in a batch submission
|
||||
*/
|
||||
export const BatchAmend = ({ index, submission }: BatchAmendProps) => {
|
||||
return (
|
||||
<tr key={`amend-${index}`}>
|
||||
<td>{index}</td>
|
||||
<td>
|
||||
<TxOrderType orderType={'OrderAmendment'} />
|
||||
</td>
|
||||
<td>
|
||||
<OrderSummary id={submission.orderId} modifier="edited" />
|
||||
</td>
|
||||
<td>
|
||||
<MarketLink id={submission.marketId} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import type { BatchCancellationInstruction } from '../../../../routes/types/block-explorer-response';
|
||||
import { TxOrderType } from '../../tx-order-type';
|
||||
import { MarketLink } from '../../../links';
|
||||
import OrderSummary from '../../../order-summary/order-summary';
|
||||
|
||||
interface BatchCancelProps {
|
||||
index: number;
|
||||
submission: BatchCancellationInstruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table row for a single cancellation in a batch submission
|
||||
*/
|
||||
export const BatchCancel = ({ index, submission }: BatchCancelProps) => {
|
||||
return (
|
||||
<tr>
|
||||
<td>{index}</td>
|
||||
<td>
|
||||
<TxOrderType orderType={'OrderCancellation'} />
|
||||
</td>
|
||||
<td>
|
||||
<OrderSummary id={submission.orderId} modifier="cancelled" />
|
||||
</td>
|
||||
<td>
|
||||
<MarketLink id={submission.marketId} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import type { BatchInstruction } from '../../../../routes/types/block-explorer-response';
|
||||
import { TxOrderType } from '../../tx-order-type';
|
||||
import { MarketLink } from '../../../links';
|
||||
import OrderTxSummary from '../../../order-summary/order-tx-summary';
|
||||
|
||||
interface BatchOrderProps {
|
||||
index: number;
|
||||
submission: BatchInstruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table row for a single order in a batch submission
|
||||
*/
|
||||
export const BatchOrder = ({ index, submission }: BatchOrderProps) => {
|
||||
return (
|
||||
<tr key={`batch-${index}`}>
|
||||
<td>{index}</td>
|
||||
<td>
|
||||
<TxOrderType orderType={'OrderSubmission'} />
|
||||
</td>
|
||||
<td>
|
||||
<OrderTxSummary order={submission} />
|
||||
</td>
|
||||
<td>
|
||||
<MarketLink id={submission.marketId} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
@ -31,6 +31,10 @@ export const ChainResponseCode = ({
|
||||
const icon = isSuccess ? '✅' : '❌';
|
||||
const label = ErrorCodes.get(code) || 'Unknown response code';
|
||||
|
||||
// Hack for batches with many errors - see https://github.com/vegaprotocol/vega/issues/7245
|
||||
const displayError =
|
||||
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
|
||||
|
||||
return (
|
||||
<div title={`Response code: ${code} - ${label}`}>
|
||||
<span
|
||||
@ -41,8 +45,8 @@ export const ChainResponseCode = ({
|
||||
{icon}
|
||||
</span>
|
||||
{hideLabel ? null : <span>{label}</span>}
|
||||
{!hideLabel && !!error ? (
|
||||
<span className="ml-1">— {error}</span>
|
||||
{!hideLabel && !!displayError ? (
|
||||
<span className="ml-1 whitespace-pre">— {displayError}</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
@ -14,6 +14,12 @@ interface TxDetailsSharedProps {
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
// Applied to all header cells
|
||||
const sharedHeaderProps = {
|
||||
// Ensures that multi line contents still have the header aligned to the first line
|
||||
className: 'align-top',
|
||||
};
|
||||
|
||||
/**
|
||||
* These rows are shown for every transaction type, providing a consistent set of rows for the top
|
||||
* of a transaction details row. The order is relatively arbitrary but felt right - it might need to
|
||||
@ -34,27 +40,27 @@ export const TxDetailsShared = ({
|
||||
return (
|
||||
<>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Type')}</TableCell>
|
||||
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||
<TableCell>{txData.type}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Hash')}</TableCell>
|
||||
<TableCell {...sharedHeaderProps}>{t('Hash')}</TableCell>
|
||||
<TableCell>
|
||||
<code>{txData.hash}</code>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Submitter')}</TableCell>
|
||||
<TableCell {...sharedHeaderProps}>{t('Submitter')}</TableCell>
|
||||
<TableCell>{pubKey ? <PartyLink id={pubKey} /> : '-'}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Block')}</TableCell>
|
||||
<TableCell {...sharedHeaderProps}>{t('Block')}</TableCell>
|
||||
<TableCell>
|
||||
<BlockLink height={height} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Time')}</TableCell>
|
||||
<TableCell {...sharedHeaderProps}>{t('Time')}</TableCell>
|
||||
<TableCell>
|
||||
{time ? (
|
||||
<div>
|
||||
@ -71,7 +77,7 @@ export const TxDetailsShared = ({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Response code')}</TableCell>
|
||||
<TableCell {...sharedHeaderProps}>{t('Response code')}</TableCell>
|
||||
<TableCell>
|
||||
<ChainResponseCode code={txData.code} error={txData.error} />
|
||||
</TableCell>
|
||||
|
@ -1,8 +1,15 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type {
|
||||
BatchCancellationInstruction,
|
||||
BatchInstruction,
|
||||
BlockExplorerTransactionResult,
|
||||
} from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableWithTbody, TableRow, TableCell } from '../../table';
|
||||
import { TableWithTbody, TableRow, TableCell, Table } from '../../table';
|
||||
import { BatchCancel } from './batch-submission/batch-cancel';
|
||||
import { BatchAmend } from './batch-submission/batch-amend';
|
||||
import { BatchOrder } from './batch-submission/batch-order';
|
||||
|
||||
interface TxDetailsBatchProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@ -16,6 +23,11 @@ interface TxDetailsBatchProps {
|
||||
*
|
||||
* Design considerations for batch:
|
||||
* - So far it's very basic details about the size of the batch
|
||||
*
|
||||
* Batches are processed in the following order:
|
||||
* - Cancellations
|
||||
* - Amends
|
||||
* - Submissions
|
||||
*/
|
||||
export const TxDetailsBatch = ({
|
||||
txData,
|
||||
@ -26,47 +38,80 @@ export const TxDetailsBatch = ({
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const countSubmissions =
|
||||
txData.command.batchMarketInstructions.submissions?.length || 0;
|
||||
const countAmendments =
|
||||
txData.command.batchMarketInstructions.amendments?.length || 0;
|
||||
const countCancellations =
|
||||
txData.command.batchMarketInstructions.cancellations?.length || 0;
|
||||
const countTotal = countSubmissions + countAmendments + countCancellations;
|
||||
const submissions: BatchInstruction[] =
|
||||
txData.command.batchMarketInstructions.submissions;
|
||||
const countSubmissions = submissions?.length || 0;
|
||||
|
||||
const amendments: BatchInstruction[] =
|
||||
txData.command.batchMarketInstructions.amendments;
|
||||
const countAmendments = amendments?.length || 0;
|
||||
|
||||
const cancellations: BatchCancellationInstruction[] =
|
||||
txData.command.batchMarketInstructions.cancellations;
|
||||
const countCancellations = cancellations.length || 0;
|
||||
const countTotal = countSubmissions + countAmendments + countCancellations;
|
||||
let index = 0;
|
||||
return (
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Batch size')}</TableCell>
|
||||
<TableCell>
|
||||
<span>{countTotal}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>
|
||||
<span className="ml-5">{t('Submissions')}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span>{countSubmissions}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>
|
||||
<span className="ml-5">{t('Amendments')}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span>{countAmendments}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>
|
||||
<span className="ml-5">{t('Cancellations')}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span>{countCancellations}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableWithTbody>
|
||||
<div key={`tx-${index}`}>
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared
|
||||
txData={txData}
|
||||
pubKey={pubKey}
|
||||
blockData={blockData}
|
||||
/>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Batch size')}</TableCell>
|
||||
<TableCell>
|
||||
<span>{countTotal}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>
|
||||
<span className="ml-5">{t('Cancellations')}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span>{countCancellations}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>
|
||||
<span className="ml-5">{t('Amendments')}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span>{countAmendments}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>
|
||||
<span className="ml-5">{t('Submissions')}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span>{countSubmissions}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableWithTbody>
|
||||
|
||||
<Table className="max-w-5xl min-w-fit">
|
||||
<thead>
|
||||
<TableRow modifier="bordered" className="font-mono">
|
||||
<th align="left">{t('#')}</th>
|
||||
<th align="left">{t('Type')}</th>
|
||||
<th align="left">{t('Order')}</th>
|
||||
<th align="left">{t('Market')}</th>
|
||||
</TableRow>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cancellations.map((c) => (
|
||||
<BatchCancel key={`bc-${index}`} submission={c} index={index++} />
|
||||
))}
|
||||
{amendments.map((a) => (
|
||||
<BatchAmend key={`ba-${index}`} submission={a} index={index++} />
|
||||
))}
|
||||
{submissions.map((s) => (
|
||||
<BatchOrder key={`bo-${index}`} submission={s} index={index++} />
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -45,7 +45,7 @@ export const TxDetailsWrapper = ({
|
||||
const raw = get(blockData, `result.block.data.txs[${txData.index}]`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div key={`txd-${txData.hash}`}>
|
||||
<section>{child({ txData, pubKey, blockData })}</section>
|
||||
|
||||
<details title={t('Decoded transaction')} className="mt-3">
|
||||
@ -59,7 +59,7 @@ export const TxDetailsWrapper = ({
|
||||
<code className="break-all font-mono text-xs">{raw}</code>
|
||||
</details>
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -15,9 +15,15 @@ fragment ExplorerPartyAssetsAccounts on AccountBalance {
|
||||
balance
|
||||
market {
|
||||
id
|
||||
decimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
product {
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerPartyAssetsAccountsFragment = { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } | null };
|
||||
export type ExplorerPartyAssetsAccountsFragment = { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null };
|
||||
|
||||
export type ExplorerPartyAssetsQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } | null } } | null> | null } | null } }> } | null };
|
||||
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null };
|
||||
|
||||
export const ExplorerPartyAssetsAccountsFragmentDoc = gql`
|
||||
fragment ExplorerPartyAssetsAccounts on AccountBalance {
|
||||
@ -30,9 +30,15 @@ export const ExplorerPartyAssetsAccountsFragmentDoc = gql`
|
||||
balance
|
||||
market {
|
||||
id
|
||||
decimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
product {
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ export const PartyAccounts = ({ accounts }: PartyAccountsProps) => {
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={`pa-${account.asset.id}-${account.type}`}
|
||||
title={account.asset.name}
|
||||
id={`${accountTypeString[account.type]} ${m ? ` - ${m}` : ''}`}
|
||||
>
|
||||
|
@ -51,12 +51,12 @@ const Party = () => {
|
||||
const staking = (
|
||||
<section>
|
||||
{p?.stakingSummary?.currentStakeAvailable ? (
|
||||
<p className="mt-4 leading-3">
|
||||
<div className="mt-4 leading-3">
|
||||
<strong className="font-semibold">{t('Staking Balance: ')}</strong>
|
||||
<GovernanceAssetBalance
|
||||
price={p.stakingSummary.currentStakeAvailable}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
|
@ -15,7 +15,7 @@ export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
return (
|
||||
<section className="mb-10">
|
||||
<section className="mb-10" key={txData.hash}>
|
||||
<TxDetailsWrapper height={txData.block} txData={txData} pubKey={pubKey} />
|
||||
</section>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user