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:
parent
19a79afcc9
commit
a5f9ed90e8
@ -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'
|
||||
);
|
||||
|
@ -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 )
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
|
1
libs/orders/src/lib/components/order-feedback/index.ts
Normal file
1
libs/orders/src/lib/components/order-feedback/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './order-feedback';
|
@ -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`
|
||||
);
|
||||
});
|
||||
});
|
116
libs/orders/src/lib/components/order-feedback/order-feedback.tsx
Normal file
116
libs/orders/src/lib/components/order-feedback/order-feedback.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from './components';
|
||||
export * from './order-hooks';
|
||||
export * from './utils';
|
||||
export * from './market';
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -20,6 +20,7 @@ export const ORDER_EVENT_SUB = gql`
|
||||
id
|
||||
name
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
59
libs/orders/src/lib/order-hooks/use-order-event.ts
Normal file
59
libs/orders/src/lib/order-hooks/use-order-event.ts
Normal 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;
|
||||
};
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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(() => {
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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';
|
||||
|
3
libs/react-helpers/src/lib/time.ts
Normal file
3
libs/react-helpers/src/lib/time.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const toNanoSeconds = (date: Date) => {
|
||||
return date.getTime().toString() + '000000';
|
||||
};
|
10
libs/types/src/__generated__/globalTypes.ts
generated
10
libs/types/src/__generated__/globalTypes.ts
generated
@ -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
|
||||
//==============================================================
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from './__generated__';
|
||||
export * from './candle';
|
||||
export * from './pagination';
|
||||
|
@ -1,6 +0,0 @@
|
||||
export interface Pagination {
|
||||
first?: number;
|
||||
after?: string;
|
||||
last?: number;
|
||||
before?: string;
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -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')} -
|
||||
<div data-testid={transaction.status}>
|
||||
<p className="break-all">
|
||||
{t('Please wait for your transaction to be confirmed')} -
|
||||
{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')} -
|
||||
{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 '';
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user