Compare commits

...
Author SHA1 Message Date
Matthew Russell 8b2ec489d8 feat: improve types for order transactions and dialogs 2022-07-22 11:43:06 +01:00
17 changed files with 438 additions and 466 deletions
@@ -1,13 +1,13 @@
import type { UseFormRegister } from 'react-hook-form';
import { VegaWalletOrderType } from '@vegaprotocol/wallet';
import type { Order } from '@vegaprotocol/orders';
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
import type { OrderSubmitInput } from '@vegaprotocol/orders';
export interface DealTicketAmountProps {
orderType: VegaWalletOrderType;
step: number;
register: UseFormRegister<Order>;
register: UseFormRegister<OrderSubmitInput>;
quoteName: string;
price?: string;
}
@@ -1,10 +1,16 @@
import type { ReactNode } from 'react';
import { useEffect } from 'react';
import { useState } from 'react';
import type { VegaTxState } from '@vegaprotocol/wallet';
import { VegaTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet';
import { DealTicket } from './deal-ticket';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import type { OrderEvent_busEvents_event_Order } from '@vegaprotocol/orders';
import { useOrderSubmit } from '@vegaprotocol/orders';
import { OrderStatus } from '@vegaprotocol/types';
import { OrderStatus, OrderType } from '@vegaprotocol/types';
import { Icon, Intent } from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import { useEnvironment } from '@vegaprotocol/environment';
export interface DealTicketManagerProps {
market: DealTicketQuery_market;
@@ -17,26 +23,41 @@ export const DealTicketManager = ({
}: 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';
let title;
let intent;
let icon;
let dialogContent;
if (finalizedOrder) {
title = getDialogTitle(finalizedOrder);
if (finalizedOrder.status === OrderStatus.Rejected) {
icon = <Icon name="cross" size={20} />;
intent = Intent.Danger;
dialogContent = (
<p data-testid="error-reason">
{t(`Reason: ${finalizedOrder.rejectionReason}`)}
</p>
);
}
};
icon = <Icon name="tick" size={20} />;
intent = Intent.Success;
dialogContent = (
<OrderSuccessContent order={finalizedOrder} transaction={transaction} />
);
}
return (
<>
{children || (
<DealTicket
market={market}
submit={submit}
submit={(order) => {
setOrderDialogOpen(true);
submit(order);
}}
transactionStatus={
transaction.status === VegaTxStatus.Requested ||
transaction.status === VegaTxStatus.Pending
@@ -45,15 +66,101 @@ export const DealTicketManager = ({
}
/>
)}
<VegaTransactionDialog
key={`submit-order-dialog-${transaction.txHash}`}
orderDialogOpen={orderDialogOpen}
setOrderDialogOpen={setOrderDialogOpen}
finalizedOrder={finalizedOrder}
isOpen={orderDialogOpen}
onChange={(isOpen) => {
setOrderDialogOpen(isOpen);
if (!isOpen) {
reset();
}
}}
transaction={transaction}
reset={reset}
title={getDialogTitle(finalizedOrder?.status)}
/>
intent={intent}
title={title}
icon={icon}
>
{dialogContent}
</VegaTransactionDialog>
</>
);
};
const getDialogTitle = (finalizedOrder: OrderEvent_busEvents_event_Order) => {
switch (finalizedOrder.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 undefined;
}
};
interface OrderSuccessContentProps {
order: OrderEvent_busEvents_event_Order;
transaction: VegaTxState;
}
const OrderSuccessContent = ({
order,
transaction,
}: OrderSuccessContentProps) => {
const headerClassName = 'text-h5 font-bold text-black dark:text-white';
const { VEGA_EXPLORER_URL } = useEnvironment();
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{order.market && (
<div>
<p className={headerClassName}>{t(`Market`)}</p>
<p>{t(`${order.market.name}`)}</p>
</div>
)}
<div>
<p className={headerClassName}>{t(`Status`)}</p>
<p>{t(`${order.status}`)}</p>
</div>
{order.type === OrderType.Limit && order.market && (
<div>
<p className={headerClassName}>{t(`Price`)}</p>
<p>
{addDecimalsFormatNumber(order.price, order.market.decimalPlaces)}
</p>
</div>
)}
<div>
<p className={headerClassName}>{t(`Amount`)}</p>
<p
className={
order.side === 'Buy' ? 'text-vega-green' : 'text-vega-red'
}
>
{`${order.side === 'Buy' ? '+' : '-'} ${order.size}
`}
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-8">
{transaction.txHash && (
<div>
<p className={headerClassName}>{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>
</>
);
};
@@ -16,16 +16,16 @@ import { DealTicketAmount } from './deal-ticket-amount';
import { TimeInForceSelector } from './time-in-force-selector';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import { ExpirySelector } from './expiry-selector';
import type { Order } from '@vegaprotocol/orders';
import type { OrderSubmitInput } from '@vegaprotocol/orders';
import { getDefaultOrder, useOrderValidation } from '@vegaprotocol/orders';
export type TransactionStatus = 'default' | 'pending';
export interface DealTicketProps {
market: DealTicketQuery_market;
submit: (order: Order) => void;
submit: (order: OrderSubmitInput) => void;
transactionStatus: TransactionStatus;
defaultOrder?: Order;
defaultOrder?: OrderSubmitInput;
}
export const DealTicket = ({
@@ -39,7 +39,7 @@ export const DealTicket = ({
handleSubmit,
watch,
formState: { errors },
} = useForm<Order>({
} = useForm<OrderSubmitInput>({
mode: 'onChange',
defaultValues: getDefaultOrder(market),
});
@@ -57,7 +57,7 @@ export const DealTicket = ({
const isDisabled = transactionStatus === 'pending' || disabled;
const onSubmit = useCallback(
(order: Order) => {
(order: OrderSubmitInput) => {
if (!isDisabled) {
submit(order);
}
@@ -6,25 +6,18 @@ import {
import { OrderType } from '@vegaprotocol/types';
import { FormGroup, Input, InputError, Button } 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 { Orders_party_orders } from '../__generated__';
interface OrderEditDialogProps {
title: string;
order: Order | null;
edit: (body: Order) => Promise<unknown>;
order: Orders_party_orders;
onSubmit: (order: Orders_party_orders) => void;
}
interface FormFields {
entryPrice: string;
}
export const OrderEditDialog = ({
order,
title,
edit,
}: OrderEditDialogProps) => {
export const OrderEditDialog = ({ order, onSubmit }: OrderEditDialogProps) => {
const headerClassName = 'text-h5 font-bold text-black dark:text-white';
const {
register,
@@ -39,7 +32,7 @@ export const OrderEditDialog = ({
});
if (!order) return null;
return (
<OrderDialogWrapper title={title} icon={<Icon name="hand-up" size={20} />}>
<div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{order.market && (
<div>
@@ -69,11 +62,10 @@ export const OrderEditDialog = ({
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 py-12">
<form
onSubmit={handleSubmit(async (data) => {
await edit({
onSubmit={handleSubmit((data) => {
onSubmit({
...order,
price: data.entryPrice,
});
@@ -97,6 +89,6 @@ export const OrderEditDialog = ({
</Button>
</form>
</div>
</OrderDialogWrapper>
</div>
);
};
@@ -1,8 +1,7 @@
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';
@@ -18,9 +17,6 @@ const Template: Story = (args) => {
<OrderListTable
rowData={args.data}
cancel={cancel}
setEditOrderDialogOpen={() => {
return;
}}
setEditOrder={() => {
return;
}}
@@ -41,36 +37,22 @@ const Template2: Story = (args) => {
txHash: null,
signature: null,
};
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;
}}
/>
</div>
<VegaTransactionDialog
orderDialogOpen={open}
setOrderDialogOpen={setOpen}
finalizedOrder={finalizedOrder}
isOpen={open}
onChange={setOpen}
transaction={transaction}
reset={reset}
title={'Order cancelled'}
/>
</>
@@ -5,99 +5,98 @@ import {
getDateTimeFormat,
t,
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid, Button } from '@vegaprotocol/ui-toolkit';
import {
AgGridDynamic as AgGrid,
Button,
Dialog,
} from '@vegaprotocol/ui-toolkit';
import type {
ICellRendererParams,
ValueFormatterParams,
} from 'ag-grid-community';
import type {
AgGridReact,
AgGridReactProps,
AgReactUiProps,
} from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { forwardRef, useState } from 'react';
import type { Orders_party_ordersConnection_edges_node } from '../';
import BigNumber from 'bignumber.js';
import type { OrderCancelInput } from '../../order-hooks/use-order-cancel';
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';
type OrderListProps = AgGridReactProps | AgReactUiProps;
interface OrderListProps {
data: Orders_party_ordersConnection_edges_node[] | null;
}
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
(props, ref) => {
({ data }, ref) => {
// Cancel transaction dialog
const [cancelOrderDialogOpen, setCancelOrderDialogOpen] = useState(false);
// Edit order transaction dialog
const [editOrderDialogOpen, setEditOrderDialogOpen] = useState(false);
// Order for edit order dialog
const [editOrder, setEditOrder] =
useState<Orders_party_ordersConnection_edges_node | null>(null);
const { transaction, updatedOrder, reset, cancel } = useOrderCancel();
const { transaction, 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`
);
// const getEditDialogTitle = () =>
// editedOrder
// ? t(
// `Order ${
// editOrder?.market?.tradableInstrument.instrument.code ?? ''
// } updated`
// )
// : t(
// `Edit ${
// editOrder?.market?.tradableInstrument.instrument.code ?? ''
// } order`
// );
return (
<>
<OrderListTable
{...props}
cancel={cancel}
data={data}
cancel={(order) => {
setCancelOrderDialogOpen(true);
cancel(order);
}}
ref={ref}
setEditOrderDialogOpen={setEditOrderDialogOpen}
setEditOrder={setEditOrder}
/>
<VegaTransactionDialog
key={`cancel-order-dialog-${transaction.txHash}`}
orderDialogOpen={cancelOrderDialogOpen}
setOrderDialogOpen={setCancelOrderDialogOpen}
isOpen={cancelOrderDialogOpen}
onChange={(isOpen) => {
setCancelOrderDialogOpen(isOpen);
if (!isOpen) {
reset();
}
}}
transaction={transaction}
reset={reset}
title={getCancelDialogTitle(updatedOrder?.status)}
finalizedOrder={updatedOrder}
/>
<VegaTransactionDialog
key={`edit-order-dialog-${transaction.txHash}`}
orderDialogOpen={editOrderDialogOpen}
setOrderDialogOpen={setEditOrderDialogOpen}
isOpen={editOrderDialogOpen}
onChange={(isOpen) => {
setEditOrderDialogOpen(isOpen);
if (!isOpen) {
resetEdit();
}
}}
transaction={editTransaction}
reset={resetEdit}
title={getEditDialogTitle()}
finalizedOrder={editedOrder}
>
<OrderEditDialog
title={getEditDialogTitle()}
order={editOrder}
edit={edit}
/>
</VegaTransactionDialog>
/>
<EditOrderDialog
editOrder={editOrder}
setEditOrder={setEditOrder}
onSubmit={(order) => {
setEditOrder(null);
setEditOrderDialogOpen(true);
// @ts-ignore fix me, market is null
edit(order);
}}
/>
</>
);
}
@@ -109,17 +108,16 @@ type OrderListTableValueFormatterParams = Omit<
> & {
data: Orders_party_ordersConnection_edges_node | null;
};
type OrderListTableProps = (AgGridReactProps | AgReactUiProps) & {
cancel: (body?: unknown) => Promise<unknown>;
setEditOrderDialogOpen: (value: boolean) => void;
interface OrderListTableProps {
data: Orders_party_ordersConnection_edges_node[] | null;
cancel: (order: OrderCancelInput) => void;
setEditOrder: (
order: Orders_party_ordersConnection_edges_node | null
) => void;
};
}
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
({ cancel, setEditOrderDialogOpen, setEditOrder, ...props }, ref) => {
({ data, cancel, setEditOrder }, ref) => {
return (
<AgGrid
ref={ref}
@@ -128,7 +126,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data.id}
rowHeight={40}
{...props}
>
<AgGridColumn
headerName={t('Market')}
@@ -274,7 +271,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
data-testid="edit"
variant="secondary"
onClick={() => {
setEditOrderDialogOpen(true);
setEditOrder(data);
}}
>
@@ -301,7 +297,10 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
<Button
data-testid="cancel"
onClick={async () => {
await cancel(data);
await cancel({
orderId: data.id,
marketId: data.market.id,
});
}}
>
Cancel
@@ -315,3 +314,23 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
);
}
);
interface EditOrderDialogProps {
editOrder: Orders_party_ordersConnection_edges_node | null;
setEditOrder: (
order: Orders_party_ordersConnection_edges_node | null
) => void;
onSubmit: (order: Orders_party_ordersConnection_edges_node) => void;
}
const EditOrderDialog = ({
editOrder,
setEditOrder,
onSubmit,
}: EditOrderDialogProps) => {
return (
<Dialog open={Boolean(editOrder)} onChange={() => setEditOrder(null)}>
{editOrder && <OrderEditDialog order={editOrder} onSubmit={onSubmit} />}
</Dialog>
);
};
-1
View File
@@ -1,4 +1,3 @@
export * from './components';
export * from './order-hooks';
export * from './utils';
export * from './market';
-13
View File
@@ -1,13 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { MarketState, MarketTradingMode } from '@vegaprotocol/types';
export interface Market {
__typename?: string;
id: string;
positionDecimalPlaces: number;
state: MarketState;
decimalPlaces: number;
tradingMode: MarketTradingMode;
tradableInstrument?: any;
depth?: any;
}
@@ -8,13 +8,22 @@ import type {
} from './__generated__/OrderEvent';
import { ORDER_EVENT_SUB } from './order-event-query';
import * as Sentry from '@sentry/react';
import { OrderStatus } from '@vegaprotocol/types';
import { determineId } from '@vegaprotocol/react-helpers';
import type { Subscription } from 'zen-observable-ts';
export type OrderCancelInput = {
orderId: string;
marketId: string;
};
export const useOrderCancel = () => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const {
send,
transaction,
setComplete,
reset: resetTransaction,
} = useVegaTransaction();
const [updatedOrder, setUpdatedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const client = useApolloClient();
@@ -35,23 +44,11 @@ export const useOrderCancel = () => {
}, [resetTransaction]);
const cancel = useCallback(
async (order) => {
async (order: OrderCancelInput) => {
if (!keypair) {
return;
}
if (
[
OrderStatus.Cancelled,
OrderStatus.Rejected,
OrderStatus.Expired,
OrderStatus.Filled,
OrderStatus.Stopped,
].includes(order.status)
) {
return;
}
setUpdatedOrder(null);
try {
@@ -59,13 +56,13 @@ export const useOrderCancel = () => {
pubKey: keypair.pub,
propagate: true,
orderCancellation: {
orderId: order.id,
marketId: order.market.id,
orderId: order.orderId,
marketId: order.marketId,
},
});
if (res?.signature) {
const resId = order.id ?? determineId(res.signature);
const resId = order.orderId ?? determineId(res.signature);
setUpdatedOrder(null);
if (resId) {
@@ -94,18 +91,18 @@ export const useOrderCancel = () => {
matchingOrderEvent.event.__typename === 'Order'
) {
setUpdatedOrder(matchingOrderEvent.event);
setComplete();
subRef.current?.unsubscribe();
}
});
}
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[client, keypair, send]
[client, keypair, send, setComplete]
);
return {
@@ -1,7 +1,6 @@
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';
@@ -12,10 +11,27 @@ import type {
OrderEventVariables,
} from './__generated__';
import * as Sentry from '@sentry/react';
import type { OrderTimeInForce } from '@vegaprotocol/types';
type OrderEditInput = {
id: string;
market: {
id: string;
decimalPlaces: number;
};
price: string;
timeInForce: OrderTimeInForce;
expiresAt?: string;
};
export const useOrderEdit = () => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const {
send,
transaction,
setComplete,
reset: resetTransaction,
} = useVegaTransaction();
const [updatedOrder, setUpdatedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const client = useApolloClient();
@@ -36,8 +52,8 @@ export const useOrderEdit = () => {
}, [resetTransaction]);
const edit = useCallback(
async (order: Order) => {
if (!keypair || !order.market || !order.market.id) {
async (order: OrderEditInput) => {
if (!keypair) {
return;
}
@@ -51,7 +67,7 @@ export const useOrderEdit = () => {
orderId: order.id,
marketId: order.market.id,
price: {
value: removeDecimal(order.price, order.market?.decimalPlaces),
value: removeDecimal(order.price, order.market.decimalPlaces),
},
timeInForce: VegaWalletOrderTimeInForce[order.timeInForce],
sizeDelta: 0,
@@ -96,18 +112,18 @@ export const useOrderEdit = () => {
matchingOrderEvent.event.__typename === 'Order'
) {
setUpdatedOrder(matchingOrderEvent.event);
setComplete();
subRef.current?.unsubscribe();
}
});
}
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[client, keypair, send]
[client, keypair, setComplete, send]
);
return {
@@ -1,22 +1,44 @@
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 type {
OrderEvent,
OrderEventVariables,
OrderEvent_busEvents_event_Order,
} from './__generated__';
import type {
VegaWalletOrderTimeInForce,
VegaWalletOrderSide,
} from '@vegaprotocol/wallet';
import { VegaWalletOrderType, useVegaWallet } from '@vegaprotocol/wallet';
import { determineId, removeDecimal } 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';
export const useOrderSubmit = (market: Market) => {
export type OrderSubmitInput = {
size: string;
type: VegaWalletOrderType.Market | VegaWalletOrderType.Limit;
timeInForce: VegaWalletOrderTimeInForce;
side: VegaWalletOrderSide;
price?: string;
expiration?: Date;
};
export type OrderSubmitMarket = {
id: string;
decimalPlaces: number;
positionDecimalPlaces: number;
};
export const useOrderSubmit = (market: OrderSubmitMarket) => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const {
send,
transaction,
setComplete,
reset: resetTransaction,
} = useVegaTransaction();
const [finalizedOrder, setFinalizedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const client = useApolloClient();
@@ -37,12 +59,13 @@ export const useOrderSubmit = (market: Market) => {
}, [resetTransaction]);
const submit = useCallback(
async (order: Order) => {
if (!keypair || !order.side) {
async (order: OrderSubmitInput) => {
if (!keypair) {
return;
}
setFinalizedOrder(null);
try {
const res = await send({
pubKey: keypair.pub,
@@ -57,11 +80,12 @@ export const useOrderSubmit = (market: Market) => {
type: order.type,
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'
: undefined,
expiresAt:
order.type === VegaWalletOrderType.Limit && 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'
: undefined,
},
});
@@ -93,6 +117,7 @@ export const useOrderSubmit = (market: Market) => {
matchingOrderEvent.event.__typename === 'Order'
) {
setFinalizedOrder(matchingOrderEvent.event);
setComplete();
subRef.current?.unsubscribe();
}
});
@@ -104,14 +129,7 @@ export const useOrderSubmit = (market: Market) => {
return;
}
},
[
client,
keypair,
send,
market.id,
market.decimalPlaces,
market.positionDecimalPlaces,
]
[client, keypair, send, market, setComplete]
);
return {
@@ -1,22 +1,27 @@
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 { OrderSubmitInput } from './use-order-submit';
export type ValidationProps = {
export type UseValidationArgs = {
step: number;
market: Market;
market: {
id: string;
decimalPlaces: number;
positionDecimalPlaces: number;
state: MarketState;
tradingMode: MarketTradingMode;
};
orderType: OrderType;
orderTimeInForce: OrderTimeInForce;
fieldErrors?: FieldErrors<Order>;
fieldErrors?: FieldErrors<OrderSubmitInput>;
};
export const marketTranslations = (marketState: MarketState) => {
@@ -34,7 +39,7 @@ export const useOrderValidation = ({
fieldErrors = {},
orderType,
orderTimeInForce,
}: ValidationProps) => {
}: UseValidationArgs) => {
const { keypair } = useVegaWallet();
const { message, isDisabled } = useMemo(() => {
+4 -29
View File
@@ -4,38 +4,13 @@ import {
VegaWalletOrderSide,
} from '@vegaprotocol/wallet';
import { toDecimal } from '@vegaprotocol/react-helpers';
import type { Market } from '../market';
import type { OrderStatus } from '@vegaprotocol/types';
import type { OrderSubmitInput, OrderSubmitMarket } from '../order-hooks';
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;
};
export const getDefaultOrder = (market: Market): Order => ({
export const getDefaultOrder = (
market: OrderSubmitMarket
): OrderSubmitInput => ({
type: VegaWalletOrderType.Market,
side: VegaWalletOrderSide.Buy,
timeInForce: VegaWalletOrderTimeInForce.IOC,
size: String(toDecimal(market.positionDecimalPlaces)),
rejectionReason: null,
market: null,
});
+6 -3
View File
@@ -8,8 +8,7 @@ export enum VegaTxStatus {
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 {
@@ -48,6 +47,10 @@ export const useVegaTransaction = () => {
setTransaction(initialState);
}, [setTransaction]);
const setComplete = useCallback(() => {
setTransaction({ status: VegaTxStatus.Complete });
}, [setTransaction]);
const send = useCallback(
async (tx: TransactionSubmission) => {
setTransaction({
@@ -91,5 +94,5 @@ export const useVegaTransaction = () => {
[sendTx, handleError, setTransaction, reset]
);
return { send, transaction, reset };
return { send, transaction, setComplete, reset };
};
@@ -1,155 +1,95 @@
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { useEffect } from 'react';
import get from 'lodash/get';
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 { 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;
intent?: Intent;
icon?: ReactNode;
children?: 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 = '',
title,
intent,
icon,
children,
}: VegaTransactionDialogProps) => {
// open / close dialog
useEffect(() => {
if (transaction.status !== VegaTxStatus.Default || finalizedOrder) {
setOrderDialogOpen(true);
} else {
setOrderDialogOpen(false);
}
}, [finalizedOrder, setOrderDialogOpen, transaction.status]);
const defaultProps = useDefaultProps(transaction);
return (
<Dialog
open={orderDialogOpen}
onChange={(isOpen) => {
setOrderDialogOpen(isOpen);
// If closing reset
if (!isOpen) {
reset();
}
}}
intent={getDialogIntent(finalizedOrder, transaction)}
open={isOpen}
onChange={onChange}
intent={intent || defaultProps.intent}
>
<VegaDialog
key={`${title.toLowerCase().split(' ').join('-')}-tx-${
transaction.txHash
}`}
transaction={transaction}
finalizedOrder={finalizedOrder}
title={title}
children={children}
/>
<DialogWrapper
title={title || defaultProps.title}
icon={icon || defaultProps.icon}
>
{children || defaultProps.content}
</DialogWrapper>
</Dialog>
);
};
interface VegaDialogProps {
transaction: VegaTxState;
finalizedOrder: Order | null;
interface DialogWrapperProps {
children: ReactNode;
icon: ReactNode;
title: string;
children?: ReactNode;
}
export const VegaDialog = ({
transaction,
finalizedOrder,
title,
export const DialogWrapper = ({
children,
}: VegaDialogProps) => {
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 capitalize mb-12">
{title}
</h1>
<div className="text-black-40 dark:text-white-40">{children}</div>
</div>
</div>
);
};
const useDefaultProps = (transaction: VegaTxState) => {
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>
);
}
// Transaction error
if (transaction.status === VegaTxStatus.Error) {
return (
<OrderDialogWrapper
title="Order rejected by wallet"
icon={<Icon name="warning-sign" size={20} />}
>
{transaction.error && (
<pre className="text-ui break-all whitespace-pre-wrap">
{get(transaction.error, 'error') ??
JSON.stringify(transaction.error, null, 2)}
</pre>
)}
</OrderDialogWrapper>
);
}
// Pending consensus
if (!finalizedOrder) {
return (
<OrderDialogWrapper
title="Awaiting network confirmation"
icon={<Loader size="small" />}
>
{transaction.txHash && (
switch (transaction.status) {
case VegaTxStatus.Requested:
return {
title: 'Confirm transaction in Vega wallet',
intent: Intent.Warning,
icon: <Icon name="hand-up" size={20} />,
content: (
<p>
{t(
'Please open your wallet application and confirm or reject the transaction'
)}
</p>
),
};
case VegaTxStatus.Pending:
return {
title: 'Awaiting transaction',
intent: Intent.None,
icon: <Loader size="small" />,
content: (
<p className="break-all">
{t('Waiting for few more blocks')} - &nbsp;
{t('Waiting for confirmation')} -{' '}
<a
className="underline"
data-testid="tx-block-explorer"
@@ -160,107 +100,46 @@ 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>
);
}
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>
),
};
case VegaTxStatus.Error:
return {
title: 'Transaction rejected by wallet',
icon: <Icon name="warning-sign" size={20} />,
intent: Intent.Danger,
content: (
<pre className="text-ui break-all whitespace-pre-wrap">
{get(transaction.error, 'error') ??
JSON.stringify(transaction.error, null, 2)}
</pre>
),
};
case VegaTxStatus.Complete:
return {
title: 'Transaction complete',
icon: <Icon name="tick" size={20} />,
intent: Intent.Success,
content: (
<p className="break-all">
{t('Transaction seen on chain')} -{' '}
<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>
)}
</div>
</OrderDialogWrapper>
);
};
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>
);
</p>
),
};
default:
return {
title: '',
icon: null,
intent: undefined,
content: null,
};
}
};
-13
View File
@@ -44,16 +44,3 @@ export interface Market {
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;
}
@@ -117,6 +117,12 @@ const getProps = (
intent: Intent.None,
children: <Step>Awaiting transaction</Step>,
},
[VegaTxStatus.Complete]: {
title: t('Withdrawal successful'),
icon: 'tick',
intent: Intent.Success,
children: <Step>Withdrawal has completed</Step>,
},
};
const ethTxPropsMap: Record<EthTxStatus, DialogProps> = {