Feat/522 close position (#1762)
* feat: use close position hook and dialog setup * chore: update wallet tx interface for batch market instruction * feat: add usage of data provider to show relevant order information * feat: render correctly formatted values in close position dialog * feat: make vega tx dialog more flexibly by allowing custom ui for every state of the tx * feat: adjust text alignment and spacing between active orders and order to close * feat: add unit tests * chore: remove stray log * chore: fix lint * chore: ignore ts error for formatter function of vesting chart * feat: split components up, memozie variables * feat: add shared loading state to prevent content popping in * feat: add time in force label * feat: move transaction result hook to wallet lib * feat: prevent being able to close vega tx dialog, must reject tx * chore: add test for useTransactionResult hook * chore: fix positiosn test after hook relocation * Revert "feat: prevent being able to close vega tx dialog, must reject tx" This reverts commit d1ecda69c3c55822bb042320f82b2e1c3833b99a. * chore: add check for order edge to be defined * chore: remove close callback * feat: use tx result state to determine dialog state * chore: update close position hook to check for transaction result * fix: readd types tif selection persistance * feat: convert order event func to be async, use it in close position for more result context * fix: rename utils * chore: adjust error language Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
This commit is contained in:
parent
317dfc4bb1
commit
d0976bbd46
@ -321,9 +321,15 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||
>
|
||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||
</Dialog>
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={transaction}
|
||||
order={finalizedOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
disabled: true,
|
||||
|
@ -67,20 +67,26 @@ const OrdersManager = () => {
|
||||
<orderCancel.Dialog
|
||||
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
||||
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
||||
>
|
||||
<OrderFeedback
|
||||
transaction={orderCancel.transaction}
|
||||
order={orderCancel.cancelledOrder}
|
||||
/>
|
||||
</orderCancel.Dialog>
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={orderCancel.transaction}
|
||||
order={orderCancel.cancelledOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<orderEdit.Dialog
|
||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||
>
|
||||
<OrderFeedback
|
||||
transaction={orderEdit.transaction}
|
||||
order={orderEdit.updatedOrder}
|
||||
/>
|
||||
</orderEdit.Dialog>
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={orderEdit.transaction}
|
||||
order={orderEdit.updatedOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
isOpen={Boolean(editOrder)}
|
||||
|
@ -14,16 +14,23 @@ interface ProposalFormTransactionDialogProps {
|
||||
export const ProposalFormTransactionDialog = ({
|
||||
finalizedProposal,
|
||||
TransactionDialog,
|
||||
}: ProposalFormTransactionDialogProps) => (
|
||||
<div data-testid="proposal-transaction-dialog">
|
||||
<TransactionDialog
|
||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||
>
|
||||
{finalizedProposal?.rejectionReason ? (
|
||||
<p>{finalizedProposal.rejectionReason}</p>
|
||||
) : undefined}
|
||||
</TransactionDialog>
|
||||
</div>
|
||||
);
|
||||
}: ProposalFormTransactionDialogProps) => {
|
||||
// Render a custom complete UI if the proposal was rejected other wise
|
||||
// pass undefined so that the default vega transaction dialog UI gets used
|
||||
const completeContent = finalizedProposal?.rejectionReason ? (
|
||||
<p>{finalizedProposal.rejectionReason}</p>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<div data-testid="proposal-transaction-dialog">
|
||||
<TransactionDialog
|
||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||
content={{
|
||||
Complete: completeContent,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -48,6 +48,7 @@ export const VestingChart = () => {
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: colors.black }}
|
||||
separator=":"
|
||||
// @ts-ignore formatter doesnt seem to allow returning JSX but nonetheless it works
|
||||
formatter={(value: number) => {
|
||||
return (
|
||||
<div
|
||||
|
@ -36,9 +36,12 @@ export const DealTicketManager = ({
|
||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||
>
|
||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||
</Dialog>
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { FormGroup, Select } from '@vegaprotocol/ui-toolkit';
|
||||
import { Schema } from '@vegaprotocol/types';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { timeInForceLabel } from '@vegaprotocol/orders';
|
||||
|
||||
interface TimeInForceSelectorProps {
|
||||
value: Schema.OrderTimeInForce;
|
||||
@ -9,26 +10,6 @@ interface TimeInForceSelectorProps {
|
||||
onSelect: (tif: Schema.OrderTimeInForce) => void;
|
||||
}
|
||||
|
||||
// More detail in https://docs.vega.xyz/docs/mainnet/graphql/enums/order-time-in-force
|
||||
export const timeInForceLabel = (tif: string) => {
|
||||
switch (tif) {
|
||||
case Schema.OrderTimeInForce.TIME_IN_FORCE_GTC:
|
||||
return t(`Good 'til Cancelled (GTC)`);
|
||||
case Schema.OrderTimeInForce.TIME_IN_FORCE_IOC:
|
||||
return t('Immediate or Cancel (IOC)');
|
||||
case Schema.OrderTimeInForce.TIME_IN_FORCE_FOK:
|
||||
return t('Fill or Kill (FOK)');
|
||||
case Schema.OrderTimeInForce.TIME_IN_FORCE_GTT:
|
||||
return t(`Good 'til Time (GTT)`);
|
||||
case Schema.OrderTimeInForce.TIME_IN_FORCE_GFN:
|
||||
return t('Good for Normal (GFN)');
|
||||
case Schema.OrderTimeInForce.TIME_IN_FORCE_GFA:
|
||||
return t('Good for Auction (GFA)');
|
||||
default:
|
||||
return t(tif);
|
||||
}
|
||||
};
|
||||
|
||||
type PossibleOrderKeys = Exclude<
|
||||
Schema.OrderType,
|
||||
Schema.OrderType.TYPE_NETWORK
|
||||
|
@ -62,20 +62,26 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
|
||||
<orderCancel.Dialog
|
||||
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
||||
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
||||
>
|
||||
<OrderFeedback
|
||||
transaction={orderCancel.transaction}
|
||||
order={orderCancel.cancelledOrder}
|
||||
/>
|
||||
</orderCancel.Dialog>
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={orderCancel.transaction}
|
||||
order={orderCancel.cancelledOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<orderEdit.Dialog
|
||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||
>
|
||||
<OrderFeedback
|
||||
transaction={orderEdit.transaction}
|
||||
order={orderEdit.updatedOrder}
|
||||
/>
|
||||
</orderEdit.Dialog>
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={orderEdit.transaction}
|
||||
order={orderEdit.updatedOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
isOpen={Boolean(editOrder)}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './components';
|
||||
export * from './order-hooks';
|
||||
export * from './utils';
|
||||
|
@ -3,3 +3,4 @@ export * from './order-event-query';
|
||||
export * from './use-order-cancel';
|
||||
export * from './use-order-submit';
|
||||
export * from './use-order-edit';
|
||||
export * from './use-order-event';
|
||||
|
@ -46,10 +46,9 @@ export const useOrderCancel = () => {
|
||||
},
|
||||
});
|
||||
|
||||
waitForOrderEvent(args.orderId, pubKey, (cancelledOrder) => {
|
||||
setCancelledOrder(cancelledOrder);
|
||||
setComplete();
|
||||
});
|
||||
const cancelledOrder = await waitForOrderEvent(args.orderId, pubKey);
|
||||
setCancelledOrder(cancelledOrder);
|
||||
setComplete();
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return;
|
||||
|
@ -54,10 +54,9 @@ export const useOrderEdit = (order: Order | null) => {
|
||||
},
|
||||
});
|
||||
|
||||
waitForOrderEvent(order.id, pubKey, (updatedOrder) => {
|
||||
setUpdatedOrder(updatedOrder);
|
||||
setComplete();
|
||||
});
|
||||
const updatedOrder = await waitForOrderEvent(order.id, pubKey);
|
||||
setUpdatedOrder(updatedOrder);
|
||||
setComplete();
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return;
|
||||
|
@ -8,44 +8,48 @@ import type {
|
||||
} from './';
|
||||
import type { Subscription } from 'zen-observable-ts';
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import { BusEventType } from '@vegaprotocol/types';
|
||||
|
||||
type WaitFunc = (
|
||||
orderId: string,
|
||||
partyId: string
|
||||
) => Promise<OrderEvent_busEvents_event_Order>;
|
||||
|
||||
export const useOrderEvent = (transaction: VegaTxState) => {
|
||||
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;
|
||||
const waitForOrderEvent = useCallback<WaitFunc>(
|
||||
(id: string, partyId: string) => {
|
||||
return new Promise((resolve) => {
|
||||
subRef.current = client
|
||||
.subscribe<OrderEvent, OrderEventVariables>({
|
||||
query: ORDER_EVENT_SUB,
|
||||
variables: { partyId },
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
if (!data?.busEvents?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return e.event.id === id;
|
||||
});
|
||||
// No types available for the subscription result
|
||||
const matchingOrderEvent = data.busEvents.find((e) => {
|
||||
if (e.event.__typename !== BusEventType.Order) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
matchingOrderEvent &&
|
||||
matchingOrderEvent.event.__typename === 'Order'
|
||||
) {
|
||||
callback(matchingOrderEvent.event);
|
||||
subRef.current?.unsubscribe();
|
||||
}
|
||||
});
|
||||
return e.event.id === id;
|
||||
});
|
||||
|
||||
if (
|
||||
matchingOrderEvent &&
|
||||
matchingOrderEvent.event.__typename === BusEventType.Order
|
||||
) {
|
||||
resolve(matchingOrderEvent.event);
|
||||
subRef.current?.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
@ -132,10 +132,9 @@ export const useOrderSubmit = () => {
|
||||
if (res) {
|
||||
const orderId = determineId(res.signature);
|
||||
if (orderId) {
|
||||
waitForOrderEvent(orderId, pubKey, (order) => {
|
||||
setFinalizedOrder(order);
|
||||
setComplete();
|
||||
});
|
||||
const order = await waitForOrderEvent(orderId, pubKey);
|
||||
setFinalizedOrder(order);
|
||||
setComplete();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
22
libs/orders/src/lib/utils.ts
Normal file
22
libs/orders/src/lib/utils.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { OrderTimeInForce } from '@vegaprotocol/types';
|
||||
|
||||
// More detail in https://docs.vega.xyz/docs/mainnet/graphql/enums/order-time-in-force
|
||||
export const timeInForceLabel = (tif: string) => {
|
||||
switch (tif) {
|
||||
case OrderTimeInForce.TIME_IN_FORCE_GTC:
|
||||
return t(`Good 'til Cancelled (GTC)`);
|
||||
case OrderTimeInForce.TIME_IN_FORCE_IOC:
|
||||
return t('Immediate or Cancel (IOC)');
|
||||
case OrderTimeInForce.TIME_IN_FORCE_FOK:
|
||||
return t('Fill or Kill (FOK)');
|
||||
case OrderTimeInForce.TIME_IN_FORCE_GTT:
|
||||
return t(`Good 'til Time (GTT)`);
|
||||
case OrderTimeInForce.TIME_IN_FORCE_GFN:
|
||||
return t('Good for Normal (GFN)');
|
||||
case OrderTimeInForce.TIME_IN_FORCE_GFA:
|
||||
return t('Good for Auction (GFA)');
|
||||
default:
|
||||
return t(tif);
|
||||
}
|
||||
};
|
@ -2,13 +2,8 @@
|
||||
export default {
|
||||
displayName: 'positions',
|
||||
preset: '../../jest.preset.js',
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'ts-jest',
|
||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/positions',
|
||||
|
@ -3,6 +3,5 @@ export * from './lib/positions-container';
|
||||
export * from './lib/positions-data-providers';
|
||||
export * from './lib/positions-table';
|
||||
export * from './lib/use-close-position';
|
||||
export * from './lib/use-position-event';
|
||||
export * from './lib/use-positions-data';
|
||||
export * from './lib/use-positions-assets';
|
||||
|
108
libs/positions/src/lib/close-position-dialog/complete.tsx
Normal file
108
libs/positions/src/lib/close-position-dialog/complete.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { OrderEvent_busEvents_event_Order } from '@vegaprotocol/orders';
|
||||
import { t, truncateByChars } from '@vegaprotocol/react-helpers';
|
||||
import { OrderRejectionReasonMapping, OrderStatus } from '@vegaprotocol/types';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import type { TransactionResult, VegaTxState } from '@vegaprotocol/wallet';
|
||||
import type { ClosingOrder as IClosingOrder } from '../use-close-position';
|
||||
import { useRequestClosePositionData } from '../use-request-close-position-data';
|
||||
import { ClosingOrder } from './shared';
|
||||
|
||||
interface CompleteProps {
|
||||
partyId: string;
|
||||
transaction: VegaTxState;
|
||||
transactionResult?: TransactionResult;
|
||||
closingOrder?: IClosingOrder;
|
||||
closingOrderResult?: OrderEvent_busEvents_event_Order;
|
||||
}
|
||||
|
||||
export const Complete = ({
|
||||
partyId,
|
||||
transaction,
|
||||
transactionResult,
|
||||
closingOrder,
|
||||
closingOrderResult,
|
||||
}: CompleteProps) => {
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
|
||||
if (!transactionResult || !closingOrderResult) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{closingOrderResult.status === OrderStatus.STATUS_FILLED &&
|
||||
transactionResult.status ? (
|
||||
<Success partyId={partyId} order={closingOrder} />
|
||||
) : (
|
||||
<Error
|
||||
transactionResult={transactionResult}
|
||||
closingOrderResult={closingOrderResult}
|
||||
/>
|
||||
)}
|
||||
{transaction.txHash && (
|
||||
<>
|
||||
<p className="font-semibold mt-4">{t('Transaction')}</p>
|
||||
<p>
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/txs/${transaction.txHash}`}
|
||||
target="_blank"
|
||||
>
|
||||
{truncateByChars(transaction.txHash)}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Success = ({
|
||||
partyId,
|
||||
order,
|
||||
}: {
|
||||
partyId: string;
|
||||
order?: IClosingOrder;
|
||||
}) => {
|
||||
const { market, marketData, orders } = useRequestClosePositionData(
|
||||
order?.marketId,
|
||||
partyId
|
||||
);
|
||||
|
||||
if (!market || !marketData || !orders) {
|
||||
return <div>{t('Loading...')}</div>;
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<div className="text-vega-red">{t('Could retrieve closing order')}</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h2 className="font-bold">{t('Position closed')}</h2>
|
||||
<ClosingOrder order={order} market={market} marketData={marketData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Error = ({
|
||||
transactionResult,
|
||||
closingOrderResult,
|
||||
}: {
|
||||
transactionResult: TransactionResult;
|
||||
closingOrderResult: OrderEvent_busEvents_event_Order;
|
||||
}) => {
|
||||
const reason =
|
||||
closingOrderResult.rejectionReason &&
|
||||
OrderRejectionReasonMapping[closingOrderResult.rejectionReason];
|
||||
return (
|
||||
<div className="text-vega-red">
|
||||
{reason ? (
|
||||
<p>{reason}</p>
|
||||
) : (
|
||||
<p>
|
||||
{t('Transaction failed')}: {transactionResult.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
101
libs/positions/src/lib/close-position-dialog/requested.spec.tsx
Normal file
101
libs/positions/src/lib/close-position-dialog/requested.spec.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types';
|
||||
import * as dataHook from '../use-request-close-position-data';
|
||||
import { Requested } from './requested';
|
||||
|
||||
jest.mock('./use-request-close-position-data');
|
||||
|
||||
describe('Close position dialog - Request', () => {
|
||||
const props = {
|
||||
partyId: 'party-id',
|
||||
order: {
|
||||
marketId: 'market-id',
|
||||
type: OrderType.TYPE_MARKET as const,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK as const,
|
||||
side: Side.SIDE_BUY,
|
||||
size: '10',
|
||||
},
|
||||
};
|
||||
|
||||
it('loading state', async () => {
|
||||
jest.spyOn(dataHook, 'useRequestClosePositionData').mockReturnValue({
|
||||
market: null,
|
||||
marketData: null,
|
||||
orders: [],
|
||||
});
|
||||
render(<Requested {...props} />);
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders message if no closing order found', async () => {
|
||||
const orders = [
|
||||
{
|
||||
size: '200',
|
||||
price: '999',
|
||||
side: Side.SIDE_BUY,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
},
|
||||
{
|
||||
size: '300',
|
||||
price: '888',
|
||||
side: Side.SIDE_SELL,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
},
|
||||
];
|
||||
jest.spyOn(dataHook, 'useRequestClosePositionData').mockReturnValue({
|
||||
market: {
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'test market',
|
||||
product: {
|
||||
// @ts-ignore avoiding having to add every property on the type
|
||||
settlementAsset: {
|
||||
symbol: 'SYM',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore avoid all fields
|
||||
marketData: {
|
||||
markPrice: '100',
|
||||
},
|
||||
// @ts-ignore avoid all fields
|
||||
orders,
|
||||
});
|
||||
render(<Requested {...props} />);
|
||||
|
||||
// closing order
|
||||
const closingOrderHeader = screen.getByText('Position to be closed');
|
||||
const closingOrderTable = within(
|
||||
closingOrderHeader.nextElementSibling?.querySelector(
|
||||
'tbody'
|
||||
) as HTMLElement
|
||||
);
|
||||
const closingOrderRow = closingOrderTable.getAllByRole('row');
|
||||
expect(closingOrderRow[0].children[0]).toHaveTextContent('test market');
|
||||
expect(closingOrderRow[0].children[1]).toHaveTextContent('+0.10');
|
||||
expect(closingOrderRow[0].children[2]).toHaveTextContent('~1.00 SYM');
|
||||
|
||||
// orders
|
||||
const ordersHeading = screen.getByText('Orders to be closed');
|
||||
const ordersTable = within(
|
||||
ordersHeading.nextElementSibling?.querySelector('tbody') as HTMLElement
|
||||
);
|
||||
const orderRows = ordersTable.getAllByRole('row');
|
||||
expect(orderRows).toHaveLength(orders.length);
|
||||
expect(orderRows[0].children[0]).toHaveTextContent('+2.00');
|
||||
expect(orderRows[0].children[1]).toHaveTextContent('9.99 SYM');
|
||||
expect(orderRows[0].children[2]).toHaveTextContent(
|
||||
"Good 'til Cancelled (GTC)"
|
||||
);
|
||||
|
||||
expect(orderRows[1].children[0]).toHaveTextContent('-3.00');
|
||||
expect(orderRows[1].children[1]).toHaveTextContent('8.88 SYM');
|
||||
expect(orderRows[1].children[2]).toHaveTextContent(
|
||||
"Good 'til Cancelled (GTC)"
|
||||
);
|
||||
});
|
||||
});
|
35
libs/positions/src/lib/close-position-dialog/requested.tsx
Normal file
35
libs/positions/src/lib/close-position-dialog/requested.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { ClosingOrder as IClosingOrder } from '../use-close-position';
|
||||
import { useRequestClosePositionData } from '../use-request-close-position-data';
|
||||
import { ActiveOrders, ClosingOrder } from './shared';
|
||||
|
||||
export const Requested = ({
|
||||
order,
|
||||
partyId,
|
||||
}: {
|
||||
order?: IClosingOrder;
|
||||
partyId: string;
|
||||
}) => {
|
||||
const { market, marketData, orders, loading } = useRequestClosePositionData(
|
||||
order?.marketId,
|
||||
partyId
|
||||
);
|
||||
|
||||
if (loading || !market || !marketData || !orders) {
|
||||
return <div>{t('Loading...')}</div>;
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<div className="text-vega-red">{t('Could not create closing order')}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="font-bold">{t('Position to be closed')}</h2>
|
||||
<ClosingOrder order={order} market={market} marketData={marketData} />
|
||||
<ActiveOrders market={market} orders={orders} />
|
||||
</>
|
||||
);
|
||||
};
|
115
libs/positions/src/lib/close-position-dialog/shared.tsx
Normal file
115
libs/positions/src/lib/close-position-dialog/shared.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import type {
|
||||
MarketDataFieldsFragment,
|
||||
SingleMarketFieldsFragment,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import type { Order } from '@vegaprotocol/orders';
|
||||
import { timeInForceLabel } from '@vegaprotocol/orders';
|
||||
import { addDecimalsFormatNumber, Size, t } from '@vegaprotocol/react-helpers';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ClosingOrder as IClosingOrder } from '../use-close-position';
|
||||
|
||||
export const ClosingOrder = ({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
}: {
|
||||
order: IClosingOrder;
|
||||
market: SingleMarketFieldsFragment;
|
||||
marketData: MarketDataFieldsFragment;
|
||||
}) => {
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
const estimatedPrice =
|
||||
marketData && market
|
||||
? addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
|
||||
: '-';
|
||||
const size = market ? (
|
||||
<Size
|
||||
value={order.size}
|
||||
side={order.side}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
|
||||
return (
|
||||
<BasicTable
|
||||
headers={[t('Market'), t('Amount'), t('Est price')]}
|
||||
rows={[
|
||||
[
|
||||
market.tradableInstrument.instrument.name,
|
||||
size,
|
||||
`~${estimatedPrice} ${asset?.symbol}`,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActiveOrders = ({
|
||||
market,
|
||||
orders,
|
||||
}: {
|
||||
market: SingleMarketFieldsFragment;
|
||||
orders: Order[];
|
||||
}) => {
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
|
||||
if (!orders.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h2 className="font-bold">{t('Orders to be closed')}</h2>
|
||||
<BasicTable
|
||||
headers={[t('Amount'), t('Target price'), t('Time in force')]}
|
||||
rows={orders.map((o) => {
|
||||
return [
|
||||
<Size
|
||||
value={o.size}
|
||||
side={o.side}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
/>,
|
||||
`${addDecimalsFormatNumber(o.price, market.decimalPlaces)} ${
|
||||
asset.symbol
|
||||
}`,
|
||||
timeInForceLabel(o.timeInForce),
|
||||
];
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface BasicTableProps {
|
||||
headers: ReactNode[];
|
||||
rows: ReactNode[][];
|
||||
}
|
||||
|
||||
const BasicTable = ({ headers, rows }: BasicTableProps) => {
|
||||
return (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((h, i) => (
|
||||
<th key={i} className="text-left font-medium">
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((cells, i) => (
|
||||
<tr key={i}>
|
||||
{cells.map((c, i) => (
|
||||
<td key={i} className="align-top">
|
||||
{c}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
@ -1,24 +1,28 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import { PositionsTable, useClosePosition, usePositionsData } from '../';
|
||||
import { useRef } from 'react';
|
||||
import { AsyncRenderer, Icon, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useClosePosition, usePositionsData, PositionsTable } from '../';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { Requested } from './close-position-dialog/requested';
|
||||
import { Complete } from './close-position-dialog/complete';
|
||||
import type { TransactionResult } from '@vegaprotocol/wallet';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface PositionsManagerProps {
|
||||
partyId: string;
|
||||
}
|
||||
|
||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
const { submit, Dialog } = useClosePosition();
|
||||
const onClose = useCallback(
|
||||
(position: Position) => {
|
||||
submit(position);
|
||||
},
|
||||
[submit]
|
||||
);
|
||||
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { data, error, loading, getRows } = usePositionsData(partyId, gridRef);
|
||||
const {
|
||||
submit,
|
||||
closingOrder,
|
||||
closingOrderResult,
|
||||
transaction,
|
||||
transactionResult,
|
||||
Dialog,
|
||||
} = useClosePosition();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
@ -29,13 +33,66 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||
rowData={data?.length ? undefined : []}
|
||||
datasource={{ getRows }}
|
||||
onClose={onClose}
|
||||
onClose={(position) => submit(position)}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
|
||||
<Dialog>
|
||||
<p>Your position was not closed! This is still not implemented.</p>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
intent={getDialogIntent(transactionResult)}
|
||||
icon={getDialogIcon(transactionResult)}
|
||||
title={getDialogTitle(transactionResult)}
|
||||
content={{
|
||||
Requested: <Requested partyId={partyId} order={closingOrder} />,
|
||||
Complete: (
|
||||
<Complete
|
||||
partyId={partyId}
|
||||
closingOrder={closingOrder}
|
||||
closingOrderResult={closingOrderResult}
|
||||
transaction={transaction}
|
||||
transactionResult={transactionResult}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getDialogIntent = (transactionResult?: TransactionResult) => {
|
||||
if (!transactionResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
transactionResult &&
|
||||
'error' in transactionResult &&
|
||||
transactionResult.error
|
||||
) {
|
||||
return Intent.Danger;
|
||||
}
|
||||
|
||||
return Intent.Success;
|
||||
};
|
||||
|
||||
const getDialogIcon = (transactionResult?: TransactionResult) => {
|
||||
if (!transactionResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactionResult.status) {
|
||||
return <Icon name="tick" />;
|
||||
}
|
||||
|
||||
return <Icon name="error" />;
|
||||
};
|
||||
|
||||
const getDialogTitle = (transactionResult?: TransactionResult) => {
|
||||
if (!transactionResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactionResult.status) {
|
||||
return t('Position closed');
|
||||
}
|
||||
|
||||
return t('Position not closed');
|
||||
};
|
||||
|
233
libs/positions/src/lib/use-close-position.spec.tsx
Normal file
233
libs/positions/src/lib/use-close-position.spec.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
BusEventType,
|
||||
OrderStatus,
|
||||
OrderTimeInForce,
|
||||
OrderType,
|
||||
Schema as Types,
|
||||
Side,
|
||||
} from '@vegaprotocol/types';
|
||||
import { useClosePosition } from './use-close-position';
|
||||
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { initialState } from '@vegaprotocol/wallet';
|
||||
import type { TransactionEventSubscription } from '@vegaprotocol/wallet';
|
||||
import { TransactionEventDocument } from '@vegaprotocol/wallet';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import type { OrderEvent } from '@vegaprotocol/orders';
|
||||
import { ORDER_EVENT_SUB } from '@vegaprotocol/orders';
|
||||
|
||||
const pubKey = 'test-pubkey';
|
||||
const defaultWalletContext = {
|
||||
pubKey,
|
||||
pubKeys: [{ publicKey: pubKey, name: 'test pubkey' }],
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
selectPubKey: jest.fn(),
|
||||
connector: null,
|
||||
};
|
||||
const txResult = {
|
||||
__typename: 'TransactionResult',
|
||||
partyId: pubKey,
|
||||
hash: '0x123',
|
||||
status: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
function setup(context?: Partial<VegaWalletContextShape>) {
|
||||
const mockTransactionResult: MockedResponse<TransactionEventSubscription> = {
|
||||
request: {
|
||||
query: TransactionEventDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
busEvents: [
|
||||
{
|
||||
type: Types.BusEventType.TransactionResult,
|
||||
event: txResult,
|
||||
__typename: 'BusEvent',
|
||||
},
|
||||
] as TransactionEventSubscription['busEvents'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockOrderResult: MockedResponse<OrderEvent> = {
|
||||
request: {
|
||||
query: ORDER_EVENT_SUB,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
busEvents: [
|
||||
{
|
||||
type: BusEventType.Order,
|
||||
event: {
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
id: '2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50',
|
||||
status: OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Side.SIDE_BUY,
|
||||
market: {
|
||||
id: 'market-id',
|
||||
decimalPlaces: 5,
|
||||
positionDecimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
__typename: 'Instrument',
|
||||
},
|
||||
},
|
||||
__typename: 'Market',
|
||||
},
|
||||
__typename: 'Order',
|
||||
},
|
||||
__typename: 'BusEvent',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={[mockTransactionResult, mockOrderResult]}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ ...defaultWalletContext, ...context }}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useClosePosition(), { wrapper });
|
||||
}
|
||||
|
||||
describe('useClosePosition', () => {
|
||||
const txResponse = {
|
||||
signature:
|
||||
'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909',
|
||||
transactionHash: '0x123',
|
||||
};
|
||||
|
||||
it('doesnt send the tx if there is no open volume', () => {
|
||||
const mockSend = jest.fn();
|
||||
const { result } = setup({ sendTx: mockSend });
|
||||
expect(result.current).toEqual({
|
||||
submit: expect.any(Function),
|
||||
transaction: initialState,
|
||||
Dialog: expect.any(Function),
|
||||
});
|
||||
result.current.submit({ marketId: 'test-market', openVolume: '0' });
|
||||
expect(mockSend).not.toBeCalled();
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||
});
|
||||
|
||||
it('doesnt send the tx if there is no pubkey', () => {
|
||||
const mockSend = jest.fn();
|
||||
const { result } = setup({ sendTx: mockSend, pubKey: null });
|
||||
result.current.submit({ marketId: 'test-market', openVolume: '1000' });
|
||||
expect(mockSend).not.toBeCalled();
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||
});
|
||||
|
||||
it('closes long positions', async () => {
|
||||
const marketId = 'test-market';
|
||||
const openVolume = '1000';
|
||||
const mockSend = jest.fn().mockResolvedValue(txResponse);
|
||||
const { result } = setup({ sendTx: mockSend, pubKey });
|
||||
|
||||
act(() => {
|
||||
result.current.submit({ marketId, openVolume });
|
||||
});
|
||||
|
||||
expect(mockSend).toBeCalledWith(defaultWalletContext.pubKey, {
|
||||
batchMarketInstructions: {
|
||||
cancellations: [
|
||||
{
|
||||
marketId,
|
||||
orderId: '',
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
marketId,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
side: Side.SIDE_SELL,
|
||||
size: openVolume,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Requested);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.transaction).toEqual({
|
||||
status: VegaTxStatus.Complete,
|
||||
signature: txResponse.signature,
|
||||
txHash: txResponse.transactionHash,
|
||||
dialogOpen: true,
|
||||
error: null,
|
||||
});
|
||||
expect(result.current.transactionResult).toEqual(txResult);
|
||||
});
|
||||
});
|
||||
|
||||
it('closes short positions', async () => {
|
||||
const marketId = 'test-market';
|
||||
const openVolume = '-1000';
|
||||
const mockSend = jest.fn().mockResolvedValue(txResponse);
|
||||
const { result } = setup({ sendTx: mockSend, pubKey });
|
||||
|
||||
act(() => {
|
||||
result.current.submit({ marketId, openVolume });
|
||||
});
|
||||
|
||||
expect(mockSend).toBeCalledWith(defaultWalletContext.pubKey, {
|
||||
batchMarketInstructions: {
|
||||
cancellations: [
|
||||
{
|
||||
marketId,
|
||||
orderId: '',
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
marketId,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
side: Side.SIDE_BUY,
|
||||
size: openVolume.replace('-', ''),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Requested);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.transaction).toEqual({
|
||||
status: VegaTxStatus.Complete,
|
||||
signature: txResponse.signature,
|
||||
txHash: txResponse.transactionHash,
|
||||
dialogOpen: true,
|
||||
error: null,
|
||||
});
|
||||
expect(result.current.transactionResult).toEqual(txResult);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,61 +1,105 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useVegaTransaction, determineId } from '@vegaprotocol/wallet';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { TransactionResult } from '@vegaprotocol/wallet';
|
||||
import { determineId } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet, useTransactionResult } from '@vegaprotocol/wallet';
|
||||
import { useVegaTransaction } from '@vegaprotocol/wallet';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { usePositionEvent } from '../';
|
||||
import type { Position } from '../';
|
||||
import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types';
|
||||
import { useOrderEvent } from '@vegaprotocol/orders';
|
||||
import type { OrderEvent_busEvents_event_Order } from '@vegaprotocol/orders';
|
||||
|
||||
export interface ClosingOrder {
|
||||
marketId: string;
|
||||
type: OrderType.TYPE_MARKET;
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK;
|
||||
side: Side;
|
||||
size: string;
|
||||
}
|
||||
|
||||
export const useClosePosition = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const {
|
||||
send,
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
const waitForPositionEvent = usePositionEvent(transaction);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
resetTransaction();
|
||||
}, [resetTransaction]);
|
||||
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
|
||||
const [closingOrder, setClosingOrder] = useState<ClosingOrder>();
|
||||
const [closingOrderResult, setClosingOrderResult] =
|
||||
useState<OrderEvent_busEvents_event_Order>();
|
||||
const [transactionResult, setTransactionResult] =
|
||||
useState<TransactionResult>();
|
||||
const waitForTransactionResult = useTransactionResult();
|
||||
const waitForOrder = useOrderEvent(transaction);
|
||||
|
||||
const submit = useCallback(
|
||||
async (position: Position) => {
|
||||
if (!pubKey || position.openVolume === '0') {
|
||||
async ({
|
||||
marketId,
|
||||
openVolume,
|
||||
}: {
|
||||
marketId: string;
|
||||
openVolume: string;
|
||||
}) => {
|
||||
if (!pubKey || openVolume === '0') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await send(pubKey, {
|
||||
orderCancellation: {
|
||||
marketId: position.marketId,
|
||||
orderId: '',
|
||||
},
|
||||
});
|
||||
setTransactionResult(undefined);
|
||||
setClosingOrder(undefined);
|
||||
|
||||
if (res?.signature) {
|
||||
const resId = determineId(res.signature);
|
||||
if (resId) {
|
||||
waitForPositionEvent(resId, pubKey, () => {
|
||||
setComplete();
|
||||
});
|
||||
}
|
||||
try {
|
||||
// figure out if opsition is long or short and make side the opposite
|
||||
const side = openVolume.startsWith('-')
|
||||
? Side.SIDE_BUY
|
||||
: Side.SIDE_SELL;
|
||||
|
||||
// volume could be prefixed with '-' if position is short, remove it
|
||||
const size = openVolume.replace('-', '');
|
||||
const closingOrder = {
|
||||
marketId: marketId,
|
||||
type: OrderType.TYPE_MARKET as const,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK as const,
|
||||
side,
|
||||
size,
|
||||
};
|
||||
|
||||
setClosingOrder(closingOrder);
|
||||
|
||||
const command = {
|
||||
batchMarketInstructions: {
|
||||
cancellations: [
|
||||
{
|
||||
marketId,
|
||||
orderId: '', // omit order id to cancel all active orders
|
||||
},
|
||||
],
|
||||
submissions: [closingOrder],
|
||||
},
|
||||
};
|
||||
|
||||
const res = await send(pubKey, command);
|
||||
|
||||
if (res) {
|
||||
const orderId = determineId(res.signature);
|
||||
const [txResult, orderResult] = await Promise.all([
|
||||
waitForTransactionResult(res.transactionHash, pubKey),
|
||||
waitForOrder(orderId, pubKey),
|
||||
]);
|
||||
setTransactionResult(txResult);
|
||||
setClosingOrderResult(orderResult);
|
||||
setComplete();
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[pubKey, send, setComplete, waitForPositionEvent]
|
||||
[pubKey, send, setComplete, waitForTransactionResult, waitForOrder]
|
||||
);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
Dialog,
|
||||
transactionResult,
|
||||
submit,
|
||||
reset,
|
||||
closingOrder,
|
||||
closingOrderResult,
|
||||
Dialog,
|
||||
};
|
||||
};
|
||||
|
@ -1,15 +0,0 @@
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
// this should be replaced by implementation of busEvents listener when it will be available
|
||||
export const usePositionEvent = (transaction: VegaTxState) => {
|
||||
const waitForOrderEvent = useCallback(
|
||||
(id: string, partyId: string, callback: () => void) => {
|
||||
Promise.resolve().then(() => {
|
||||
callback();
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
return waitForOrderEvent;
|
||||
};
|
58
libs/positions/src/lib/use-request-close-position-data.ts
Normal file
58
libs/positions/src/lib/use-request-close-position-data.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
|
||||
import { isOrderActive, ordersWithMarketProvider } from '@vegaprotocol/orders';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useRequestClosePositionData = (
|
||||
marketId?: string,
|
||||
partyId?: string
|
||||
) => {
|
||||
const marketVariables = useMemo(() => ({ marketId }), [marketId]);
|
||||
const orderVariables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const { data: market, loading: marketLoading } = useDataProvider({
|
||||
dataProvider: marketProvider,
|
||||
variables: marketVariables,
|
||||
skip: !marketId,
|
||||
});
|
||||
const { data: marketData, loading: marketDataLoading } = useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
variables: marketVariables,
|
||||
});
|
||||
const { data: orderData, loading: orderDataLoading } = useDataProvider({
|
||||
dataProvider: ordersWithMarketProvider,
|
||||
variables: orderVariables,
|
||||
});
|
||||
|
||||
const orders = useMemo(() => {
|
||||
if (!orderData || !market) return [];
|
||||
return (
|
||||
orderData
|
||||
.filter((o) => {
|
||||
// Filter out orders not on market for position
|
||||
if (
|
||||
!o ||
|
||||
!o.node ||
|
||||
!o.node.market ||
|
||||
o.node.market.id !== market.id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isOrderActive(o.node.status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
// @ts-ignore o is never null as its been filtered out above
|
||||
.map((o) => o.node)
|
||||
);
|
||||
}, [orderData, market]);
|
||||
|
||||
return {
|
||||
market,
|
||||
marketData,
|
||||
orders,
|
||||
loading: marketLoading || marketDataLoading || orderDataLoading,
|
||||
};
|
||||
};
|
@ -2,22 +2,19 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": ["node"]
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||
},
|
||||
"files": [
|
||||
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||
"../../node_modules/@nrwl/next/typings/image.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts",
|
||||
"jest.config.ts"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
]
|
||||
}
|
||||
|
13
libs/wallet/src/TransactionResult.graphql
Normal file
13
libs/wallet/src/TransactionResult.graphql
Normal file
@ -0,0 +1,13 @@
|
||||
subscription TransactionEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [TransactionResult]) {
|
||||
type
|
||||
event {
|
||||
... on TransactionResult {
|
||||
partyId
|
||||
hash
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
libs/wallet/src/__generated___/TransactionResult.ts
Normal file
51
libs/wallet/src/__generated___/TransactionResult.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TransactionEventSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type TransactionEventSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'Account' } | { __typename?: 'Asset' } | { __typename?: 'AuctionEvent' } | { __typename?: 'Deposit' } | { __typename?: 'LiquidityProvision' } | { __typename?: 'LossSocialization' } | { __typename?: 'MarginLevels' } | { __typename?: 'Market' } | { __typename?: 'MarketData' } | { __typename?: 'MarketEvent' } | { __typename?: 'MarketTick' } | { __typename?: 'NodeSignature' } | { __typename?: 'OracleSpec' } | { __typename?: 'Order' } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal' } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult', partyId: string, hash: string, status: boolean, error?: string | null } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote' } | { __typename?: 'Withdrawal' } }> | null };
|
||||
|
||||
|
||||
export const TransactionEventDocument = gql`
|
||||
subscription TransactionEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [TransactionResult]) {
|
||||
type
|
||||
event {
|
||||
... on TransactionResult {
|
||||
partyId
|
||||
hash
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTransactionEventSubscription__
|
||||
*
|
||||
* To run a query within a React component, call `useTransactionEventSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTransactionEventSubscription` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useTransactionEventSubscription({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTransactionEventSubscription(baseOptions: Apollo.SubscriptionHookOptions<TransactionEventSubscription, TransactionEventSubscriptionVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useSubscription<TransactionEventSubscription, TransactionEventSubscriptionVariables>(TransactionEventDocument, options);
|
||||
}
|
||||
export type TransactionEventSubscriptionHookResult = ReturnType<typeof useTransactionEventSubscription>;
|
||||
export type TransactionEventSubscriptionResult = Apollo.SubscriptionResult<TransactionEventSubscription>;
|
@ -136,7 +136,7 @@ const Connecting = ({
|
||||
</Center>
|
||||
<p className="text-center">
|
||||
{t(`${window.location.host} now has access to your Wallet, however you don't
|
||||
have sufficient permissions to retrieve your public keys. Got to your wallet to approve new permissions.`)}
|
||||
have sufficient permissions to retrieve your public keys. Go to your wallet to approve new permissions.`)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
@ -20,36 +20,41 @@ export interface UndelegateSubmissionBody {
|
||||
};
|
||||
}
|
||||
|
||||
interface OrderSubmission {
|
||||
marketId: string;
|
||||
reference?: string;
|
||||
type: OrderType;
|
||||
side: Side;
|
||||
timeInForce: OrderTimeInForce;
|
||||
size: string;
|
||||
price?: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
interface OrderCancellation {
|
||||
orderId: string;
|
||||
marketId: string;
|
||||
}
|
||||
|
||||
interface OrderAmendment {
|
||||
marketId: string;
|
||||
orderId: string;
|
||||
reference?: string;
|
||||
timeInForce: OrderTimeInForce;
|
||||
sizeDelta?: number;
|
||||
price?: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
export interface OrderSubmissionBody {
|
||||
orderSubmission: {
|
||||
marketId: string;
|
||||
reference?: string;
|
||||
type: OrderType;
|
||||
side: Side;
|
||||
timeInForce: OrderTimeInForce;
|
||||
size: string;
|
||||
price?: string;
|
||||
expiresAt?: string;
|
||||
};
|
||||
orderSubmission: OrderSubmission;
|
||||
}
|
||||
|
||||
export interface OrderCancellationBody {
|
||||
orderCancellation: {
|
||||
orderId: string;
|
||||
marketId: string;
|
||||
};
|
||||
orderCancellation: OrderCancellation;
|
||||
}
|
||||
|
||||
export interface OrderAmendmentBody {
|
||||
orderAmendment: {
|
||||
marketId: string;
|
||||
orderId: string;
|
||||
reference?: string;
|
||||
timeInForce: OrderTimeInForce;
|
||||
sizeDelta?: number;
|
||||
price?: string;
|
||||
expiresAt?: string;
|
||||
};
|
||||
orderAmendment: OrderAmendment;
|
||||
}
|
||||
|
||||
export interface VoteSubmissionBody {
|
||||
@ -250,6 +255,18 @@ export interface ProposalSubmissionBody {
|
||||
proposalSubmission: ProposalSubmission;
|
||||
}
|
||||
|
||||
export interface BatchMarketInstructionSubmissionBody {
|
||||
batchMarketInstructions: {
|
||||
// Will be processed in this order and the total amount of instructions is
|
||||
// restricted by the net param spam.protection.max.batchSize
|
||||
cancellations?: OrderCancellation[];
|
||||
amendments?: OrderAmendment[];
|
||||
// Note: If multiple orders are submitted the first order ID is determined by hashing the signature of the transaction
|
||||
// (see determineId function). For each subsequent order's ID, a hash of the previous orders ID is used
|
||||
submissions?: OrderSubmission[];
|
||||
};
|
||||
}
|
||||
|
||||
export type Transaction =
|
||||
| OrderSubmissionBody
|
||||
| OrderCancellationBody
|
||||
@ -258,7 +275,8 @@ export type Transaction =
|
||||
| DelegateSubmissionBody
|
||||
| UndelegateSubmissionBody
|
||||
| OrderAmendmentBody
|
||||
| ProposalSubmissionBody;
|
||||
| ProposalSubmissionBody
|
||||
| BatchMarketInstructionSubmissionBody;
|
||||
|
||||
export interface TransactionResponse {
|
||||
transactionHash: string;
|
||||
|
@ -2,6 +2,7 @@ export * from './context';
|
||||
export * from './use-vega-wallet';
|
||||
export * from './connectors';
|
||||
export * from './use-vega-transaction';
|
||||
export * from './use-transaction-result';
|
||||
export * from './use-eager-connect';
|
||||
export * from './manage-dialog';
|
||||
export * from './vega-transaction-dialog';
|
||||
@ -9,3 +10,4 @@ export * from './provider';
|
||||
export * from './connect-dialog';
|
||||
export * from './utils';
|
||||
export * from './constants';
|
||||
export * from './__generated___/TransactionResult';
|
||||
|
57
libs/wallet/src/use-transaction-result.spec.tsx
Normal file
57
libs/wallet/src/use-transaction-result.spec.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
import type { TransactionEventSubscription } from './__generated___/TransactionResult';
|
||||
import { TransactionEventDocument } from './__generated___/TransactionResult';
|
||||
import { useTransactionResult } from './use-transaction-result';
|
||||
|
||||
const pubKey = 'test-pubkey';
|
||||
const txHash = '0x123';
|
||||
const event = {
|
||||
__typename: 'TransactionResult',
|
||||
partyId: pubKey,
|
||||
hash: txHash,
|
||||
status: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
function setup(mock: MockedResponse) {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={[mock]}>{children}</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useTransactionResult(), { wrapper });
|
||||
}
|
||||
|
||||
describe('useTransactionResult', () => {
|
||||
it('resolves when a matching txhash is found', async () => {
|
||||
const mock: MockedResponse<TransactionEventSubscription> = {
|
||||
request: {
|
||||
query: TransactionEventDocument,
|
||||
variables: {
|
||||
partyId: pubKey,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
busEvents: [
|
||||
{
|
||||
type: Types.BusEventType.TransactionResult,
|
||||
event,
|
||||
__typename: 'BusEvent',
|
||||
},
|
||||
] as TransactionEventSubscription['busEvents'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = setup(mock);
|
||||
expect(result.current).toEqual(expect.any(Function));
|
||||
const promi = result.current(txHash, pubKey);
|
||||
expect(typeof promi === 'object' && typeof promi.then === 'function').toBe(
|
||||
true
|
||||
);
|
||||
const res = await promi;
|
||||
expect(res).toEqual(event);
|
||||
});
|
||||
});
|
74
libs/wallet/src/use-transaction-result.ts
Normal file
74
libs/wallet/src/use-transaction-result.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { BusEventType } from '@vegaprotocol/types';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { Subscription } from 'zen-observable-ts';
|
||||
import type {
|
||||
TransactionEventSubscription,
|
||||
TransactionEventSubscriptionVariables,
|
||||
} from './__generated___/TransactionResult';
|
||||
import { TransactionEventDocument } from './__generated___/TransactionResult';
|
||||
|
||||
export interface TransactionResult {
|
||||
partyId: string;
|
||||
hash: string;
|
||||
status: boolean;
|
||||
error?: string | null;
|
||||
__typename: 'TransactionResult';
|
||||
}
|
||||
|
||||
type WaitFunc = (txHash: string, partyId: string) => Promise<TransactionResult>;
|
||||
|
||||
/**
|
||||
* Returns a function that can be called to subscribe to a transaction
|
||||
* result event and resolves when an event with a matching txhash is seen
|
||||
*/
|
||||
export const useTransactionResult = () => {
|
||||
const client = useApolloClient();
|
||||
const subRef = useRef<Subscription | null>(null);
|
||||
|
||||
const waitForTransactionResult = useCallback<WaitFunc>(
|
||||
(txHash: string, partyId: string) => {
|
||||
return new Promise((resolve) => {
|
||||
subRef.current = client
|
||||
.subscribe<
|
||||
TransactionEventSubscription,
|
||||
TransactionEventSubscriptionVariables
|
||||
>({
|
||||
query: TransactionEventDocument,
|
||||
variables: { partyId },
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
if (!data?.busEvents?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingTransaction = data.busEvents.find((e) => {
|
||||
if (e.event.__typename !== BusEventType.TransactionResult) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.event.hash.toLocaleLowerCase() === txHash.toLowerCase();
|
||||
});
|
||||
|
||||
if (
|
||||
matchingTransaction &&
|
||||
matchingTransaction.event.__typename ===
|
||||
BusEventType.TransactionResult
|
||||
) {
|
||||
resolve(matchingTransaction.event as TransactionResult);
|
||||
subRef.current?.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
subRef.current?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return waitForTransactionResult;
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useVegaWallet } from './use-vega-wallet';
|
||||
import type { VegaTransactionContentMap } from './vega-transaction-dialog';
|
||||
import { VegaTransactionDialog } from './vega-transaction-dialog';
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Transaction } from './connectors';
|
||||
@ -8,10 +9,10 @@ import { ClientErrors } from './connectors';
|
||||
import { WalletError } from './connectors';
|
||||
|
||||
export interface DialogProps {
|
||||
children?: JSX.Element;
|
||||
intent?: Intent;
|
||||
title?: string;
|
||||
icon?: ReactNode;
|
||||
content?: VegaTransactionContentMap;
|
||||
}
|
||||
|
||||
export enum VegaTxStatus {
|
||||
|
@ -96,25 +96,30 @@ describe('VegaTransactionDialog', () => {
|
||||
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();
|
||||
});
|
||||
it.each(Object.keys(VegaTxStatus))(
|
||||
'renders custom content for %s',
|
||||
(status) => {
|
||||
const title = `${status} title`;
|
||||
const text = `${status} content`;
|
||||
const content = {
|
||||
[status]: <div>{text}</div>,
|
||||
};
|
||||
render(
|
||||
<VegaTransactionDialog
|
||||
{...props}
|
||||
transaction={{
|
||||
...props.transaction,
|
||||
txHash: 'tx-hash',
|
||||
status: status as VegaTxStatus,
|
||||
}}
|
||||
title={title}
|
||||
content={content}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(title);
|
||||
expect(screen.getByText(text)).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
|
||||
function testBlockExplorerLink(txHash: string) {
|
||||
expect(screen.getByTestId('tx-block-explorer')).toHaveTextContent(
|
||||
|
@ -5,37 +5,32 @@ import type { ReactNode } from 'react';
|
||||
import type { VegaTxState } from '../use-vega-transaction';
|
||||
import { VegaTxStatus } from '../use-vega-transaction';
|
||||
|
||||
export type VegaTransactionContentMap = {
|
||||
[C in VegaTxStatus]?: JSX.Element;
|
||||
};
|
||||
export interface VegaTransactionDialogProps {
|
||||
isOpen: boolean;
|
||||
onChange: (isOpen: boolean) => void;
|
||||
transaction: VegaTxState;
|
||||
children?: ReactNode;
|
||||
intent?: Intent;
|
||||
title?: string;
|
||||
icon?: ReactNode;
|
||||
content?: VegaTransactionContentMap;
|
||||
}
|
||||
|
||||
export const VegaTransactionDialog = ({
|
||||
isOpen,
|
||||
onChange,
|
||||
transaction,
|
||||
children,
|
||||
intent,
|
||||
title,
|
||||
icon,
|
||||
content,
|
||||
}: VegaTransactionDialogProps) => {
|
||||
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={isOpen}
|
||||
@ -45,10 +40,42 @@ export const VegaTransactionDialog = ({
|
||||
icon={computedIcon}
|
||||
size="small"
|
||||
>
|
||||
{content}
|
||||
<Content transaction={transaction} content={content} />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
interface ContentProps {
|
||||
transaction: VegaTxState;
|
||||
content?: VegaTransactionContentMap;
|
||||
}
|
||||
|
||||
const Content = ({ transaction, content }: ContentProps) => {
|
||||
if (!content) {
|
||||
return <VegaDialog transaction={transaction} />;
|
||||
}
|
||||
|
||||
if (transaction.status === VegaTxStatus.Default && content.Default) {
|
||||
return content.Default;
|
||||
}
|
||||
|
||||
if (transaction.status === VegaTxStatus.Requested && content.Requested) {
|
||||
return content.Requested;
|
||||
}
|
||||
|
||||
if (transaction.status === VegaTxStatus.Pending && content.Pending) {
|
||||
return content.Pending;
|
||||
}
|
||||
|
||||
if (transaction.status === VegaTxStatus.Error && content.Error) {
|
||||
return content.Error;
|
||||
}
|
||||
|
||||
if (transaction.status === VegaTxStatus.Complete && content.Complete) {
|
||||
return content.Complete;
|
||||
}
|
||||
|
||||
return <VegaDialog transaction={transaction} />;
|
||||
};
|
||||
|
||||
interface VegaDialogProps {
|
||||
transaction: VegaTxState;
|
||||
|
@ -35,17 +35,21 @@ export const WithdrawalDialogs = ({
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
<createWithdraw.Dialog>
|
||||
<WithdrawalFeedback
|
||||
withdrawal={createWithdraw.withdrawal}
|
||||
transaction={createWithdraw.transaction}
|
||||
availableTimestamp={createWithdraw.availableTimestamp}
|
||||
submitWithdraw={(id) => {
|
||||
createWithdraw.reset();
|
||||
completeWithdraw.submit(id);
|
||||
}}
|
||||
/>
|
||||
</createWithdraw.Dialog>
|
||||
<createWithdraw.Dialog
|
||||
content={{
|
||||
Complete: (
|
||||
<WithdrawalFeedback
|
||||
withdrawal={createWithdraw.withdrawal}
|
||||
transaction={createWithdraw.transaction}
|
||||
availableTimestamp={createWithdraw.availableTimestamp}
|
||||
submitWithdraw={(id) => {
|
||||
createWithdraw.reset();
|
||||
completeWithdraw.submit(id);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<completeWithdraw.Dialog />
|
||||
</>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user