diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts index 6f20e96ec..f467d0652 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts @@ -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) => { + 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' ); diff --git a/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts b/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts index a029d366f..f9383bb90 100644 --- a/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts +++ b/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts @@ -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 ) diff --git a/libs/deal-ticket/src/components/deal-ticket-manager.tsx b/libs/deal-ticket/src/components/deal-ticket-manager.tsx index b202f6c6f..76dbd130d 100644 --- a/libs/deal-ticket/src/components/deal-ticket-manager.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-manager.tsx @@ -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 || ( submit(order)} transactionStatus={ transaction.status === VegaTxStatus.Requested || transaction.status === VegaTxStatus.Pending @@ -45,15 +33,68 @@ export const DealTicketManager = ({ } /> )} - + intent={getDialogIntent(finalizedOrder?.status)} + icon={getDialogIcon(finalizedOrder?.status)} + > + + ); }; + +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 ; + case OrderStatus.Rejected: + case OrderStatus.Stopped: + case OrderStatus.Cancelled: + return ; + default: + return; + } +}; diff --git a/libs/orders/src/lib/components/index.ts b/libs/orders/src/lib/components/index.ts index dff9b29da..993d9042a 100644 --- a/libs/orders/src/lib/components/index.ts +++ b/libs/orders/src/lib/components/index.ts @@ -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'; diff --git a/libs/orders/src/lib/components/mocks/generate-orders.ts b/libs/orders/src/lib/components/mocks/generate-orders.ts index b28b5eb40..3e41ee971 100644 --- a/libs/orders/src/lib/components/mocks/generate-orders.ts +++ b/libs/orders/src/lib/components/mocks/generate-orders.ts @@ -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 -) => - 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 +) => { + 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', diff --git a/libs/orders/src/lib/components/order-feedback/index.ts b/libs/orders/src/lib/components/order-feedback/index.ts new file mode 100644 index 000000000..cfb0f1b36 --- /dev/null +++ b/libs/orders/src/lib/components/order-feedback/index.ts @@ -0,0 +1 @@ +export * from './order-feedback'; diff --git a/libs/orders/src/lib/components/order-feedback/order-feedback.spec.tsx b/libs/orders/src/lib/components/order-feedback/order-feedback.spec.tsx new file mode 100644 index 000000000..aeac88fff --- /dev/null +++ b/libs/orders/src/lib/components/order-feedback/order-feedback.spec.tsx @@ -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(); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders error reason', () => { + const orderFields = { + status: OrderStatus.Rejected, + rejectionReason: OrderRejectionReason.OrderAmendFailure, + }; + const order = generateOrder(orderFields); + render(); + 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(); + 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` + ); + }); +}); diff --git a/libs/orders/src/lib/components/order-feedback/order-feedback.tsx b/libs/orders/src/lib/components/order-feedback/order-feedback.tsx new file mode 100644 index 000000000..3ab9c483e --- /dev/null +++ b/libs/orders/src/lib/components/order-feedback/order-feedback.tsx @@ -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 ( +

+ {order.rejectionReason && + t(`Reason: ${formatLabel(order.rejectionReason)}`)} +

+ ); + } + + if (order.status === OrderStatus.Cancelled) { + return ( +
+
+ {order.market && ( +
+

{t(`Market`)}

+

{t(`${order.market.name}`)}

+
+ )} +
+
+ {transaction.txHash && ( +
+

{t('Transaction')}

+ + {transaction.txHash} + +
+ )} +
+
+ ); + } + + return ( +
+
+ {order.market && ( +
+

{t(`Market`)}

+

{t(`${order.market.name}`)}

+
+ )} +
+

{t(`Status`)}

+

{t(`${order.status}`)}

+
+ {order.type === OrderType.Limit && order.market && ( +
+

{t(`Price`)}

+

+ {addDecimalsFormatNumber(order.price, order.market.decimalPlaces)} +

+
+ )} +
+

{t(`Amount`)}

+

+ {`${order.side === Side.Buy ? '+' : '-'} ${addDecimalsFormatNumber( + order.size, + order.market?.positionDecimalPlaces ?? 0 + )} + `} +

+
+
+
+ {transaction.txHash && ( +
+

{t('Transaction')}

+ + {transaction.txHash} + +
+ )} +
+
+ ); +}; diff --git a/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx b/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx index 94bf5fd76..4e378bf8b 100644 --- a/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx +++ b/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx @@ -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; + 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 ( - }> + } + >
{order.market && (
@@ -49,7 +63,7 @@ export const OrderEditDialog = ({ )} {order.type === OrderType.Limit && order.market && (
-

{t(`Last price`)}

+

{t(`Current price`)}

{addDecimalsFormatNumber(order.price, order.market.decimalPlaces)}

@@ -71,15 +85,7 @@ export const OrderEditDialog = ({
-
{ - await edit({ - ...order, - price: data.entryPrice, - }); - })} - data-testid="edit-order" - > +
- +
); }; diff --git a/libs/orders/src/lib/components/order-list/order-list.stories.tsx b/libs/orders/src/lib/components/order-list/order-list.stories.tsx index 8fbc6ad10..5853e3a85 100644 --- a/libs/orders/src/lib/components/order-list/order-list.stories.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.stories.tsx @@ -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) => { { - return; - }} setEditOrder={() => { return; }} @@ -31,47 +29,43 @@ const Template: Story = (args) => { const Template2: Story = (args) => { const [open, setOpen] = useState(false); + const [editOrder, setEditOrder] = useState(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 ( <>
{ - return; - }} - setEditOrder={() => { - return; + setEditOrder={(order) => { + setEditOrder(order); }} />
+ { + if (!isOpen) setEditOrder(null); + }} + order={editOrder} + onSubmit={(fields) => { + return; + }} /> ); diff --git a/libs/orders/src/lib/components/order-list/order-list.tsx b/libs/orders/src/lib/components/order-list/order-list.tsx index 2593872ec..c3963e0bb 100644 --- a/libs/orders/src/lib/components/order-list/order-list.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.tsx @@ -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( (props, ref) => { - const [cancelOrderDialogOpen, setCancelOrderDialogOpen] = useState(false); - const [editOrderDialogOpen, setEditOrderDialogOpen] = useState(false); - const [editOrder, setEditOrder] = - useState(null); + const [editOrder, setEditOrder] = useState(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 ( <> { + if (!order.market) return; + orderCancel.cancel({ + orderId: order.id, + marketId: order.market.id, + }); + }} ref={ref} - setEditOrderDialogOpen={setEditOrderDialogOpen} setEditOrder={setEditOrder} /> - - - - + + + + + { + 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; - 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( - ({ cancel, setEditOrderDialogOpen, setEditOrder, ...props }, ref) => { + ({ cancel, setEditOrder, ...props }, ref) => { return ( ( { - if ( - ![ - OrderStatus.Cancelled, - OrderStatus.Rejected, - OrderStatus.Expired, - OrderStatus.Filled, - OrderStatus.Stopped, - ].includes(data.status) - ) { + if (!data) return null; + if (isOrderActive(data.status)) { return ( ); } + return null; }} /> { - if ( - ![ - OrderStatus.Cancelled, - OrderStatus.Rejected, - OrderStatus.Expired, - OrderStatus.Filled, - OrderStatus.Stopped, - ].includes(data.status) - ) { + if (!data) return null; + if (isOrderActive(data.status)) { return ( - ); } + return null; }} /> @@ -315,3 +276,61 @@ export const OrderListTable = forwardRef( ); } ); + +/** + * 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'); + } +}; diff --git a/libs/orders/src/lib/index.ts b/libs/orders/src/lib/index.ts index c5b6ba5bb..35daed11d 100644 --- a/libs/orders/src/lib/index.ts +++ b/libs/orders/src/lib/index.ts @@ -1,4 +1,3 @@ export * from './components'; export * from './order-hooks'; export * from './utils'; -export * from './market'; diff --git a/libs/orders/src/lib/market.ts b/libs/orders/src/lib/market.ts deleted file mode 100644 index 381af1d46..000000000 --- a/libs/orders/src/lib/market.ts +++ /dev/null @@ -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; -} diff --git a/libs/orders/src/lib/order-hooks/__generated__/OrderEvent.ts b/libs/orders/src/lib/order-hooks/__generated__/OrderEvent.ts index d633ec796..807c70496 100644 --- a/libs/orders/src/lib/order-hooks/__generated__/OrderEvent.ts +++ b/libs/orders/src/lib/order-hooks/__generated__/OrderEvent.ts @@ -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 */ diff --git a/libs/orders/src/lib/order-hooks/order-event-query.ts b/libs/orders/src/lib/order-hooks/order-event-query.ts index 8b07cb111..33059f125 100644 --- a/libs/orders/src/lib/order-hooks/order-event-query.ts +++ b/libs/orders/src/lib/order-hooks/order-event-query.ts @@ -20,6 +20,7 @@ export const ORDER_EVENT_SUB = gql` id name decimalPlaces + positionDecimalPlaces } } } diff --git a/libs/orders/src/lib/order-hooks/use-order-cancel.spec.tsx b/libs/orders/src/lib/order-hooks/use-order-cancel.spec.tsx index 45584650b..3991f91f0 100644 --- a/libs/orders/src/lib/order-hooks/use-order-cancel.spec.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-cancel.spec.tsx @@ -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, }); }); }); diff --git a/libs/orders/src/lib/order-hooks/use-order-cancel.tsx b/libs/orders/src/lib/order-hooks/use-order-cancel.tsx index 12e5222f7..dd8439eeb 100644 --- a/libs/orders/src/lib/order-hooks/use-order-cancel.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-cancel.tsx @@ -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(null); - const client = useApolloClient(); - const subRef = useRef(null); + const waitForOrderEvent = useOrderEvent(); - useEffect(() => { - return () => { - subRef.current?.unsubscribe(); - setUpdatedOrder(null); - resetTransaction(); - }; - }, [resetTransaction]); + const [cancelledOrder, setCancelledOrder] = + useState(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({ - 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, }; diff --git a/libs/orders/src/lib/order-hooks/use-order-edit.spec.tsx b/libs/orders/src/lib/order-hooks/use-order-edit.spec.tsx index 9676b7494..9f27edba1 100644 --- a/libs/orders/src/lib/order-hooks/use-order-edit.spec.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-edit.spec.tsx @@ -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) { +function setup(order: OrderFields, context?: Partial) { const mocks: MockedResponse = { request: { query: ORDER_EVENT_SUB, @@ -119,86 +108,47 @@ function setup(context?: Partial) { ); - 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, diff --git a/libs/orders/src/lib/order-hooks/use-order-edit.tsx b/libs/orders/src/lib/order-hooks/use-order-edit.tsx index 3fde8a2a8..2e1a9e3d0 100644 --- a/libs/orders/src/lib/order-hooks/use-order-edit.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-edit.tsx @@ -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(null); - const client = useApolloClient(); - const subRef = useRef(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({ - 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, }; diff --git a/libs/orders/src/lib/order-hooks/use-order-event.ts b/libs/orders/src/lib/order-hooks/use-order-event.ts new file mode 100644 index 000000000..e7b257a75 --- /dev/null +++ b/libs/orders/src/lib/order-hooks/use-order-event.ts @@ -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(null); + + const waitForOrderEvent = useCallback( + ( + id: string, + partyId: string, + callback: (order: OrderEvent_busEvents_event_Order) => void + ) => { + subRef.current = client + .subscribe({ + 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; +}; diff --git a/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx b/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx index e981c1df5..2df7ca0ae 100644 --- a/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx @@ -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, }, }); }); diff --git a/libs/orders/src/lib/order-hooks/use-order-submit.ts b/libs/orders/src/lib/order-hooks/use-order-submit.ts index aa92e7dbb..68b0828da 100644 --- a/libs/orders/src/lib/order-hooks/use-order-submit.ts +++ b/libs/orders/src/lib/order-hooks/use-order-submit.ts @@ -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(null); - const client = useApolloClient(); - const subRef = useRef(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({ - 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, }; diff --git a/libs/orders/src/lib/order-hooks/use-order-validation.tsx b/libs/orders/src/lib/order-hooks/use-order-validation.tsx index ed4585b52..e85a006e4 100644 --- a/libs/orders/src/lib/order-hooks/use-order-validation.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-validation.tsx @@ -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; @@ -34,7 +37,7 @@ export const useOrderValidation = ({ fieldErrors = {}, orderType, orderTimeInForce, -}: ValidationProps) => { +}: ValidationArgs) => { const { keypair } = useVegaWallet(); const { message, isDisabled } = useMemo(() => { diff --git a/libs/orders/src/lib/utils/get-default-order.ts b/libs/orders/src/lib/utils/get-default-order.ts index 5ea2e99c3..c3229704b 100644 --- a/libs/orders/src/lib/utils/get-default-order.ts +++ b/libs/orders/src/lib/utils/get-default-order.ts @@ -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, }); diff --git a/libs/react-helpers/src/index.ts b/libs/react-helpers/src/index.ts index e98c785b4..75f83254e 100644 --- a/libs/react-helpers/src/index.ts +++ b/libs/react-helpers/src/index.ts @@ -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'; diff --git a/libs/react-helpers/src/lib/time.ts b/libs/react-helpers/src/lib/time.ts new file mode 100644 index 000000000..afc177e7f --- /dev/null +++ b/libs/react-helpers/src/lib/time.ts @@ -0,0 +1,3 @@ +export const toNanoSeconds = (date: Date) => { + return date.getTime().toString() + '000000'; +}; diff --git a/libs/types/src/__generated__/globalTypes.ts b/libs/types/src/__generated__/globalTypes.ts index 7ef4cc642..aaf291df8 100644 --- a/libs/types/src/__generated__/globalTypes.ts +++ b/libs/types/src/__generated__/globalTypes.ts @@ -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 //============================================================== diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index f01cb9e4c..a1cf99672 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -1,3 +1,2 @@ export * from './__generated__'; export * from './candle'; -export * from './pagination'; diff --git a/libs/types/src/pagination.ts b/libs/types/src/pagination.ts deleted file mode 100644 index a178c09a7..000000000 --- a/libs/types/src/pagination.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Pagination { - first?: number; - after?: string; - last?: number; - before?: string; -} diff --git a/libs/ui-toolkit/src/components/dialog/dialog.tsx b/libs/ui-toolkit/src/components/dialog/dialog.tsx index fd01f9ff7..ba1d8aeb2 100644 --- a/libs/ui-toolkit/src/components/dialog/dialog.tsx +++ b/libs/ui-toolkit/src/components/dialog/dialog.tsx @@ -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" /> - {title && ( -

- {title} -

- )} - {children} +
+ {icon &&
{icon}
} +
+ {title && ( +

+ {title} +

+ )} +
{children}
+
+
diff --git a/libs/wallet/src/use-vega-transaction.spec.tsx b/libs/wallet/src/use-vega-transaction.spec.tsx index 59d115990..93532ddc9 100644 --- a/libs/wallet/src/use-vega-transaction.spec.tsx +++ b/libs/wallet/src/use-vega-transaction.spec.tsx @@ -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); -}); diff --git a/libs/wallet/src/use-vega-transaction.ts b/libs/wallet/src/use-vega-transaction.tsx similarity index 64% rename from libs/wallet/src/use-vega-transaction.ts rename to libs/wallet/src/use-vega-transaction.tsx index 011dee781..2a79bb97b 100644 --- a/libs/wallet/src/use-vega-transaction.ts +++ b/libs/wallet/src/use-vega-transaction.tsx @@ -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) => ( + { + if (!isOpen) reset(); + setTransaction({ dialogOpen: isOpen }); + }} + transaction={transaction} + /> + ); + }, [transaction, setTransaction, reset]); + + return { + send, + transaction, + reset, + setComplete, + TransactionDialog, + }; }; diff --git a/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.spec.tsx b/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.spec.tsx index 50cfe0655..01c3a4b90 100644 --- a/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.spec.tsx +++ b/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.spec.tsx @@ -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(); - 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( - - ); - 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( - - ); - 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( - - ); - 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( - - ); - 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( - - ); - 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( - - ); - 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( - - ); - 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(); + expect(screen.getByTestId('dialog-title')).toHaveTextContent(/confirm/i); + expect(screen.getByTestId(VegaTxStatus.Requested)).toHaveTextContent( + /please open your wallet/i + ); + }); + + it('pending', () => { + render( + + ); + expect(screen.getByTestId('dialog-title')).toHaveTextContent(/awaiting/i); + expect(screen.getByTestId(VegaTxStatus.Pending)).toHaveTextContent( + /please wait/i + ); + testBlockExplorerLink('tx-hash'); + }); + + it('error', () => { + render( + + ); + expect(screen.getByTestId('dialog-title')).toHaveTextContent(/failed/i); + expect(screen.getByTestId(VegaTxStatus.Error)).toHaveTextContent( + /rejected/i + ); + }); + + it('default complete', () => { + render( + + ); + expect(screen.getByTestId('dialog-title')).toHaveTextContent(/complete/i); + expect(screen.getByTestId(VegaTxStatus.Complete)).toHaveTextContent( + /confirmed/i + ); + testBlockExplorerLink('tx-hash'); + }); + + it('custom complete', () => { + render( + +
Custom content
+
+ ); + 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}` + ); + } }); diff --git a/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.tsx b/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.tsx index 7f142d263..cbc1bebca 100644 --- a/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.tsx +++ b/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.tsx @@ -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 + ) : ( + + ); return ( { - setOrderDialogOpen(isOpen); - - // If closing reset - if (!isOpen) { - reset(); - } - }} - intent={getDialogIntent(finalizedOrder, transaction)} + open={isOpen} + onChange={onChange} + intent={computedIntent} + title={computedTitle} + icon={computedIcon} > - + {content} ); }; 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
{children}
; - } - - // Rejected by wallet if (transaction.status === VegaTxStatus.Requested) { return ( - } - > -

- {t( - 'Please open your wallet application and confirm or reject the transaction' - )} -

-
+

+ {t( + 'Please open your wallet application and confirm or reject the transaction' + )} +

); } - // Transaction error if (transaction.status === VegaTxStatus.Error) { return ( - } - > +
{transaction.error && (
             {get(transaction.error, 'error') ??
               JSON.stringify(transaction.error, null, 2)}
           
)} - +
); } - // Pending consensus - if (!finalizedOrder) { + if (transaction.status === VegaTxStatus.Pending) { return ( - } - > - {transaction.txHash && ( -

- {t('Waiting for few more blocks')} -   +

+

+ {t('Please wait for your transaction to be confirmed')} -   + {transaction.txHash && ( {t('View in block explorer')} -

- )} - - ); - } - - // Order on network but was rejected - if (finalizedOrder.status === 'Rejected') { - return ( - } - > -

- {finalizedOrder.rejectionReason && - t(`Reason: ${formatLabel(finalizedOrder.rejectionReason)}`)} + )}

-
+
); } - return ( - }> -
- {finalizedOrder.market && ( -
-

{t(`Market`)}

-

{t(`${finalizedOrder.market.name}`)}

-
- )} -
-

{t(`Status`)}

-

{t(`${finalizedOrder.status}`)}

-
- {finalizedOrder.type === OrderType.Limit && finalizedOrder.market && ( -
-

{t(`Price`)}

-

- {addDecimalsFormatNumber( - finalizedOrder.price, - finalizedOrder.market.decimalPlaces - )} -

-
- )} -
-

{t(`Amount`)}

-

- {`${finalizedOrder.side === 'Buy' ? '+' : '-'} ${ - finalizedOrder.size - } - `} -

-
-
-
- {transaction.txHash && ( -
-

{t(`Transaction`)}

+ if (transaction.status === VegaTxStatus.Complete) { + return ( +
+

+ {t('Your transaction has been confirmed')} -   + {transaction.txHash && ( - {transaction.txHash} + {t('View in block explorer')} -

- )} + )} +

- - ); + ); + } + + 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 ( -
-
{icon}
-
-

- {title} -

- {children} -
-
- ); +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 ; + case VegaTxStatus.Pending: + return ; + case VegaTxStatus.Error: + return ; + case VegaTxStatus.Complete: + return ; + default: + return ''; + } }; diff --git a/libs/wallet/src/wallet-types.ts b/libs/wallet/src/wallet-types.ts index 480e1eb4b..a09fab938 100644 --- a/libs/wallet/src/wallet-types.ts +++ b/libs/wallet/src/wallet-types.ts @@ -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; -} diff --git a/libs/web3/src/lib/transaction-dialog/dialog-wrapper.tsx b/libs/web3/src/lib/transaction-dialog/dialog-wrapper.tsx deleted file mode 100644 index dfb5ddc74..000000000 --- a/libs/web3/src/lib/transaction-dialog/dialog-wrapper.tsx +++ /dev/null @@ -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 ( -
-
{icon}
-
-

{title}

-
{children}
-
-
- ); -}; diff --git a/libs/web3/src/lib/transaction-dialog/transaction-dialog.tsx b/libs/web3/src/lib/transaction-dialog/transaction-dialog.tsx index 23e1a6d47..d928515d7 100644 --- a/libs/web3/src/lib/transaction-dialog/transaction-dialog.tsx +++ b/libs/web3/src/lib/transaction-dialog/transaction-dialog.tsx @@ -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 ( - - {renderContent()} + + {renderContent()} ); }; diff --git a/libs/withdraws/src/lib/withdraw-dialog.tsx b/libs/withdraws/src/lib/withdraw-dialog.tsx index 90ef33f44..4c4845732 100644 --- a/libs/withdraws/src/lib/withdraw-dialog.tsx +++ b/libs/withdraws/src/lib/withdraw-dialog.tsx @@ -25,38 +25,22 @@ export const WithdrawDialog = ({ onDialogChange, }: WithdrawDialogProps) => { const { ETHERSCAN_URL } = useEnvironment(); - const { intent, ...props } = getProps(approval, vegaTx, ethTx, ETHERSCAN_URL); - return ( - - - + 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 ( -
-
{icon}
-
-

- {title} -

- {children} -
-
+ + {children} + ); }; @@ -118,6 +102,12 @@ const getProps = ( intent: Intent.None, children: Awaiting transaction, }, + [VegaTxStatus.Complete]: { + title: t('Withdrawal transaction complete'), + icon: , + intent: Intent.Success, + children: Withdrawal created, + }, }; const completeProps = {