feat(explorer): order amend view (#2314)

* feat(explorer): add amend order view
This commit is contained in:
Edd 2022-12-07 16:00:09 +00:00 committed by GitHub
parent 0a11831ab0
commit 1bb3233fb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 428 additions and 49 deletions

View File

@ -5,6 +5,7 @@ fragment ExplorerDeterministicOrderFields on Order {
status
version
createdAt
updatedAt
expiresAt
timeInForce
price
@ -26,8 +27,8 @@ fragment ExplorerDeterministicOrderFields on Order {
}
}
query ExplorerDeterministicOrder($orderId: ID!) {
orderByID(id: $orderId) {
query ExplorerDeterministicOrder($orderId: ID!, $version: Int) {
orderByID(id: $orderId, version: $version) {
...ExplorerDeterministicOrderFields
}
}

View File

@ -3,14 +3,15 @@ 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 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 ExplorerDeterministicOrderQueryVariables = Types.Exact<{
orderId: Types.Scalars['ID'];
version?: Types.InputMaybe<Types.Scalars['Int']>;
}>;
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 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 const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
fragment ExplorerDeterministicOrderFields on Order {
@ -20,6 +21,7 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
status
version
createdAt
updatedAt
expiresAt
timeInForce
price
@ -42,8 +44,8 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
}
`;
export const ExplorerDeterministicOrderDocument = gql`
query ExplorerDeterministicOrder($orderId: ID!) {
orderByID(id: $orderId) {
query ExplorerDeterministicOrder($orderId: ID!, $version: Int) {
orderByID(id: $orderId, version: $version) {
...ExplorerDeterministicOrderFields
}
}
@ -62,6 +64,7 @@ export const ExplorerDeterministicOrderDocument = gql`
* const { data, loading, error } = useExplorerDeterministicOrderQuery({
* variables: {
* orderId: // value for 'orderId'
* version: // value for 'version'
* },
* });
*/

View File

@ -0,0 +1,153 @@
import type { components } from '../../../types/explorer';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import AmendOrderDetails from './amend-order-details';
import { ExplorerDeterministicOrderDocument } from './__generated__/Order';
import { render } from '@testing-library/react';
import { Schema } from '@vegaprotocol/types';
import { ExplorerMarketDocument } from '../links/market-link/__generated__/Market';
type Amend = components['schemas']['v1OrderAmendment'];
function renderAmendOrderDetails(
id: string,
version: number,
amend: Amend,
mocks: MockedResponse[]
) {
return render(
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<AmendOrderDetails version={version} id={id} amend={amend} />
</MemoryRouter>
</MockedProvider>
);
}
function renderExistingAmend(id: string, version: number, amend: Amend) {
const mocks = [
{
request: {
query: ExplorerDeterministicOrderDocument,
variables: {
orderId: '123',
version: 1,
},
},
result: {
data: {
orderByID: {
__typename: 'Order',
id: '123',
type: 'GTT',
status: Schema.OrderStatus.STATUS_ACTIVE,
version: version,
createdAt: '123',
updatedAt: '456',
expiresAt: '789',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
price: '200',
side: 'BUY',
remaining: '99',
rejectionReason: 'rejection',
reference: '123',
size: '100',
party: {
__typename: 'Party',
id: '234',
},
market: {
__typename: 'Market',
id: '789',
decimalPlaces: '5',
tradableInstrument: {
instrument: {
name: 'test',
},
},
},
},
},
},
},
{
request: {
query: ExplorerMarketDocument,
variables: {
id: '789',
},
},
result: {
data: {
market: {
id: '789',
decimalPlaces: 5,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
name: 'test-label',
product: {
quoteName: 'dai',
},
},
},
},
},
},
},
];
return renderAmendOrderDetails(id, version, amend, mocks);
}
describe('Amend order details', () => {
it('Renders price if price changed', async () => {
const amend: Amend = {
sizeDelta: '123',
};
const res = renderExistingAmend('123', 1, amend);
expect(await res.findByText('New size')).toBeInTheDocument();
});
it('Renders Reference if provided', async () => {
const amend: Amend = {
peggedReference: 'PEGGED_REFERENCE_MID',
};
const res = renderExistingAmend('123', 1, amend);
expect(await res.findByText('New reference')).toBeInTheDocument();
expect(await res.findByText('Mid')).toBeInTheDocument();
});
it('Renders offset if provided: positive', async () => {
const amend: Amend = {
peggedOffset: '1',
};
const res = renderExistingAmend('123', 1, amend);
expect(await res.findByText('New offset')).toBeInTheDocument();
expect(await res.findByText('1')).toBeInTheDocument();
});
it('Renders positive price if provided', async () => {
const amend: Amend = {
price: '7879',
};
const res = renderExistingAmend('123', 1, amend);
expect(await res.findByText('New price')).toBeInTheDocument();
expect(await res.findByText('7879')).toBeInTheDocument();
});
it('Renders negative price if provided', async () => {
const amend: Amend = {
price: '-7879',
};
const res = renderExistingAmend('123', 1, amend);
expect(await res.findByText('New price')).toBeInTheDocument();
expect(await res.findByText('-7879')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,139 @@
import { t } from '@vegaprotocol/react-helpers';
import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
import { MarketLink } from '../links';
import PriceInMarket from '../price-in-market/price-in-market';
import { Time } from '../time';
import { sideText, peggedReference } from './lib/order-labels';
import type { components } from '../../../types/explorer';
import { VegaColours } from '@vegaprotocol/tailwindcss-config';
import { wrapperClasses } from './deterministic-order-details';
export interface AmendOrderDetailsProps {
id: string;
amend: components['schemas']['v1OrderAmendment'];
// Version to fetch, with 0 being 'latest' and 1 being 'first'. Defaults to 0
version?: number;
}
export function getSideDeltaColour(delta: string): string {
if (delta.charAt(0) === '-') {
return VegaColours.pink.DEFAULT;
} else {
return VegaColours.green.DEFAULT;
}
}
/**
* This component renders the changes to an order made in an amend. It's very
* similar to the deterministic-order-details component, and should probably
* eventually be merged in to that view. However the APIs to make that experience
* work nicely are not available, so instead of making 1 complex component, it's
* currently 2 similar components.
*
* @param param0
* @returns
*/
const AmendOrderDetails = ({
id,
version = 0,
amend,
}: AmendOrderDetailsProps) => {
const { data, error } = useExplorerDeterministicOrderQuery({
variables: { orderId: id, version },
});
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">
{t('Edits to ')}
{sideText[o.side]}
{t(' order')}
</h2>
<p className="text-gray-500 mb-4">
In <MarketLink id={o.market.id} />, updated at{' '}
<Time date={o.updatedAt} />.
</p>
<div className="grid md:grid-cols-4 gap-x-6">
{amend.sizeDelta && amend.sizeDelta !== '0' ? (
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('New size')}
</h2>
<h5
className={`text-lg font-medium text-gray-500 mb-0 capitalize ${getSideDeltaColour(
amend.sizeDelta
)}`}
>
{amend.sizeDelta}
</h5>
</div>
) : null}
{amend.price && amend.price !== '0' ? (
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('New price')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
<PriceInMarket price={amend.price} marketId={o.market.id} />
</h5>
</div>
) : null}
{amend.peggedReference &&
amend.peggedReference !== 'PEGGED_REFERENCE_UNSPECIFIED' ? (
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('New reference')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
{peggedReference[amend.peggedReference]}
</h5>
</div>
) : null}
{amend.peggedOffset ? (
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('New offset')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
{amend.peggedOffset}
</h5>
</div>
) : null}
</div>
</div>
</div>
</div>
);
};
export default AmendOrderDetails;

View File

@ -1,9 +1,9 @@
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';
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
export interface DeterministicOrderDetailsProps {
id: string;
@ -11,41 +11,7 @@ export interface DeterministicOrderDetailsProps {
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 =
export 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';
/**
@ -64,7 +30,7 @@ const DeterministicOrderDetails = ({
version = 0,
}: DeterministicOrderDetailsProps) => {
const { data, error } = useExplorerDeterministicOrderQuery({
variables: { orderId: id },
variables: { orderId: id, version },
});
if (error || (data && !data.orderByID)) {

View File

@ -0,0 +1,49 @@
import { t } from '@vegaprotocol/react-helpers';
import type { Schema } from '@vegaprotocol/types';
export interface DeterministicOrderDetailsProps {
id: string;
// Version to fetch, with 0 being 'latest' and 1 being 'first'. Defaults to 0
version?: number;
}
export 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'),
};
export const sideText: Record<Schema.Side, string> = {
SIDE_BUY: t('Buy'),
SIDE_SELL: t('Sell'),
};
export 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'),
};
export 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'),
};
export const peggedReference: Record<Schema.PeggedReference, string> = {
PEGGED_REFERENCE_BEST_ASK: 'Best Ask',
PEGGED_REFERENCE_BEST_BID: 'Best Bid',
PEGGED_REFERENCE_MID: 'Mid',
};

View File

@ -13,6 +13,7 @@ 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';
import { TxDetailsOrderAmend } from './tx-order-amend';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@ -75,6 +76,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsOrder;
case 'Cancel Order':
return TxDetailsOrderCancel;
case 'Amend Order':
return TxDetailsOrderAmend;
case 'Validator Heartbeat':
return TxDetailsHeartbeat;
case 'Amend LiquidityProvision Order':

View File

@ -0,0 +1,57 @@
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 AmendOrderDetails from '../../order-details/amend-order-details';
interface TxDetailsOrderAmendProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Someone cancelled an order
*/
export const TxDetailsOrderAmend = ({
txData,
pubKey,
blockData,
}: TxDetailsOrderAmendProps) => {
if (!txData || !txData.command.orderAmendment) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const marketId: string = txData.command.orderAmendment.marketId || '-';
const orderId: string = txData.command.orderAmendment.orderId || '-';
return (
<>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Order')}</TableCell>
<TableCell>
<code>{orderId}</code>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
</TableCell>
</TableRow>
</TableWithTbody>
{orderId !== '-' ? (
<AmendOrderDetails id={orderId} amend={txData.command.orderAmendment} />
) : null}
</>
);
};

View File

@ -4,7 +4,7 @@ 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';
import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
interface TxDetailsOrderCancelProps {
txData: BlockExplorerTransactionResult | undefined;
@ -35,6 +35,12 @@ export const TxDetailsOrderCancel = ({
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Order')}</TableCell>
<TableCell>
<code>{orderId}</code>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>

View File

@ -5,7 +5,7 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
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';
import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
interface TxDetailsOrderProps {
txData: BlockExplorerTransactionResult | undefined;
@ -44,6 +44,12 @@ export const TxDetailsOrder = ({
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Order')}</TableCell>
<TableCell>
<code>{deterministicId}</code>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>

View File

@ -15,7 +15,6 @@ import { PageHeader } from '../../../components/page-header';
import { useExplorerPartyAssetsQuery } from './__generated__/party-assets';
import type { Schema } from '@vegaprotocol/types';
import get from 'lodash/get';
import PartyIdError from './error/party-id-error';
const accountTypeString: Record<Schema.AccountType, string> = {
ACCOUNT_TYPE_BOND: t('Bond'),
@ -134,9 +133,6 @@ const Party = () => {
>
{t('Party')}
</h1>
{partyRes.error ? (
<PartyIdError id={partyId} error={partyRes.error} />
) : null}
{partyRes.data ? (
<>
{header}