Fix/Order dialog state (#850)

* feat: remove dialog state handling from dialog and split out edit dialog

* feat: add complete state to use-vega-transaction, fix cancel dialog

* feat: add custom dialog content for order submission

* feat: handle custom title and custom intent

* feat: separate components, make order dialog wrapper more generic

* feat: remove dialog wrapper and add icon to dialog

* chore: remove other dialog wrappers and use icon and title props on main dialog

* chore: adjust default color of dialog text

* fix: tests for tx dialog and vega tx hook

* fix: order edit and cancel hook tests

* chore: add edit dialog to stories

* fix: e2e test for deal ticket

* feat: return dialog from hook

* refactor: add use-order-event hook to dedupe bus event logic

* refactor: add custom title and intent to order submit dialog

* chore: remove console logs

* fix: type error due to component being named idalog

* chore: add helper function for converting nanoseconds

* chore: remove capitalization text transform to dialog titles

* chore: remove unused import

* feat: handle titles and intents for cancel and edit

* chore: remove unused var
This commit is contained in:
Matthew Russell 2022-07-26 14:35:30 +01:00 committed by GitHub
parent 19a79afcc9
commit a5f9ed90e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1021 additions and 1137 deletions

View File

@ -28,13 +28,6 @@ const mockTx = {
};
describe('deal ticket orders', () => {
const orderSizeField = 'order-size';
const orderPriceField = 'order-price';
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
const orderStatusHeader = 'order-status-header';
const orderTransactionHash = 'tx-block-explorer';
before(() => {
cy.mockGQL((req) => {
mockTradingPage(req, MarketState.Active);
@ -107,7 +100,15 @@ describe('deal ticket orders', () => {
});
const testOrder = (order: Order, expected?: Partial<Order>) => {
const orderSizeField = 'order-size';
const orderPriceField = 'order-price';
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
const dialogTitle = 'dialog-title';
const orderTransactionHash = 'tx-block-explorer';
const { type, side, size, price, timeInForce, expiresAt } = order;
cy.get(`[name="order-type"][value="${type}"`).click({ force: true }); // force as input is hidden and displayed as a button
cy.get(`[name="order-side"][value="${side}"`).click({ force: true });
cy.getByTestId(orderSizeField).clear().type(size);
@ -139,7 +140,7 @@ describe('deal ticket orders', () => {
...expectedOrder,
},
});
cy.getByTestId(orderStatusHeader).should(
cy.getByTestId(dialogTitle).should(
'have.text',
'Awaiting network confirmation'
);

View File

@ -368,14 +368,14 @@ export interface MarketInfoQuery_market {
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )

View File

@ -1,10 +1,11 @@
import type { ReactNode } from 'react';
import { useState } from 'react';
import { VegaTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import { DealTicket } from './deal-ticket';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import { useOrderSubmit } from '@vegaprotocol/orders';
import { useOrderSubmit, OrderFeedback } from '@vegaprotocol/orders';
import { OrderStatus } from '@vegaprotocol/types';
import { Icon, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
export interface DealTicketManagerProps {
market: DealTicketQuery_market;
@ -15,28 +16,15 @@ export const DealTicketManager = ({
market,
children,
}: DealTicketManagerProps) => {
const [orderDialogOpen, setOrderDialogOpen] = useState(false);
const { submit, transaction, finalizedOrder, reset } = useOrderSubmit(market);
const getDialogTitle = (status?: string) => {
switch (status) {
case OrderStatus.Active:
return 'Order submitted';
case OrderStatus.Filled:
return 'Order filled';
case OrderStatus.PartiallyFilled:
return 'Order partially filled';
case OrderStatus.Parked:
return 'Order parked';
default:
return 'Submission failed';
}
};
const { submit, transaction, finalizedOrder, TransactionDialog } =
useOrderSubmit(market);
return (
<>
{children || (
<DealTicket
market={market}
submit={submit}
submit={(order) => submit(order)}
transactionStatus={
transaction.status === VegaTxStatus.Requested ||
transaction.status === VegaTxStatus.Pending
@ -45,15 +33,68 @@ export const DealTicketManager = ({
}
/>
)}
<VegaTransactionDialog
key={`submit-order-dialog-${transaction.txHash}`}
orderDialogOpen={orderDialogOpen}
setOrderDialogOpen={setOrderDialogOpen}
finalizedOrder={finalizedOrder}
transaction={transaction}
reset={reset}
<TransactionDialog
title={getDialogTitle(finalizedOrder?.status)}
/>
intent={getDialogIntent(finalizedOrder?.status)}
icon={getDialogIcon(finalizedOrder?.status)}
>
<OrderFeedback transaction={transaction} order={finalizedOrder} />
</TransactionDialog>
</>
);
};
const getDialogTitle = (status?: OrderStatus): string | undefined => {
if (!status) {
return;
}
switch (status) {
case OrderStatus.Active:
return t('Order submitted');
case OrderStatus.Filled:
return t('Order filled');
case OrderStatus.PartiallyFilled:
return t('Order partially filled');
case OrderStatus.Parked:
return t('Order parked');
default:
return t('Submission failed');
}
};
const getDialogIntent = (status?: OrderStatus): Intent | undefined => {
if (!status) {
return;
}
switch (status) {
case OrderStatus.Parked:
case OrderStatus.Expired:
return Intent.Warning;
case OrderStatus.Rejected:
case OrderStatus.Stopped:
case OrderStatus.Cancelled:
return Intent.Danger;
default:
return;
}
};
const getDialogIcon = (status?: OrderStatus): ReactNode | undefined => {
if (!status) {
return;
}
switch (status) {
case OrderStatus.Parked:
case OrderStatus.Expired:
return <Icon name="warning-sign" size={20} />;
case OrderStatus.Rejected:
case OrderStatus.Stopped:
case OrderStatus.Cancelled:
return <Icon name="error" size={20} />;
default:
return;
}
};

View File

@ -1,4 +1,6 @@
export * from './order-data-provider';
export * from './order-feedback';
export * from './order-list';
export * from './order-list-manager';
export * from './order-list-container';
export * from './mocks/generate-orders';

View File

@ -6,42 +6,42 @@ import {
Side,
} from '@vegaprotocol/types';
import type { Orders_party_ordersConnection_edges_node } from '../';
import type { PartialDeep } from 'type-fest';
export const generateOrder = (
partialOrder: Partial<Orders_party_ordersConnection_edges_node>
) =>
merge(
{
__typename: 'Order',
id: 'order-id2',
market: {
__typename: 'Market',
id: 'market-id',
name: 'market-name',
decimalPlaces: 2,
positionDecimalPlaces: 2,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'instrument-code',
},
partialOrder?: PartialDeep<Orders_party_ordersConnection_edges_node>
) => {
const order: Orders_party_ordersConnection_edges_node = {
__typename: 'Order',
id: 'order-id2',
market: {
__typename: 'Market',
id: 'market-id',
name: 'market-name',
decimalPlaces: 2,
positionDecimalPlaces: 2,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'instrument-code',
},
},
size: '10',
type: OrderType.Market,
status: OrderStatus.Active,
side: Side.Buy,
remaining: '5',
price: '',
timeInForce: OrderTimeInForce.IOC,
createdAt: new Date().toISOString(),
updatedAt: null,
expiresAt: null,
rejectionReason: null,
} as Orders_party_ordersConnection_edges_node,
partialOrder
);
},
size: '10',
type: OrderType.Market,
status: OrderStatus.Active,
side: Side.Buy,
remaining: '5',
price: '',
timeInForce: OrderTimeInForce.IOC,
createdAt: new Date().toISOString(),
updatedAt: null,
expiresAt: null,
rejectionReason: null,
};
return merge(order, partialOrder);
};
export const limitOrder = generateOrder({
id: 'limit-order',

View File

@ -0,0 +1 @@
export * from './order-feedback';

View File

@ -0,0 +1,87 @@
import { render, screen } from '@testing-library/react';
import { formatLabel } from '@vegaprotocol/react-helpers';
import {
OrderRejectionReason,
OrderStatus,
OrderType,
Side,
} from '@vegaprotocol/types';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import { generateOrder } from '../mocks/generate-orders';
import type { OrderFeedbackProps } from './order-feedback';
import { OrderFeedback } from './order-feedback';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_EXPLORER_URL: 'https://test.explorer.vega.network',
}),
}));
describe('OrderFeedback', () => {
let props: OrderFeedbackProps;
beforeEach(() => {
props = {
transaction: {
status: VegaTxStatus.Complete,
error: null,
txHash: 'tx-hash',
signature: null,
},
order: null,
};
});
it('renders null if no order provided', () => {
const { container } = render(<OrderFeedback {...props} />);
expect(container).toBeEmptyDOMElement();
});
it('renders error reason', () => {
const orderFields = {
status: OrderStatus.Rejected,
rejectionReason: OrderRejectionReason.OrderAmendFailure,
};
const order = generateOrder(orderFields);
render(<OrderFeedback {...props} order={order} />);
expect(screen.getByTestId('error-reason')).toHaveTextContent(
`Reason: ${formatLabel(orderFields.rejectionReason)}`
);
});
it('should render order details when order is placed successfully', () => {
const order = generateOrder({
type: OrderType.Limit,
price: '100',
size: '200',
side: Side.Buy,
market: {
decimalPlaces: 2,
positionDecimalPlaces: 0,
},
});
render(<OrderFeedback {...props} order={order} />);
expect(screen.getByTestId('order-confirmed')).toBeInTheDocument();
expect(screen.getByTestId('tx-block-explorer')).toHaveTextContent(
// eslint-disable-next-line
props.transaction.txHash!
);
expect(screen.getByTestId('tx-block-explorer')).toHaveTextContent(
// eslint-disable-next-line
props.transaction.txHash!
);
expect(screen.getByText('Market').nextElementSibling).toHaveTextContent(
// eslint-disable-next-line
order.market!.name
);
expect(screen.getByText('Status').nextElementSibling).toHaveTextContent(
order.status
);
expect(screen.getByText('Price').nextElementSibling).toHaveTextContent(
'1.00'
);
expect(screen.getByText('Amount').nextElementSibling).toHaveTextContent(
`+ 200`
);
});
});

View File

@ -0,0 +1,116 @@
import { useEnvironment } from '@vegaprotocol/environment';
import type { OrderEvent_busEvents_event_Order } from '../../order-hooks/__generated__';
import {
addDecimalsFormatNumber,
formatLabel,
t,
} from '@vegaprotocol/react-helpers';
import { OrderStatus, OrderType, Side } from '@vegaprotocol/types';
import type { VegaTxState } from '@vegaprotocol/wallet';
export interface OrderFeedbackProps {
transaction: VegaTxState;
order: OrderEvent_busEvents_event_Order | null;
}
export const OrderFeedback = ({ transaction, order }: OrderFeedbackProps) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
const labelClass = 'font-bold';
if (!order) return null;
// Order on network but was rejected
if (order.status === OrderStatus.Rejected) {
return (
<p data-testid="error-reason">
{order.rejectionReason &&
t(`Reason: ${formatLabel(order.rejectionReason)}`)}
</p>
);
}
if (order.status === OrderStatus.Cancelled) {
return (
<div data-testid="order-confirmed">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
{order.market && (
<div>
<p className={labelClass}>{t(`Market`)}</p>
<p>{t(`${order.market.name}`)}</p>
</div>
)}
</div>
<div>
{transaction.txHash && (
<div>
<p className={labelClass}>{t('Transaction')}</p>
<a
className="underline break-words"
data-testid="tx-block-explorer"
href={`${VEGA_EXPLORER_URL}/txs/0x${transaction.txHash}`}
target="_blank"
rel="noreferrer"
>
{transaction.txHash}
</a>
</div>
)}
</div>
</div>
);
}
return (
<div data-testid="order-confirmed">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
{order.market && (
<div>
<p className={labelClass}>{t(`Market`)}</p>
<p>{t(`${order.market.name}`)}</p>
</div>
)}
<div>
<p className={labelClass}>{t(`Status`)}</p>
<p>{t(`${order.status}`)}</p>
</div>
{order.type === OrderType.Limit && order.market && (
<div>
<p className={labelClass}>{t(`Price`)}</p>
<p>
{addDecimalsFormatNumber(order.price, order.market.decimalPlaces)}
</p>
</div>
)}
<div>
<p className={labelClass}>{t(`Amount`)}</p>
<p
className={
order.side === Side.Buy ? 'text-vega-green' : 'text-vega-red'
}
>
{`${order.side === Side.Buy ? '+' : '-'} ${addDecimalsFormatNumber(
order.size,
order.market?.positionDecimalPlaces ?? 0
)}
`}
</p>
</div>
</div>
<div>
{transaction.txHash && (
<div>
<p className={labelClass}>{t('Transaction')}</p>
<a
className="underline break-words"
data-testid="tx-block-explorer"
href={`${VEGA_EXPLORER_URL}/txs/0x${transaction.txHash}`}
target="_blank"
rel="noreferrer"
>
{transaction.txHash}
</a>
</div>
)}
</div>
</div>
);
};

View File

@ -4,16 +4,22 @@ import {
addDecimalsFormatNumber,
} from '@vegaprotocol/react-helpers';
import { OrderType } from '@vegaprotocol/types';
import { FormGroup, Input, InputError, Button } from '@vegaprotocol/ui-toolkit';
import {
FormGroup,
Input,
InputError,
Button,
Dialog,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form';
import Icon from 'react-syntax-highlighter';
import { OrderDialogWrapper } from '@vegaprotocol/wallet';
import type { Order } from '@vegaprotocol/wallet';
import type { OrderFields } from '../order-data-provider';
interface OrderEditDialogProps {
title: string;
order: Order | null;
edit: (body: Order) => Promise<unknown>;
isOpen: boolean;
onChange: (isOpen: boolean) => void;
order: OrderFields | null;
onSubmit: (fields: FormFields) => void;
}
interface FormFields {
@ -21,9 +27,10 @@ interface FormFields {
}
export const OrderEditDialog = ({
isOpen,
onChange,
order,
title,
edit,
onSubmit,
}: OrderEditDialogProps) => {
const headerClassName = 'text-h5 font-bold text-black dark:text-white';
const {
@ -37,9 +44,16 @@ export const OrderEditDialog = ({
: '',
},
});
if (!order) return null;
return (
<OrderDialogWrapper title={title} icon={<Icon name="hand-up" size={20} />}>
<Dialog
open={isOpen}
onChange={onChange}
title={t('Edit order')}
icon={<Icon name="edit" />}
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{order.market && (
<div>
@ -49,7 +63,7 @@ export const OrderEditDialog = ({
)}
{order.type === OrderType.Limit && order.market && (
<div>
<p className={headerClassName}>{t(`Last price`)}</p>
<p className={headerClassName}>{t(`Current price`)}</p>
<p>
{addDecimalsFormatNumber(order.price, order.market.decimalPlaces)}
</p>
@ -71,15 +85,7 @@ export const OrderEditDialog = ({
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 py-12">
<form
onSubmit={handleSubmit(async (data) => {
await edit({
...order,
price: data.entryPrice,
});
})}
data-testid="edit-order"
>
<form onSubmit={handleSubmit(onSubmit)} data-testid="edit-order">
<FormGroup label={t('Entry price')} labelFor="entryPrice">
<Input
{...register('entryPrice', { required: t('Required') })}
@ -97,6 +103,6 @@ export const OrderEditDialog = ({
</Button>
</form>
</div>
</OrderDialogWrapper>
</Dialog>
);
};

View File

@ -1,10 +1,11 @@
import type { Story, Meta } from '@storybook/react';
import { OrderType, OrderStatus, OrderTimeInForce } from '@vegaprotocol/types';
import { OrderList, OrderListTable } from './order-list';
import { useState } from 'react';
import type { Order, VegaTxState } from '@vegaprotocol/wallet';
import type { VegaTxState } from '@vegaprotocol/wallet';
import { VegaTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet';
import { generateOrdersArray } from '../mocks';
import { OrderEditDialog } from './order-edit-dialog';
import type { OrderFields } from '../order-data-provider';
export default {
component: OrderList,
@ -18,9 +19,6 @@ const Template: Story = (args) => {
<OrderListTable
rowData={args.data}
cancel={cancel}
setEditOrderDialogOpen={() => {
return;
}}
setEditOrder={() => {
return;
}}
@ -31,47 +29,43 @@ const Template: Story = (args) => {
const Template2: Story = (args) => {
const [open, setOpen] = useState(false);
const [editOrder, setEditOrder] = useState<OrderFields | null>(null);
const cancel = () => {
setOpen(!open);
return Promise.resolve();
};
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
status: VegaTxStatus.Requested,
error: null,
txHash: null,
signature: null,
dialogOpen: false,
};
const finalizedOrder: Order = {
status: OrderStatus.Cancelled,
rejectionReason: null,
size: '10',
price: '1000',
market: { name: 'ETH/DAI (30 Jun 2022)', decimalPlaces: 5 },
type: OrderType.Limit,
timeInForce: OrderTimeInForce.GTC,
};
const reset = () => null;
return (
<>
<div style={{ height: 1000 }}>
<OrderListTable
rowData={args.data}
cancel={cancel}
setEditOrderDialogOpen={() => {
return;
}}
setEditOrder={() => {
return;
setEditOrder={(order) => {
setEditOrder(order);
}}
/>
</div>
<VegaTransactionDialog
orderDialogOpen={open}
setOrderDialogOpen={setOpen}
finalizedOrder={finalizedOrder}
isOpen={open}
onChange={setOpen}
transaction={transaction}
reset={reset}
title={'Order cancelled'}
/>
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(null);
}}
order={editOrder}
onSubmit={(fields) => {
return;
}}
/>
</>
);

View File

@ -5,7 +5,11 @@ import {
getDateTimeFormat,
t,
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid, Button } from '@vegaprotocol/ui-toolkit';
import {
AgGridDynamic as AgGrid,
Button,
Intent,
} from '@vegaprotocol/ui-toolkit';
import type {
ICellRendererParams,
ValueFormatterParams,
@ -21,83 +25,61 @@ import type { Orders_party_ordersConnection_edges_node } from '../';
import BigNumber from 'bignumber.js';
import { useOrderCancel } from '../../order-hooks/use-order-cancel';
import { VegaTransactionDialog } from '@vegaprotocol/wallet';
import { useOrderEdit } from '../../order-hooks/use-order-edit';
import { OrderEditDialog } from './order-edit-dialog';
import type { OrderFields } from '../order-data-provider/__generated__';
import { OrderFeedback } from '../order-feedback';
type OrderListProps = AgGridReactProps | AgReactUiProps;
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
(props, ref) => {
const [cancelOrderDialogOpen, setCancelOrderDialogOpen] = useState(false);
const [editOrderDialogOpen, setEditOrderDialogOpen] = useState(false);
const [editOrder, setEditOrder] =
useState<Orders_party_ordersConnection_edges_node | null>(null);
const [editOrder, setEditOrder] = useState<OrderFields | null>(null);
const orderCancel = useOrderCancel();
const orderEdit = useOrderEdit(editOrder);
const { transaction, updatedOrder, reset, cancel } = useOrderCancel();
const {
transaction: editTransaction,
updatedOrder: editedOrder,
reset: resetEdit,
edit,
} = useOrderEdit();
const getCancelDialogTitle = (status?: string) => {
switch (status) {
case OrderStatus.Cancelled:
return 'Order cancelled';
case OrderStatus.Rejected:
return 'Order rejected';
case OrderStatus.Expired:
return 'Order expired';
default:
return 'Cancellation failed';
}
};
const getEditDialogTitle = () =>
editedOrder
? t(
`Order ${
editOrder?.market?.tradableInstrument.instrument.code ?? ''
} updated`
)
: t(
`Edit ${
editOrder?.market?.tradableInstrument.instrument.code ?? ''
} order`
);
return (
<>
<OrderListTable
{...props}
cancel={cancel}
cancel={(order) => {
if (!order.market) return;
orderCancel.cancel({
orderId: order.id,
marketId: order.market.id,
});
}}
ref={ref}
setEditOrderDialogOpen={setEditOrderDialogOpen}
setEditOrder={setEditOrder}
/>
<VegaTransactionDialog
key={`cancel-order-dialog-${transaction.txHash}`}
orderDialogOpen={cancelOrderDialogOpen}
setOrderDialogOpen={setCancelOrderDialogOpen}
transaction={transaction}
reset={reset}
title={getCancelDialogTitle(updatedOrder?.status)}
finalizedOrder={updatedOrder}
/>
<VegaTransactionDialog
key={`edit-order-dialog-${transaction.txHash}`}
orderDialogOpen={editOrderDialogOpen}
setOrderDialogOpen={setEditOrderDialogOpen}
transaction={editTransaction}
reset={resetEdit}
title={getEditDialogTitle()}
finalizedOrder={editedOrder}
<orderCancel.TransactionDialog
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
>
<OrderEditDialog
title={getEditDialogTitle()}
order={editOrder}
edit={edit}
<OrderFeedback
transaction={orderCancel.transaction}
order={orderCancel.cancelledOrder}
/>
</VegaTransactionDialog>
</orderCancel.TransactionDialog>
<orderEdit.TransactionDialog
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
>
<OrderFeedback
transaction={orderEdit.transaction}
order={orderEdit.updatedOrder}
/>
</orderEdit.TransactionDialog>
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(null);
}}
order={editOrder}
onSubmit={(fields) => {
setEditOrder(null);
orderEdit.edit({ price: fields.entryPrice });
}}
/>
</>
);
}
@ -111,15 +93,12 @@ type OrderListTableValueFormatterParams = Omit<
};
type OrderListTableProps = (AgGridReactProps | AgReactUiProps) & {
cancel: (body?: unknown) => Promise<unknown>;
setEditOrderDialogOpen: (value: boolean) => void;
setEditOrder: (
order: Orders_party_ordersConnection_edges_node | null
) => void;
cancel: (order: OrderFields) => void;
setEditOrder: (order: OrderFields) => void;
};
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
({ cancel, setEditOrderDialogOpen, setEditOrder, ...props }, ref) => {
({ cancel, setEditOrder, ...props }, ref) => {
return (
<AgGrid
ref={ref}
@ -260,54 +239,36 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
<AgGridColumn
field="edit"
cellRenderer={({ data }: ICellRendererParams) => {
if (
![
OrderStatus.Cancelled,
OrderStatus.Rejected,
OrderStatus.Expired,
OrderStatus.Filled,
OrderStatus.Stopped,
].includes(data.status)
) {
if (!data) return null;
if (isOrderActive(data.status)) {
return (
<Button
data-testid="edit"
variant="secondary"
onClick={() => {
setEditOrderDialogOpen(true);
setEditOrder(data);
}}
>
Edit
{t('Edit')}
</Button>
);
}
return null;
}}
/>
<AgGridColumn
field="cancel"
cellRenderer={({ data }: ICellRendererParams) => {
if (
![
OrderStatus.Cancelled,
OrderStatus.Rejected,
OrderStatus.Expired,
OrderStatus.Filled,
OrderStatus.Stopped,
].includes(data.status)
) {
if (!data) return null;
if (isOrderActive(data.status)) {
return (
<Button
data-testid="cancel"
onClick={async () => {
await cancel(data);
}}
>
<Button data-testid="cancel" onClick={() => cancel(data)}>
Cancel
</Button>
);
}
return null;
}}
/>
@ -315,3 +276,61 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
);
}
);
/**
* Check if an order is active to determine if it can be edited or cancelled
*/
const isOrderActive = (status: OrderStatus) => {
return ![
OrderStatus.Cancelled,
OrderStatus.Rejected,
OrderStatus.Expired,
OrderStatus.Filled,
OrderStatus.Stopped,
].includes(status);
};
const getEditDialogTitle = (status?: OrderStatus): string | undefined => {
if (!status) {
return;
}
switch (status) {
case OrderStatus.Active:
return t('Order updated');
case OrderStatus.Filled:
return t('Order filled');
case OrderStatus.PartiallyFilled:
return t('Order partially filled');
case OrderStatus.Parked:
return t('Order parked');
default:
return t('Submission failed');
}
};
const getCancelDialogIntent = (status?: OrderStatus): Intent | undefined => {
if (!status) {
return;
}
switch (status) {
case OrderStatus.Cancelled:
return Intent.Success;
default:
return Intent.Danger;
}
};
const getCancelDialogTitle = (status?: OrderStatus): string | undefined => {
if (!status) {
return;
}
switch (status) {
case OrderStatus.Cancelled:
return t('Order cancelled');
default:
return t('Order cancellation failed');
}
};

View File

@ -1,4 +1,3 @@
export * from './components';
export * from './order-hooks';
export * from './utils';
export * from './market';

View File

@ -1,13 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { MarketState, MarketTradingMode } from '@vegaprotocol/types';
export interface Market {
__typename?: string;
id: string;
positionDecimalPlaces: number;
state: MarketState;
decimalPlaces: number;
tradingMode: MarketTradingMode;
tradableInstrument?: any;
depth?: any;
}

View File

@ -40,6 +40,12 @@ export interface OrderEvent_busEvents_event_Order_market {
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64).
* i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes.
* 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market.
*/
positionDecimalPlaces: number;
}
export interface OrderEvent_busEvents_event_Order {
@ -76,6 +82,10 @@ export interface OrderEvent_busEvents_event_Order {
* The timeInForce of order (determines how and if it executes, and whether it persists on the book)
*/
timeInForce: OrderTimeInForce;
/**
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
*/
expiresAt: string | null;
/**
* Whether the order is to buy or sell
*/

View File

@ -20,6 +20,7 @@ export const ORDER_EVENT_SUB = gql`
id
name
decimalPlaces
positionDecimalPlaces
}
}
}

View File

@ -1,7 +1,6 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react-hooks';
import { MarketState, MarketTradingMode, OrderType } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import type {
@ -15,33 +14,6 @@ import type {
} from './__generated__/OrderEvent';
import { ORDER_EVENT_SUB } from './order-event-query';
const defaultMarket = {
__typename: 'Market',
id: 'market-id',
decimalPlaces: 2,
positionDecimalPlaces: 1,
tradingMode: MarketTradingMode.Continuous,
state: MarketState.Active,
name: 'market-name',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
product: {
__typename: 'Future',
quoteName: 'quote-name',
},
},
},
depth: {
__typename: 'MarketDepth',
lastTrade: {
__typename: 'Trade',
price: '100',
},
},
};
const defaultWalletContext = {
keypair: null,
keypairs: [],
@ -147,23 +119,15 @@ describe('useOrderCancel', () => {
expect(result.current.transaction.error).toEqual(null);
});
it('should not sendTx if no keypair', async () => {
it('should not sendTx if no keypair', () => {
const mockSendTx = jest.fn();
const order = {
type: OrderType.Market,
size: '10',
price: '1234567.89',
status: '',
rejectionReason: null,
market: defaultMarket,
};
const { result } = setup({
sendTx: mockSendTx,
keypairs: [],
keypair: null,
});
await act(async () => {
result.current.cancel(order);
act(() => {
result.current.cancel({ orderId: 'order-id', marketId: 'market-id' });
});
expect(mockSendTx).not.toHaveBeenCalled();
});
@ -173,30 +137,24 @@ describe('useOrderCancel', () => {
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const order = {
type: OrderType.Limit,
size: '10',
price: '1234567.89',
status: '',
rejectionReason: null,
market: defaultMarket,
};
const { result } = setup({
sendTx: mockSendTx,
keypairs: [keypair],
keypair,
});
await act(async () => {
result.current.cancel(order);
const args = {
orderId: 'order-id',
marketId: 'market-id',
};
act(() => {
result.current.cancel(args);
});
expect(mockSendTx).toHaveBeenCalledWith({
pubKey: keypair.pub,
propagate: true,
orderCancellation: {
marketId: 'market-id',
},
orderCancellation: args,
});
});
});

View File

@ -1,116 +1,68 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useState } from 'react';
import { useVegaWallet, useVegaTransaction } from '@vegaprotocol/wallet';
import { useApolloClient } from '@apollo/client';
import type {
OrderEvent,
OrderEventVariables,
OrderEvent_busEvents_event_Order,
} from './__generated__/OrderEvent';
import { ORDER_EVENT_SUB } from './order-event-query';
import type { OrderEvent_busEvents_event_Order } from './__generated__/OrderEvent';
import * as Sentry from '@sentry/react';
import { OrderStatus } from '@vegaprotocol/types';
import { determineId } from '@vegaprotocol/react-helpers';
import type { Subscription } from 'zen-observable-ts';
import { useOrderEvent } from './use-order-event';
interface CancelOrderArgs {
orderId: string;
marketId: string;
}
export const useOrderCancel = () => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const [updatedOrder, setUpdatedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const client = useApolloClient();
const subRef = useRef<Subscription | null>(null);
const waitForOrderEvent = useOrderEvent();
useEffect(() => {
return () => {
subRef.current?.unsubscribe();
setUpdatedOrder(null);
resetTransaction();
};
}, [resetTransaction]);
const [cancelledOrder, setCancelledOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const {
send,
transaction,
reset: resetTransaction,
setComplete,
TransactionDialog,
} = useVegaTransaction();
const reset = useCallback(() => {
resetTransaction();
setUpdatedOrder(null);
subRef.current?.unsubscribe();
setCancelledOrder(null);
}, [resetTransaction]);
const cancel = useCallback(
async (order) => {
async (args: CancelOrderArgs) => {
if (!keypair) {
return;
}
if (
[
OrderStatus.Cancelled,
OrderStatus.Rejected,
OrderStatus.Expired,
OrderStatus.Filled,
OrderStatus.Stopped,
].includes(order.status)
) {
return;
}
setUpdatedOrder(null);
setCancelledOrder(null);
try {
const res = await send({
await send({
pubKey: keypair.pub,
propagate: true,
orderCancellation: {
orderId: order.id,
marketId: order.market.id,
orderId: args.orderId,
marketId: args.marketId,
},
});
if (res?.signature) {
const resId = order.id ?? determineId(res.signature);
setUpdatedOrder(null);
if (resId) {
// Start a subscription looking for the newly created order
subRef.current = client
.subscribe<OrderEvent, OrderEventVariables>({
query: ORDER_EVENT_SUB,
variables: { partyId: keypair?.pub || '' },
})
.subscribe(({ data }) => {
if (!data?.busEvents?.length) {
return;
}
// No types available for the subscription result
const matchingOrderEvent = data.busEvents.find((e) => {
if (e.event.__typename !== 'Order') {
return false;
}
return e.event.id === resId;
});
if (
matchingOrderEvent &&
matchingOrderEvent.event.__typename === 'Order'
) {
setUpdatedOrder(matchingOrderEvent.event);
subRef.current?.unsubscribe();
}
});
}
}
return res;
waitForOrderEvent(args.orderId, keypair.pub, (cancelledOrder) => {
setCancelledOrder(cancelledOrder);
setComplete();
});
} catch (e) {
Sentry.captureException(e);
return;
}
},
[client, keypair, send]
[keypair, send, setComplete, waitForOrderEvent]
);
return {
transaction,
updatedOrder,
cancelledOrder,
TransactionDialog,
cancel,
reset,
};

View File

@ -3,11 +3,7 @@ import type {
VegaKeyExtended,
VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import {
VegaWalletOrderSide,
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
} from '@vegaprotocol/wallet';
import { VegaWalletOrderTimeInForce } from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react';
import { useOrderEdit } from './use-order-edit';
@ -18,15 +14,8 @@ import type {
import { ORDER_EVENT_SUB } from './order-event-query';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import {
MarketTradingMode,
MarketState,
OrderTimeInForce,
} from '@vegaprotocol/types';
import type {
OrderAmendmentBodyOrderAmendment,
OrderAmendmentBody,
} from '@vegaprotocol/vegawallet-service-api-client';
import type { OrderFields } from '../components';
import { generateOrder } from '../components';
const defaultWalletContext = {
keypair: null,
@ -38,7 +27,7 @@ const defaultWalletContext = {
connector: null,
};
function setup(context?: Partial<VegaWalletContextShape>) {
function setup(order: OrderFields, context?: Partial<VegaWalletContextShape>) {
const mocks: MockedResponse<OrderEvent> = {
request: {
query: ORDER_EVENT_SUB,
@ -119,86 +108,47 @@ function setup(context?: Partial<VegaWalletContextShape>) {
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderEdit(), { wrapper });
return renderHook(() => useOrderEdit(order), { wrapper });
}
const defaultMarket = {
__typename: 'Market',
id: 'market-id',
decimalPlaces: 2,
positionDecimalPlaces: 1,
tradingMode: MarketTradingMode.Continuous,
state: MarketState.Active,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
product: {
__typename: 'Future',
quoteName: 'quote-name',
},
},
},
depth: {
__typename: 'MarketDepth',
lastTrade: {
__typename: 'Trade',
price: '100',
},
},
};
const order = {
id: 'order-id',
type: VegaWalletOrderType.Limit,
size: '10',
timeInForce: OrderTimeInForce.GTT, // order timeInForce is transformed to wallet timeInForce
side: VegaWalletOrderSide.Buy,
price: '1234567.89',
expiration: new Date('2022-01-01'),
expiresAt: new Date('2022-01-01'),
status: VegaTxStatus.Pending,
rejectionReason: null,
market: {
id: 'market-id',
decimalPlaces: 2,
name: 'ETHDAI',
positionDecimalPlaces: 2,
},
};
describe('useOrderEdit', () => {
it('should edit a correctly formatted order', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const { result } = setup({
const order = generateOrder({
price: '123456789',
market: { decimalPlaces: 2 },
});
const { result } = setup(order, {
sendTx: mockSendTx,
keypairs: [keypair],
keypair,
});
await act(async () => {
result.current.edit(order);
act(() => {
result.current.edit({ price: '1234567.89' });
});
expect(mockSendTx).toHaveBeenCalledWith({
pubKey: keypair.pub,
propagate: true,
orderAmendment: {
orderId: 'order-id',
marketId: defaultMarket.id, // Market provided from hook argument
timeInForce: VegaWalletOrderTimeInForce.GTT,
orderId: order.id,
// eslint-disable-next-line
marketId: order.market!.id,
timeInForce: VegaWalletOrderTimeInForce[order.timeInForce],
price: { value: '123456789' }, // Decimal removed
sizeDelta: 0,
expiresAt: { value: order.expiration?.getTime() + '000000' }, // Nanoseconds append
} as unknown as OrderAmendmentBodyOrderAmendment,
} as OrderAmendmentBody);
expiresAt: undefined,
},
});
});
it('has the correct default state', () => {
const { result } = setup();
const order = generateOrder();
const { result } = setup(order);
expect(typeof result.current.edit).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
@ -207,8 +157,9 @@ describe('useOrderEdit', () => {
});
it('should not sendTx if no keypair', async () => {
const order = generateOrder();
const mockSendTx = jest.fn();
const { result } = setup({
const { result } = setup(order, {
sendTx: mockSendTx,
keypairs: [],
keypair: null,

View File

@ -1,50 +1,51 @@
import { useApolloClient } from '@apollo/client';
import { determineId, removeDecimal } from '@vegaprotocol/react-helpers';
import { useState, useCallback, useEffect, useRef } from 'react';
import type { Order } from '@vegaprotocol/wallet';
import { VegaWalletOrderTimeInForce } from '@vegaprotocol/wallet';
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
import { ORDER_EVENT_SUB } from './order-event-query';
import type { Subscription } from 'zen-observable-ts';
import type {
OrderEvent_busEvents_event_Order,
OrderEvent,
OrderEventVariables,
} from './__generated__';
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/react-helpers';
import { useState, useCallback } from 'react';
import {
useVegaTransaction,
useVegaWallet,
VegaWalletOrderTimeInForce,
} from '@vegaprotocol/wallet';
import type { OrderEvent_busEvents_event_Order } from './__generated__';
import * as Sentry from '@sentry/react';
import type { OrderFields } from '../components';
import { useOrderEvent } from './use-order-event';
export const useOrderEdit = () => {
// Can only edit price for now
export interface EditOrderArgs {
price: string;
}
export const useOrderEdit = (order: OrderFields | null) => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const [updatedOrder, setUpdatedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const client = useApolloClient();
const subRef = useRef<Subscription | null>(null);
const {
send,
transaction,
reset: resetTransaction,
setComplete,
TransactionDialog,
} = useVegaTransaction();
const waitForOrderEvent = useOrderEvent();
const reset = useCallback(() => {
resetTransaction();
setUpdatedOrder(null);
subRef.current?.unsubscribe();
}, [resetTransaction]);
useEffect(() => {
return () => {
resetTransaction();
setUpdatedOrder(null);
subRef.current?.unsubscribe();
};
}, [resetTransaction]);
const edit = useCallback(
async (order: Order) => {
if (!keypair || !order.market || !order.market.id) {
async (args: EditOrderArgs) => {
if (!keypair || !order || !order.market) {
return;
}
setUpdatedOrder(null);
try {
const res = await send({
await send({
pubKey: keypair.pub,
propagate: true,
orderAmendment: {
@ -52,70 +53,35 @@ export const useOrderEdit = () => {
marketId: order.market.id,
// @ts-ignore fix me please!
price: {
value: removeDecimal(order.price, order.market?.decimalPlaces),
value: removeDecimal(args.price, order.market.decimalPlaces),
},
timeInForce: VegaWalletOrderTimeInForce[order.timeInForce],
// @ts-ignore fix me please!
sizeDelta: 0,
// @ts-ignore fix me please!
expiresAt: order.expiresAt
? {
value:
// Wallet expects timestamp in nanoseconds,
// we don't have that level of accuracy so just append 6 zeroes
new Date(order.expiresAt).getTime().toString() + '000000',
value: toNanoSeconds(new Date(order.expiresAt)), // Wallet expects timestamp in nanoseconds
}
: undefined,
},
});
if (res?.signature) {
const resId = order.id ?? determineId(res.signature);
setUpdatedOrder(null);
if (resId) {
// Start a subscription looking for the newly created order
subRef.current = client
.subscribe<OrderEvent, OrderEventVariables>({
query: ORDER_EVENT_SUB,
variables: { partyId: keypair?.pub || '' },
})
.subscribe(({ data }) => {
if (!data?.busEvents?.length) {
return;
}
// No types available for the subscription result
const matchingOrderEvent = data.busEvents.find((e) => {
if (e.event.__typename !== 'Order') {
return false;
}
return e.event.id === resId;
});
if (
matchingOrderEvent &&
matchingOrderEvent.event.__typename === 'Order'
) {
setUpdatedOrder(matchingOrderEvent.event);
subRef.current?.unsubscribe();
}
});
}
}
return res;
waitForOrderEvent(order.id, keypair.pub, (updatedOrder) => {
setUpdatedOrder(updatedOrder);
setComplete();
});
} catch (e) {
Sentry.captureException(e);
return;
}
},
[client, keypair, send]
[keypair, send, order, setComplete, waitForOrderEvent]
);
return {
transaction,
updatedOrder,
TransactionDialog,
edit,
reset,
};

View File

@ -0,0 +1,59 @@
import { useApolloClient } from '@apollo/client';
import { useCallback, useEffect, useRef } from 'react';
import { ORDER_EVENT_SUB } from './order-event-query';
import type {
OrderEvent,
OrderEventVariables,
OrderEvent_busEvents_event_Order,
} from './__generated__';
import type { Subscription } from 'zen-observable-ts';
export const useOrderEvent = () => {
const client = useApolloClient();
const subRef = useRef<Subscription | null>(null);
const waitForOrderEvent = useCallback(
(
id: string,
partyId: string,
callback: (order: OrderEvent_busEvents_event_Order) => void
) => {
subRef.current = client
.subscribe<OrderEvent, OrderEventVariables>({
query: ORDER_EVENT_SUB,
variables: { partyId },
})
.subscribe(({ data }) => {
if (!data?.busEvents?.length) {
return;
}
// No types available for the subscription result
const matchingOrderEvent = data.busEvents.find((e) => {
if (e.event.__typename !== 'Order') {
return false;
}
return e.event.id === id;
});
if (
matchingOrderEvent &&
matchingOrderEvent.event.__typename === 'Order'
) {
callback(matchingOrderEvent.event);
subRef.current?.unsubscribe();
}
});
},
[client]
);
useEffect(() => {
return () => {
subRef.current?.unsubscribe();
};
}, []);
return waitForOrderEvent;
};

View File

@ -21,6 +21,7 @@ import { ORDER_EVENT_SUB } from './order-event-query';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { Market } from '../market';
import { toNanoSeconds } from '@vegaprotocol/react-helpers';
const defaultMarket = {
__typename: 'Market',
@ -179,7 +180,9 @@ describe('useOrderSubmit', () => {
side: VegaWalletOrderSide.Buy,
timeInForce: VegaWalletOrderTimeInForce.GTT,
price: '123456789', // Decimal removed
expiresAt: order.expiration?.getTime() + '000000', // Nanoseconds append
expiresAt: order.expiration
? toNanoSeconds(order.expiration)
: undefined,
},
});
});

View File

@ -1,39 +1,52 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useApolloClient } from '@apollo/client';
import type { Order } from '../utils/get-default-order';
import { ORDER_EVENT_SUB } from './order-event-query';
import { useCallback, useState } from 'react';
import type { OrderEvent_busEvents_event_Order } from './__generated__';
import type {
OrderEvent,
OrderEventVariables,
OrderEvent_busEvents_event_Order,
} from './__generated__';
VegaWalletOrderTimeInForce,
VegaWalletOrderSide,
} from '@vegaprotocol/wallet';
import { VegaWalletOrderType, useVegaWallet } from '@vegaprotocol/wallet';
import { determineId, removeDecimal } from '@vegaprotocol/react-helpers';
import {
determineId,
removeDecimal,
toNanoSeconds,
} from '@vegaprotocol/react-helpers';
import { useVegaTransaction } from '@vegaprotocol/wallet';
import * as Sentry from '@sentry/react';
import type { Market } from '../market';
import type { Subscription } from 'zen-observable-ts';
import { useOrderEvent } from './use-order-event';
export interface Order {
type: VegaWalletOrderType;
size: string;
side: VegaWalletOrderSide;
timeInForce: VegaWalletOrderTimeInForce;
price?: string;
expiration?: Date;
}
export interface Market {
id: string;
decimalPlaces: number;
positionDecimalPlaces: number;
}
export const useOrderSubmit = (market: Market) => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const waitForOrderEvent = useOrderEvent();
const {
send,
transaction,
reset: resetTransaction,
setComplete,
TransactionDialog,
} = useVegaTransaction();
const [finalizedOrder, setFinalizedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const client = useApolloClient();
const subRef = useRef<Subscription | null>(null);
const reset = useCallback(() => {
resetTransaction();
setFinalizedOrder(null);
subRef.current?.unsubscribe();
}, [resetTransaction]);
useEffect(() => {
return () => {
resetTransaction();
setFinalizedOrder(null);
subRef.current?.unsubscribe();
};
}, [resetTransaction]);
const submit = useCallback(
@ -43,6 +56,7 @@ export const useOrderSubmit = (market: Market) => {
}
setFinalizedOrder(null);
try {
const res = await send({
pubKey: keypair.pub,
@ -58,9 +72,7 @@ export const useOrderSubmit = (market: Market) => {
side: order.side,
timeInForce: order.timeInForce,
expiresAt: order.expiration
? // Wallet expects timestamp in nanoseconds, we don't have that level of accuracy so
// just append 6 zeroes
order.expiration.getTime().toString() + '000000'
? toNanoSeconds(order.expiration) // Wallet expects timestampe in nanoseconds
: undefined,
},
});
@ -68,34 +80,10 @@ export const useOrderSubmit = (market: Market) => {
if (res?.signature) {
const resId = determineId(res.signature);
if (resId) {
// Start a subscription looking for the newly created order
subRef.current = client
.subscribe<OrderEvent, OrderEventVariables>({
query: ORDER_EVENT_SUB,
variables: { partyId: keypair?.pub || '' },
})
.subscribe(({ data }) => {
if (!data?.busEvents?.length) {
return;
}
// No types available for the subscription result
const matchingOrderEvent = data.busEvents.find((e) => {
if (e.event.__typename !== 'Order') {
return false;
}
return e.event.id === resId;
});
if (
matchingOrderEvent &&
matchingOrderEvent.event.__typename === 'Order'
) {
setFinalizedOrder(matchingOrderEvent.event);
subRef.current?.unsubscribe();
}
});
waitForOrderEvent(resId, keypair.pub, (order) => {
setFinalizedOrder(order);
setComplete();
});
}
}
return res;
@ -104,19 +92,13 @@ export const useOrderSubmit = (market: Market) => {
return;
}
},
[
client,
keypair,
send,
market.id,
market.decimalPlaces,
market.positionDecimalPlaces,
]
[keypair, send, market, setComplete, waitForOrderEvent]
);
return {
transaction,
finalizedOrder,
TransactionDialog,
submit,
reset,
};

View File

@ -1,19 +1,22 @@
import type { FieldErrors } from 'react-hook-form';
import { useMemo } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import type { Order } from '@vegaprotocol/wallet';
import {
useVegaWallet,
VegaWalletOrderTimeInForce as OrderTimeInForce,
VegaWalletOrderType as OrderType,
} from '@vegaprotocol/wallet';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { Market } from '../market';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
import type { Order } from './use-order-submit';
export type ValidationProps = {
export type ValidationArgs = {
step: number;
market: Market;
market: {
state: MarketState;
tradingMode: MarketTradingMode;
positionDecimalPlaces: number;
};
orderType: OrderType;
orderTimeInForce: OrderTimeInForce;
fieldErrors?: FieldErrors<Order>;
@ -34,7 +37,7 @@ export const useOrderValidation = ({
fieldErrors = {},
orderType,
orderTimeInForce,
}: ValidationProps) => {
}: ValidationArgs) => {
const { keypair } = useVegaWallet();
const { message, isDisabled } = useMemo(() => {

View File

@ -4,38 +4,11 @@ import {
VegaWalletOrderSide,
} from '@vegaprotocol/wallet';
import { toDecimal } from '@vegaprotocol/react-helpers';
import type { Market } from '../market';
import type { OrderStatus } from '@vegaprotocol/types';
export type Order =
| {
size: string;
type: VegaWalletOrderType.Market;
timeInForce: VegaWalletOrderTimeInForce;
side: VegaWalletOrderSide;
price?: never;
expiration?: never;
rejectionReason: string | null;
status?: OrderStatus;
market?: Market | null;
}
| {
size: string;
type: VegaWalletOrderType.Limit;
timeInForce: VegaWalletOrderTimeInForce;
side: VegaWalletOrderSide;
price?: string;
expiration?: Date;
rejectionReason: string | null;
status?: OrderStatus;
market?: Market | null;
};
import type { Order, Market } from '../order-hooks';
export const getDefaultOrder = (market: Market): Order => ({
type: VegaWalletOrderType.Market,
side: VegaWalletOrderSide.Buy,
timeInForce: VegaWalletOrderTimeInForce.IOC,
size: String(toDecimal(market.positionDecimalPlaces)),
rejectionReason: null,
market: null,
});

View File

@ -8,4 +8,5 @@ export * from './lib/i18n';
export * from './lib/pagination';
export * from './lib/remove-0x';
export * from './lib/storage';
export * from './lib/time';
export * from './lib/validate';

View File

@ -0,0 +1,3 @@
export const toNanoSeconds = (date: Date) => {
return date.getTime().toString() + '000000';
};

View File

@ -293,6 +293,16 @@ export enum WithdrawalStatus {
Rejected = "Rejected",
}
/**
* Pagination constructs to support cursor based pagination in the API
*/
export interface Pagination {
first?: number | null;
after?: string | null;
last?: number | null;
before?: string | null;
}
//==============================================================
// END Enums and Input Objects
//==============================================================

View File

@ -1,3 +1,2 @@
export * from './__generated__';
export * from './candle';
export * from './pagination';

View File

@ -1,6 +0,0 @@
export interface Pagination {
first?: number;
after?: string;
last?: number;
before?: string;
}

View File

@ -10,6 +10,7 @@ interface DialogProps {
open: boolean;
onChange?: (isOpen: boolean) => void;
title?: string;
icon?: ReactNode;
intent?: Intent;
titleClassNames?: string;
contentClassNames?: string;
@ -20,6 +21,7 @@ export function Dialog({
open,
onChange,
title,
icon,
intent,
titleClassNames,
contentClassNames,
@ -50,15 +52,20 @@ export function Dialog({
className="focus:outline-none focus-visible:outline-none"
/>
</DialogPrimitives.Close>
{title && (
<h1
className={`text-h5 text-black-95 dark:text-white-95 mt-0 mb-20 ${titleClassNames}`}
data-testid="dialog-title"
>
{title}
</h1>
)}
{children}
<div className="flex gap-12 max-w-full">
{icon && <div className="pt-8 fill-current">{icon}</div>}
<div data-testid="dialog-content" className="flex-1">
{title && (
<h1
className={`text-h4 font-bold text-black-95 dark:text-white-95 mt-0 mb-6 ${titleClassNames}`}
data-testid="dialog-title"
>
{title}
</h1>
)}
<div className="text-black-60 dark:text-white-60">{children}</div>
</div>
</div>
</DialogPrimitives.Content>
</DialogPrimitives.Portal>
</DialogPrimitives.Root>

View File

@ -2,12 +2,8 @@ import { act, renderHook } from '@testing-library/react-hooks';
import type { VegaWalletContextShape } from './context';
import { VegaWalletContext } from './context';
import type { ReactNode } from 'react';
import {
initialState,
useVegaTransaction,
VegaTxStatus,
} from './use-vega-transaction';
import type { OrderSubmission } from './types';
import { useVegaTransaction, VegaTxStatus } from './use-vega-transaction';
import type { OrderSubmissionBody } from '@vegaprotocol/vegawallet-service-api-client';
const defaultWalletContext = {
keypair: null,
@ -42,7 +38,7 @@ it('If provider returns null status should be default', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(null));
const { result } = setup({ sendTx: mockSendTx });
await act(async () => {
result.current.send({} as OrderSubmission);
result.current.send({} as OrderSubmissionBody);
});
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
});
@ -54,7 +50,7 @@ it('Handles a single error', async () => {
.mockReturnValue(Promise.resolve({ error: errorMessage }));
const { result } = setup({ sendTx: mockSendTx });
await act(async () => {
result.current.send({} as OrderSubmission);
result.current.send({} as OrderSubmissionBody);
});
expect(result.current.transaction.status).toEqual(VegaTxStatus.Error);
expect(result.current.transaction.error).toEqual({ error: errorMessage });
@ -69,7 +65,7 @@ it('Handles multiple errors', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(errorObj));
const { result } = setup({ sendTx: mockSendTx });
await act(async () => {
result.current.send({} as OrderSubmission);
result.current.send({} as OrderSubmissionBody);
});
expect(result.current.transaction.status).toEqual(VegaTxStatus.Error);
expect(result.current.transaction.error).toEqual(errorObj);
@ -90,7 +86,7 @@ it('Returns the signature if successful', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(successObj));
const { result } = setup({ sendTx: mockSendTx });
await act(async () => {
result.current.send({} as OrderSubmission);
result.current.send({} as OrderSubmissionBody);
});
expect(result.current.transaction.status).toEqual(VegaTxStatus.Pending);
expect(result.current.transaction.txHash).toEqual(successObj.txHash);
@ -98,14 +94,3 @@ it('Returns the signature if successful', async () => {
successObj.tx.signature.value
);
});
it('Resets transaction state if user rejects', async () => {
const mockSendTx = jest
.fn()
.mockReturnValue(Promise.resolve({ error: 'User rejected' }));
const { result } = setup({ sendTx: mockSendTx });
await act(async () => {
result.current.send({} as OrderSubmission);
});
expect(result.current.transaction).toEqual(initialState);
});

View File

@ -1,15 +1,24 @@
import { useCallback, useState } from 'react';
import type { ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { TransactionSubmission } from './wallet-types';
import { useVegaWallet } from './use-vega-wallet';
import type { SendTxError } from './context';
import { VegaTransactionDialog } from './vega-transaction-dialog';
import type { Intent } from '@vegaprotocol/ui-toolkit';
export interface DialogProps {
children?: JSX.Element;
intent?: Intent;
title?: string;
icon?: ReactNode;
}
export enum VegaTxStatus {
Default = 'Default',
Requested = 'Requested',
Pending = 'Pending',
Error = 'Error',
// Note no complete state as we have to use api calls/subs to check if
// our transaction was completed
Complete = 'Complete',
}
export interface VegaTxState {
@ -17,6 +26,7 @@ export interface VegaTxState {
error: object | null;
txHash: string | null;
signature: string | null;
dialogOpen: boolean;
}
export const initialState = {
@ -24,6 +34,7 @@ export const initialState = {
error: null,
txHash: null,
signature: null,
dialogOpen: false,
};
export const useVegaTransaction = () => {
@ -48,6 +59,10 @@ export const useVegaTransaction = () => {
setTransaction(initialState);
}, [setTransaction]);
const setComplete = useCallback(() => {
setTransaction({ status: VegaTxStatus.Complete });
}, [setTransaction]);
const send = useCallback(
async (tx: TransactionSubmission) => {
setTransaction({
@ -55,6 +70,7 @@ export const useVegaTransaction = () => {
txHash: null,
signature: null,
status: VegaTxStatus.Requested,
dialogOpen: true,
});
const res = await sendTx(tx);
@ -63,18 +79,14 @@ export const useVegaTransaction = () => {
setTransaction({ status: VegaTxStatus.Default });
return null;
}
if ('error' in res) {
// Close dialog if user rejects the transaction
if ('errors' in res) {
handleError(res);
} else if ('error' in res) {
if (res.error === 'User rejected') {
reset();
} else {
handleError(res);
}
return null;
} else if ('errors' in res) {
handleError(res);
return null;
} else if (res.tx?.signature?.value && res.txHash) {
setTransaction({
status: VegaTxStatus.Pending,
@ -91,5 +103,25 @@ export const useVegaTransaction = () => {
[sendTx, handleError, setTransaction, reset]
);
return { send, transaction, reset };
const TransactionDialog = useMemo(() => {
return (props: DialogProps) => (
<VegaTransactionDialog
{...props}
isOpen={transaction.dialogOpen}
onChange={(isOpen) => {
if (!isOpen) reset();
setTransaction({ dialogOpen: isOpen });
}}
transaction={transaction}
/>
);
}, [transaction, setTransaction, reset]);
return {
send,
transaction,
reset,
setComplete,
TransactionDialog,
};
};

View File

@ -1,10 +1,7 @@
import { render, screen } from '@testing-library/react';
import { OrderStatus, OrderType } from '@vegaprotocol/types';
import type { VegaTxState } from '../use-vega-transaction';
import { VegaTxStatus } from '../use-vega-transaction';
import type { Order } from '../wallet-types';
import type { VegaTransactionDialogProps } from './vega-transaction-dialog';
import { VegaDialog, VegaTransactionDialog } from './vega-transaction-dialog';
import { VegaTransactionDialog } from './vega-transaction-dialog';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
@ -13,222 +10,109 @@ jest.mock('@vegaprotocol/environment', () => ({
}));
describe('VegaTransactionDialog', () => {
let defaultProps: VegaTransactionDialogProps;
let props: VegaTransactionDialogProps;
beforeEach(() => {
defaultProps = {
orderDialogOpen: true,
setOrderDialogOpen: () => false,
props = {
isOpen: true,
onChange: () => false,
transaction: {
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
},
finalizedOrder: {
status: OrderStatus.Cancelled,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
},
reset: jest.fn(),
title: 'Order cancelled',
};
});
it('should render when an order is successfully cancelled', () => {
render(<VegaTransactionDialog {...defaultProps} />);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order cancelled'
);
});
it('should render when an order is not successfully cancelled', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Active,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
const propsForTest = {
transaction,
finalizedOrder,
};
render(
<VegaTransactionDialog
{...defaultProps}
{...propsForTest}
title={'Cancellation failed'}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Cancellation failed'
);
});
describe('TransactionDialog', () => {
it('should render when an order is successful', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Active,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
render(
<VegaDialog
finalizedOrder={finalizedOrder}
transaction={transaction}
title={'Order placed'}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order placed'
);
});
it('should render when transaction is requested', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Requested,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Active,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
render(
<VegaDialog
finalizedOrder={finalizedOrder}
transaction={transaction}
title={'Order tx'}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Confirm transaction in wallet'
);
});
it('should render when transaction has error', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Error,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Active,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
render(
<VegaDialog
finalizedOrder={finalizedOrder}
transaction={transaction}
title={'Order tx'}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order rejected by wallet'
);
});
it('should render when an order is rejected', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Rejected,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
render(
<VegaDialog
finalizedOrder={finalizedOrder}
transaction={transaction}
title={'Order title'}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order failed'
);
});
it('should render when pending consensus', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Error,
error: null,
txHash: null,
signature: null,
};
render(
<VegaDialog
finalizedOrder={null}
transaction={transaction}
title={'Order title'}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order rejected by wallet'
);
});
it('should render awaiting network confirmation and add link to tx in block explorer', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
error: null,
txHash: 'TxHash',
signature: null,
};
render(
<VegaDialog
finalizedOrder={null}
transaction={transaction}
title={'Order Tx'}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Awaiting network confirmation'
);
expect(screen.getByTestId('tx-block-explorer')).toHaveTextContent(
'View in block explorer'
);
expect(screen.getByTestId('tx-block-explorer')).toHaveAttribute(
'href',
'https://test.explorer.vega.network/txs/0xTxHash'
);
});
},
};
});
it('requested', () => {
render(<VegaTransactionDialog {...props} />);
expect(screen.getByTestId('dialog-title')).toHaveTextContent(/confirm/i);
expect(screen.getByTestId(VegaTxStatus.Requested)).toHaveTextContent(
/please open your wallet/i
);
});
it('pending', () => {
render(
<VegaTransactionDialog
{...props}
transaction={{
...props.transaction,
txHash: 'tx-hash',
status: VegaTxStatus.Pending,
}}
/>
);
expect(screen.getByTestId('dialog-title')).toHaveTextContent(/awaiting/i);
expect(screen.getByTestId(VegaTxStatus.Pending)).toHaveTextContent(
/please wait/i
);
testBlockExplorerLink('tx-hash');
});
it('error', () => {
render(
<VegaTransactionDialog
{...props}
transaction={{
...props.transaction,
error: { message: 'rejected' },
status: VegaTxStatus.Error,
}}
/>
);
expect(screen.getByTestId('dialog-title')).toHaveTextContent(/failed/i);
expect(screen.getByTestId(VegaTxStatus.Error)).toHaveTextContent(
/rejected/i
);
});
it('default complete', () => {
render(
<VegaTransactionDialog
{...props}
transaction={{
...props.transaction,
txHash: 'tx-hash',
status: VegaTxStatus.Complete,
}}
/>
);
expect(screen.getByTestId('dialog-title')).toHaveTextContent(/complete/i);
expect(screen.getByTestId(VegaTxStatus.Complete)).toHaveTextContent(
/confirmed/i
);
testBlockExplorerLink('tx-hash');
});
it('custom complete', () => {
render(
<VegaTransactionDialog
{...props}
transaction={{
...props.transaction,
txHash: 'tx-hash',
status: VegaTxStatus.Complete,
}}
title="Custom title"
>
<div>Custom content</div>
</VegaTransactionDialog>
);
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
'Custom title'
);
expect(screen.getByText('Custom content')).toBeInTheDocument();
});
function testBlockExplorerLink(txHash: string) {
expect(screen.getByTestId('tx-block-explorer')).toHaveTextContent(
'View in block explorer'
);
expect(screen.getByTestId('tx-block-explorer')).toHaveAttribute(
'href',
`https://test.explorer.vega.network/txs/0x${txHash}`
);
}
});

View File

@ -1,155 +1,94 @@
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { useEffect } from 'react';
import { useEnvironment } from '@vegaprotocol/environment';
import get from 'lodash/get';
import { t } from '@vegaprotocol/react-helpers';
import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import type { VegaTxState } from '../use-vega-transaction';
import { VegaTxStatus } from '../use-vega-transaction';
import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import {
addDecimalsFormatNumber,
formatLabel,
t,
} from '@vegaprotocol/react-helpers';
import { useEnvironment } from '@vegaprotocol/environment';
import { OrderType } from '@vegaprotocol/types';
import type { Order } from '../wallet-types';
import get from 'lodash/get';
export interface VegaTransactionDialogProps {
orderDialogOpen: boolean;
setOrderDialogOpen: (isOpen: boolean) => void;
finalizedOrder: Order | null;
isOpen: boolean;
onChange: (isOpen: boolean) => void;
transaction: VegaTxState;
reset: () => void;
title?: string;
children?: ReactNode;
intent?: Intent;
title?: string;
icon?: ReactNode;
}
const getDialogIntent = (
finalizedOrder: Order | null,
transaction: VegaTxState
) => {
if (finalizedOrder) {
return !finalizedOrder.rejectionReason ? Intent.Success : Intent.Danger;
}
switch (transaction.status) {
case VegaTxStatus.Requested:
return Intent.Warning;
case VegaTxStatus.Pending:
return Intent.Warning;
case VegaTxStatus.Error:
return Intent.Danger;
default:
return Intent.None;
}
};
export const VegaTransactionDialog = ({
orderDialogOpen,
setOrderDialogOpen,
finalizedOrder,
isOpen,
onChange,
transaction,
reset,
title = '',
children,
intent,
title,
icon,
}: VegaTransactionDialogProps) => {
// open / close dialog
useEffect(() => {
if (transaction.status !== VegaTxStatus.Default || finalizedOrder) {
setOrderDialogOpen(true);
} else {
setOrderDialogOpen(false);
}
}, [finalizedOrder, setOrderDialogOpen, transaction.status]);
const computedIntent = intent ? intent : getIntent(transaction);
const computedTitle = title ? title : getTitle(transaction);
const computedIcon = icon ? icon : getIcon(transaction);
// Each dialog can specify custom dialog content using data returned via
// the subscription that confirms the transaction. So if we get a success state
// and this custom content is provided, render it
const content =
transaction.status === VegaTxStatus.Complete && children ? (
children
) : (
<VegaDialog transaction={transaction} />
);
return (
<Dialog
open={orderDialogOpen}
onChange={(isOpen) => {
setOrderDialogOpen(isOpen);
// If closing reset
if (!isOpen) {
reset();
}
}}
intent={getDialogIntent(finalizedOrder, transaction)}
open={isOpen}
onChange={onChange}
intent={computedIntent}
title={computedTitle}
icon={computedIcon}
>
<VegaDialog
key={`${title.toLowerCase().split(' ').join('-')}-tx-${
transaction.txHash
}`}
transaction={transaction}
finalizedOrder={finalizedOrder}
title={title}
children={children}
/>
{content}
</Dialog>
);
};
interface VegaDialogProps {
transaction: VegaTxState;
finalizedOrder: Order | null;
title: string;
children?: ReactNode;
}
export const VegaDialog = ({
transaction,
finalizedOrder,
title,
children,
}: VegaDialogProps) => {
/**
* Default dialog content
*/
export const VegaDialog = ({ transaction }: VegaDialogProps) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
const headerClassName = 'text-h5 font-bold text-black dark:text-white';
if (children && transaction.status === VegaTxStatus.Default) {
return <div>{children}</div>;
}
// Rejected by wallet
if (transaction.status === VegaTxStatus.Requested) {
return (
<OrderDialogWrapper
title="Confirm transaction in wallet"
icon={<Icon name="hand-up" size={20} />}
>
<p>
{t(
'Please open your wallet application and confirm or reject the transaction'
)}
</p>
</OrderDialogWrapper>
<p data-testid={transaction.status}>
{t(
'Please open your wallet application and confirm or reject the transaction'
)}
</p>
);
}
// Transaction error
if (transaction.status === VegaTxStatus.Error) {
return (
<OrderDialogWrapper
title="Order rejected by wallet"
icon={<Icon name="warning-sign" size={20} />}
>
<div data-testid={transaction.status}>
{transaction.error && (
<pre className="text-ui break-all whitespace-pre-wrap">
{get(transaction.error, 'error') ??
JSON.stringify(transaction.error, null, 2)}
</pre>
)}
</OrderDialogWrapper>
</div>
);
}
// Pending consensus
if (!finalizedOrder) {
if (transaction.status === VegaTxStatus.Pending) {
return (
<OrderDialogWrapper
title="Awaiting network confirmation"
icon={<Loader size="small" />}
>
{transaction.txHash && (
<p className="break-all">
{t('Waiting for few more blocks')} - &nbsp;
<div data-testid={transaction.status}>
<p className="break-all">
{t('Please wait for your transaction to be confirmed')} - &nbsp;
{transaction.txHash && (
<a
className="underline"
data-testid="tx-block-explorer"
@ -159,108 +98,77 @@ export const VegaDialog = ({
>
{t('View in block explorer')}
</a>
</p>
)}
</OrderDialogWrapper>
);
}
// Order on network but was rejected
if (finalizedOrder.status === 'Rejected') {
return (
<OrderDialogWrapper
title="Order failed"
icon={<Icon name="warning-sign" size={20} />}
>
<p data-testid="error-reason">
{finalizedOrder.rejectionReason &&
t(`Reason: ${formatLabel(finalizedOrder.rejectionReason)}`)}
)}
</p>
</OrderDialogWrapper>
</div>
);
}
return (
<OrderDialogWrapper title={title} icon={<Icon name="tick" size={20} />}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{finalizedOrder.market && (
<div>
<p className={headerClassName}>{t(`Market`)}</p>
<p>{t(`${finalizedOrder.market.name}`)}</p>
</div>
)}
<div>
<p className={headerClassName}>{t(`Status`)}</p>
<p>{t(`${finalizedOrder.status}`)}</p>
</div>
{finalizedOrder.type === OrderType.Limit && finalizedOrder.market && (
<div>
<p className={headerClassName}>{t(`Price`)}</p>
<p>
{addDecimalsFormatNumber(
finalizedOrder.price,
finalizedOrder.market.decimalPlaces
)}
</p>
</div>
)}
<div>
<p className={headerClassName}>{t(`Amount`)}</p>
<p
className={
finalizedOrder.side === 'Buy'
? 'text-vega-green'
: 'text-vega-red'
}
>
{`${finalizedOrder.side === 'Buy' ? '+' : '-'} ${
finalizedOrder.size
}
`}
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-8">
{transaction.txHash && (
<div>
<p className={headerClassName}>{t(`Transaction`)}</p>
if (transaction.status === VegaTxStatus.Complete) {
return (
<div data-testid={transaction.status}>
<p className="break-all">
{t('Your transaction has been confirmed')} - &nbsp;
{transaction.txHash && (
<a
className="underline break-words"
className="underline"
data-testid="tx-block-explorer"
href={`${VEGA_EXPLORER_URL}/txs/0x${transaction.txHash}`}
target="_blank"
rel="noreferrer"
>
{transaction.txHash}
{t('View in block explorer')}
</a>
</div>
)}
)}
</p>
</div>
</OrderDialogWrapper>
);
);
}
return null;
};
interface OrderDialogWrapperProps {
children: ReactNode;
icon: ReactNode;
title: string;
}
export const OrderDialogWrapper = ({
children,
icon,
title,
}: OrderDialogWrapperProps) => {
const headerClassName = 'text-h4 font-bold text-black dark:text-white';
return (
<div className="flex gap-12 max-w-full">
<div className="pt-8 fill-current">{icon}</div>
<div data-testid="order-wrapper" className="flex-1">
<h1 data-testid="order-status-header" className={headerClassName}>
{title}
</h1>
{children}
</div>
</div>
);
const getIntent = (transaction: VegaTxState) => {
switch (transaction.status) {
case VegaTxStatus.Requested:
return Intent.Warning;
case VegaTxStatus.Pending:
return Intent.Warning;
case VegaTxStatus.Error:
return Intent.Danger;
case VegaTxStatus.Complete:
return Intent.Success;
default:
return Intent.None;
}
};
const getTitle = (transaction: VegaTxState) => {
switch (transaction.status) {
case VegaTxStatus.Requested:
return t('Confirm transaction in wallet');
case VegaTxStatus.Pending:
return t('Awaiting network confirmation');
case VegaTxStatus.Error:
return t('Transaction failed');
case VegaTxStatus.Complete:
return t('Transaction complete');
default:
return '';
}
};
const getIcon = (transaction: VegaTxState) => {
switch (transaction.status) {
case VegaTxStatus.Requested:
return <Icon name="hand-up" size={20} />;
case VegaTxStatus.Pending:
return <Loader size="small" />;
case VegaTxStatus.Error:
return <Icon name="warning-sign" size={20} />;
case VegaTxStatus.Complete:
return <Icon name="tick" size={20} />;
default:
return '';
}
};

View File

@ -1,4 +1,3 @@
import type { OrderTimeInForce } from '@vegaprotocol/types';
import type {
DelegateSubmissionBody,
OrderCancellationBody,
@ -37,23 +36,3 @@ export type TransactionSubmission =
| DelegateSubmissionBody
| UndelegateSubmissionBody
| OrderAmendmentBody;
export interface Market {
name: string;
positionDecimalPlaces?: number;
decimalPlaces: number;
id?: string;
}
export interface Order {
id?: string;
status?: string;
rejectionReason?: string | null;
size: string;
price: string;
market: Market | null;
type: string | null;
side?: string;
timeInForce: OrderTimeInForce;
expiresAt?: Date | string | null;
}

View File

@ -1,23 +0,0 @@
import type { ReactNode } from 'react';
interface DialogWrapperProps {
children: ReactNode;
icon: ReactNode;
title: string;
}
export const DialogWrapper = ({
children,
icon,
title,
}: DialogWrapperProps) => {
return (
<div className="flex gap-12 max-w-full text-ui">
<div className="pt-8 fill-current">{icon}</div>
<div className="flex-1">
<h1 className="text-h4 text-black dark:text-white mb-12">{title}</h1>
<div className="text-black-40 dark:text-white-40">{children}</div>
</div>
</div>
);
};

View File

@ -4,7 +4,6 @@ import { isEthereumError } from '../ethereum-error';
import type { EthTxState } from '../use-ethereum-transaction';
import { EthTxStatus } from '../use-ethereum-transaction';
import { ConfirmRow, TxRow, ConfirmationEventRow } from './dialog-rows';
import { DialogWrapper } from './dialog-wrapper';
export interface TransactionDialogProps {
name: string;
@ -101,11 +100,16 @@ export const TransactionDialog = ({
return propsMap[status];
};
const { intent, ...wrapperProps } = getWrapperProps();
const { intent, title, icon } = getWrapperProps();
return (
<Dialog open={transaction.dialogOpen} onChange={onChange} intent={intent}>
<DialogWrapper {...wrapperProps}>{renderContent()}</DialogWrapper>
<Dialog
open={transaction.dialogOpen}
onChange={onChange}
intent={intent}
title={title}
icon={icon}
>
{renderContent()}
</Dialog>
);
};

View File

@ -25,38 +25,22 @@ export const WithdrawDialog = ({
onDialogChange,
}: WithdrawDialogProps) => {
const { ETHERSCAN_URL } = useEnvironment();
const { intent, ...props } = getProps(approval, vegaTx, ethTx, ETHERSCAN_URL);
return (
<Dialog open={dialogOpen} intent={intent} onChange={onDialogChange}>
<DialogWrapper {...props} />
</Dialog>
const { intent, title, icon, children } = getProps(
approval,
vegaTx,
ethTx,
ETHERSCAN_URL
);
};
interface DialogWrapperProps {
children: ReactNode;
icon: ReactNode;
title: string;
}
export const DialogWrapper = ({
children,
icon,
title,
}: DialogWrapperProps) => {
return (
<div className="flex gap-12 max-w-full text-ui">
<div className="pt-8 fill-current">{icon}</div>
<div className="flex-1">
<h1
data-testid="dialog-title"
className="text-h4 text-black dark:text-white capitalize mb-12"
>
{title}
</h1>
{children}
</div>
</div>
<Dialog
open={dialogOpen}
onChange={onDialogChange}
intent={intent}
title={title}
icon={icon}
>
{children}
</Dialog>
);
};
@ -118,6 +102,12 @@ const getProps = (
intent: Intent.None,
children: <Step>Awaiting transaction</Step>,
},
[VegaTxStatus.Complete]: {
title: t('Withdrawal transaction complete'),
icon: <Icon name="tick" />,
intent: Intent.Success,
children: <Step>Withdrawal created</Step>,
},
};
const completeProps = {