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)}
|
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||||
>
|
content={{
|
||||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
Complete: (
|
||||||
</Dialog>
|
<OrderFeedback
|
||||||
|
transaction={transaction}
|
||||||
|
order={finalizedOrder}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
@ -67,20 +67,26 @@ const OrdersManager = () => {
|
|||||||
<orderCancel.Dialog
|
<orderCancel.Dialog
|
||||||
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
||||||
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
||||||
>
|
content={{
|
||||||
<OrderFeedback
|
Complete: (
|
||||||
transaction={orderCancel.transaction}
|
<OrderFeedback
|
||||||
order={orderCancel.cancelledOrder}
|
transaction={orderCancel.transaction}
|
||||||
/>
|
order={orderCancel.cancelledOrder}
|
||||||
</orderCancel.Dialog>
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<orderEdit.Dialog
|
<orderEdit.Dialog
|
||||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||||
>
|
content={{
|
||||||
<OrderFeedback
|
Complete: (
|
||||||
transaction={orderEdit.transaction}
|
<OrderFeedback
|
||||||
order={orderEdit.updatedOrder}
|
transaction={orderEdit.transaction}
|
||||||
/>
|
order={orderEdit.updatedOrder}
|
||||||
</orderEdit.Dialog>
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{editOrder && (
|
{editOrder && (
|
||||||
<OrderEditDialog
|
<OrderEditDialog
|
||||||
isOpen={Boolean(editOrder)}
|
isOpen={Boolean(editOrder)}
|
||||||
|
@ -14,16 +14,23 @@ interface ProposalFormTransactionDialogProps {
|
|||||||
export const ProposalFormTransactionDialog = ({
|
export const ProposalFormTransactionDialog = ({
|
||||||
finalizedProposal,
|
finalizedProposal,
|
||||||
TransactionDialog,
|
TransactionDialog,
|
||||||
}: ProposalFormTransactionDialogProps) => (
|
}: ProposalFormTransactionDialogProps) => {
|
||||||
<div data-testid="proposal-transaction-dialog">
|
// Render a custom complete UI if the proposal was rejected other wise
|
||||||
<TransactionDialog
|
// pass undefined so that the default vega transaction dialog UI gets used
|
||||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
const completeContent = finalizedProposal?.rejectionReason ? (
|
||||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
<p>{finalizedProposal.rejectionReason}</p>
|
||||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
) : undefined;
|
||||||
>
|
|
||||||
{finalizedProposal?.rejectionReason ? (
|
return (
|
||||||
<p>{finalizedProposal.rejectionReason}</p>
|
<div data-testid="proposal-transaction-dialog">
|
||||||
) : undefined}
|
<TransactionDialog
|
||||||
</TransactionDialog>
|
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||||
</div>
|
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||||
);
|
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||||
|
content={{
|
||||||
|
Complete: completeContent,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -48,6 +48,7 @@ export const VestingChart = () => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{ backgroundColor: colors.black }}
|
contentStyle={{ backgroundColor: colors.black }}
|
||||||
separator=":"
|
separator=":"
|
||||||
|
// @ts-ignore formatter doesnt seem to allow returning JSX but nonetheless it works
|
||||||
formatter={(value: number) => {
|
formatter={(value: number) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -36,9 +36,12 @@ export const DealTicketManager = ({
|
|||||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||||
>
|
content={{
|
||||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
Complete: (
|
||||||
</Dialog>
|
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { FormGroup, Select } from '@vegaprotocol/ui-toolkit';
|
import { FormGroup, Select } from '@vegaprotocol/ui-toolkit';
|
||||||
import { Schema } from '@vegaprotocol/types';
|
import { Schema } from '@vegaprotocol/types';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { timeInForceLabel } from '@vegaprotocol/orders';
|
||||||
|
|
||||||
interface TimeInForceSelectorProps {
|
interface TimeInForceSelectorProps {
|
||||||
value: Schema.OrderTimeInForce;
|
value: Schema.OrderTimeInForce;
|
||||||
@ -9,26 +10,6 @@ interface TimeInForceSelectorProps {
|
|||||||
onSelect: (tif: Schema.OrderTimeInForce) => void;
|
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<
|
type PossibleOrderKeys = Exclude<
|
||||||
Schema.OrderType,
|
Schema.OrderType,
|
||||||
Schema.OrderType.TYPE_NETWORK
|
Schema.OrderType.TYPE_NETWORK
|
||||||
|
@ -62,20 +62,26 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
|
|||||||
<orderCancel.Dialog
|
<orderCancel.Dialog
|
||||||
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
||||||
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
||||||
>
|
content={{
|
||||||
<OrderFeedback
|
Complete: (
|
||||||
transaction={orderCancel.transaction}
|
<OrderFeedback
|
||||||
order={orderCancel.cancelledOrder}
|
transaction={orderCancel.transaction}
|
||||||
/>
|
order={orderCancel.cancelledOrder}
|
||||||
</orderCancel.Dialog>
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<orderEdit.Dialog
|
<orderEdit.Dialog
|
||||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||||
>
|
content={{
|
||||||
<OrderFeedback
|
Complete: (
|
||||||
transaction={orderEdit.transaction}
|
<OrderFeedback
|
||||||
order={orderEdit.updatedOrder}
|
transaction={orderEdit.transaction}
|
||||||
/>
|
order={orderEdit.updatedOrder}
|
||||||
</orderEdit.Dialog>
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{editOrder && (
|
{editOrder && (
|
||||||
<OrderEditDialog
|
<OrderEditDialog
|
||||||
isOpen={Boolean(editOrder)}
|
isOpen={Boolean(editOrder)}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './order-hooks';
|
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-cancel';
|
||||||
export * from './use-order-submit';
|
export * from './use-order-submit';
|
||||||
export * from './use-order-edit';
|
export * from './use-order-edit';
|
||||||
|
export * from './use-order-event';
|
||||||
|
@ -46,10 +46,9 @@ export const useOrderCancel = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
waitForOrderEvent(args.orderId, pubKey, (cancelledOrder) => {
|
const cancelledOrder = await waitForOrderEvent(args.orderId, pubKey);
|
||||||
setCancelledOrder(cancelledOrder);
|
setCancelledOrder(cancelledOrder);
|
||||||
setComplete();
|
setComplete();
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Sentry.captureException(e);
|
Sentry.captureException(e);
|
||||||
return;
|
return;
|
||||||
|
@ -54,10 +54,9 @@ export const useOrderEdit = (order: Order | null) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
waitForOrderEvent(order.id, pubKey, (updatedOrder) => {
|
const updatedOrder = await waitForOrderEvent(order.id, pubKey);
|
||||||
setUpdatedOrder(updatedOrder);
|
setUpdatedOrder(updatedOrder);
|
||||||
setComplete();
|
setComplete();
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Sentry.captureException(e);
|
Sentry.captureException(e);
|
||||||
return;
|
return;
|
||||||
|
@ -8,44 +8,48 @@ import type {
|
|||||||
} from './';
|
} from './';
|
||||||
import type { Subscription } from 'zen-observable-ts';
|
import type { Subscription } from 'zen-observable-ts';
|
||||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
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) => {
|
export const useOrderEvent = (transaction: VegaTxState) => {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const subRef = useRef<Subscription | null>(null);
|
const subRef = useRef<Subscription | null>(null);
|
||||||
|
|
||||||
const waitForOrderEvent = useCallback(
|
const waitForOrderEvent = useCallback<WaitFunc>(
|
||||||
(
|
(id: string, partyId: string) => {
|
||||||
id: string,
|
return new Promise((resolve) => {
|
||||||
partyId: string,
|
subRef.current = client
|
||||||
callback: (order: OrderEvent_busEvents_event_Order) => void
|
.subscribe<OrderEvent, OrderEventVariables>({
|
||||||
) => {
|
query: ORDER_EVENT_SUB,
|
||||||
subRef.current = client
|
variables: { partyId },
|
||||||
.subscribe<OrderEvent, OrderEventVariables>({
|
})
|
||||||
query: ORDER_EVENT_SUB,
|
.subscribe(({ data }) => {
|
||||||
variables: { partyId },
|
if (!data?.busEvents?.length) {
|
||||||
})
|
return;
|
||||||
.subscribe(({ data }) => {
|
|
||||||
if (!data?.busEvents?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No types available for the subscription result
|
|
||||||
const matchingOrderEvent = data.busEvents.find((e) => {
|
|
||||||
if (e.event.__typename !== 'Order') {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.event.id === id;
|
// No types available for the subscription result
|
||||||
});
|
const matchingOrderEvent = data.busEvents.find((e) => {
|
||||||
|
if (e.event.__typename !== BusEventType.Order) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
return e.event.id === id;
|
||||||
matchingOrderEvent &&
|
});
|
||||||
matchingOrderEvent.event.__typename === 'Order'
|
|
||||||
) {
|
if (
|
||||||
callback(matchingOrderEvent.event);
|
matchingOrderEvent &&
|
||||||
subRef.current?.unsubscribe();
|
matchingOrderEvent.event.__typename === BusEventType.Order
|
||||||
}
|
) {
|
||||||
});
|
resolve(matchingOrderEvent.event);
|
||||||
|
subRef.current?.unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[client]
|
[client]
|
||||||
);
|
);
|
||||||
|
@ -132,10 +132,9 @@ export const useOrderSubmit = () => {
|
|||||||
if (res) {
|
if (res) {
|
||||||
const orderId = determineId(res.signature);
|
const orderId = determineId(res.signature);
|
||||||
if (orderId) {
|
if (orderId) {
|
||||||
waitForOrderEvent(orderId, pubKey, (order) => {
|
const order = await waitForOrderEvent(orderId, pubKey);
|
||||||
setFinalizedOrder(order);
|
setFinalizedOrder(order);
|
||||||
setComplete();
|
setComplete();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 {
|
export default {
|
||||||
displayName: 'positions',
|
displayName: 'positions',
|
||||||
preset: '../../jest.preset.js',
|
preset: '../../jest.preset.js',
|
||||||
globals: {
|
|
||||||
'ts-jest': {
|
|
||||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.[tj]sx?$': 'ts-jest',
|
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
coverageDirectory: '../../coverage/libs/positions',
|
coverageDirectory: '../../coverage/libs/positions',
|
||||||
|
@ -3,6 +3,5 @@ export * from './lib/positions-container';
|
|||||||
export * from './lib/positions-data-providers';
|
export * from './lib/positions-data-providers';
|
||||||
export * from './lib/positions-table';
|
export * from './lib/positions-table';
|
||||||
export * from './lib/use-close-position';
|
export * from './lib/use-close-position';
|
||||||
export * from './lib/use-position-event';
|
|
||||||
export * from './lib/use-positions-data';
|
export * from './lib/use-positions-data';
|
||||||
export * from './lib/use-positions-assets';
|
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 { useRef } from 'react';
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer, Icon, Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { Position } from './positions-data-providers';
|
import { useClosePosition, usePositionsData, PositionsTable } from '../';
|
||||||
import { PositionsTable, useClosePosition, usePositionsData } from '../';
|
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
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 {
|
interface PositionsManagerProps {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||||
const { submit, Dialog } = useClosePosition();
|
|
||||||
const onClose = useCallback(
|
|
||||||
(position: Position) => {
|
|
||||||
submit(position);
|
|
||||||
},
|
|
||||||
[submit]
|
|
||||||
);
|
|
||||||
|
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
const { data, error, loading, getRows } = usePositionsData(partyId, gridRef);
|
const { data, error, loading, getRows } = usePositionsData(partyId, gridRef);
|
||||||
|
const {
|
||||||
|
submit,
|
||||||
|
closingOrder,
|
||||||
|
closingOrderResult,
|
||||||
|
transaction,
|
||||||
|
transactionResult,
|
||||||
|
Dialog,
|
||||||
|
} = useClosePosition();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
@ -29,13 +33,66 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
|||||||
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||||
rowData={data?.length ? undefined : []}
|
rowData={data?.length ? undefined : []}
|
||||||
datasource={{ getRows }}
|
datasource={{ getRows }}
|
||||||
onClose={onClose}
|
onClose={(position) => submit(position)}
|
||||||
/>
|
/>
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
|
<Dialog
|
||||||
<Dialog>
|
intent={getDialogIntent(transactionResult)}
|
||||||
<p>Your position was not closed! This is still not implemented.</p>
|
icon={getDialogIcon(transactionResult)}
|
||||||
</Dialog>
|
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 { useCallback, useState } from 'react';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import type { TransactionResult } from '@vegaprotocol/wallet';
|
||||||
import { useVegaTransaction, determineId } 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 * as Sentry from '@sentry/react';
|
||||||
import { usePositionEvent } from '../';
|
import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types';
|
||||||
import type { Position } from '../';
|
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 = () => {
|
export const useClosePosition = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
|
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
|
||||||
const {
|
const [closingOrder, setClosingOrder] = useState<ClosingOrder>();
|
||||||
send,
|
const [closingOrderResult, setClosingOrderResult] =
|
||||||
transaction,
|
useState<OrderEvent_busEvents_event_Order>();
|
||||||
reset: resetTransaction,
|
const [transactionResult, setTransactionResult] =
|
||||||
setComplete,
|
useState<TransactionResult>();
|
||||||
Dialog,
|
const waitForTransactionResult = useTransactionResult();
|
||||||
} = useVegaTransaction();
|
const waitForOrder = useOrderEvent(transaction);
|
||||||
const waitForPositionEvent = usePositionEvent(transaction);
|
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
|
||||||
resetTransaction();
|
|
||||||
}, [resetTransaction]);
|
|
||||||
|
|
||||||
const submit = useCallback(
|
const submit = useCallback(
|
||||||
async (position: Position) => {
|
async ({
|
||||||
if (!pubKey || position.openVolume === '0') {
|
marketId,
|
||||||
|
openVolume,
|
||||||
|
}: {
|
||||||
|
marketId: string;
|
||||||
|
openVolume: string;
|
||||||
|
}) => {
|
||||||
|
if (!pubKey || openVolume === '0') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
setTransactionResult(undefined);
|
||||||
const res = await send(pubKey, {
|
setClosingOrder(undefined);
|
||||||
orderCancellation: {
|
|
||||||
marketId: position.marketId,
|
|
||||||
orderId: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res?.signature) {
|
try {
|
||||||
const resId = determineId(res.signature);
|
// figure out if opsition is long or short and make side the opposite
|
||||||
if (resId) {
|
const side = openVolume.startsWith('-')
|
||||||
waitForPositionEvent(resId, pubKey, () => {
|
? Side.SIDE_BUY
|
||||||
setComplete();
|
: 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;
|
return res;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Sentry.captureException(e);
|
Sentry.captureException(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pubKey, send, setComplete, waitForPositionEvent]
|
[pubKey, send, setComplete, waitForTransactionResult, waitForOrder]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transaction,
|
transaction,
|
||||||
Dialog,
|
transactionResult,
|
||||||
submit,
|
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",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "../../dist/out-tsc",
|
"outDir": "../../dist/out-tsc",
|
||||||
"types": ["node"]
|
"module": "commonjs",
|
||||||
|
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||||
},
|
},
|
||||||
"files": [
|
"include": [
|
||||||
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
|
||||||
"../../node_modules/@nrwl/next/typings/image.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"**/*.spec.ts",
|
|
||||||
"**/*.test.ts",
|
"**/*.test.ts",
|
||||||
"**/*.spec.tsx",
|
"**/*.spec.ts",
|
||||||
"**/*.test.tsx",
|
"**/*.test.tsx",
|
||||||
"**/*.spec.js",
|
"**/*.spec.tsx",
|
||||||
"**/*.test.js",
|
"**/*.test.js",
|
||||||
"**/*.spec.jsx",
|
"**/*.spec.js",
|
||||||
"**/*.test.jsx",
|
"**/*.test.jsx",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.d.ts",
|
||||||
"jest.config.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>
|
</Center>
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
{t(`${window.location.host} now has access to your Wallet, however you don't
|
{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>
|
</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 {
|
export interface OrderSubmissionBody {
|
||||||
orderSubmission: {
|
orderSubmission: OrderSubmission;
|
||||||
marketId: string;
|
|
||||||
reference?: string;
|
|
||||||
type: OrderType;
|
|
||||||
side: Side;
|
|
||||||
timeInForce: OrderTimeInForce;
|
|
||||||
size: string;
|
|
||||||
price?: string;
|
|
||||||
expiresAt?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderCancellationBody {
|
export interface OrderCancellationBody {
|
||||||
orderCancellation: {
|
orderCancellation: OrderCancellation;
|
||||||
orderId: string;
|
|
||||||
marketId: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderAmendmentBody {
|
export interface OrderAmendmentBody {
|
||||||
orderAmendment: {
|
orderAmendment: OrderAmendment;
|
||||||
marketId: string;
|
|
||||||
orderId: string;
|
|
||||||
reference?: string;
|
|
||||||
timeInForce: OrderTimeInForce;
|
|
||||||
sizeDelta?: number;
|
|
||||||
price?: string;
|
|
||||||
expiresAt?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VoteSubmissionBody {
|
export interface VoteSubmissionBody {
|
||||||
@ -250,6 +255,18 @@ export interface ProposalSubmissionBody {
|
|||||||
proposalSubmission: ProposalSubmission;
|
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 =
|
export type Transaction =
|
||||||
| OrderSubmissionBody
|
| OrderSubmissionBody
|
||||||
| OrderCancellationBody
|
| OrderCancellationBody
|
||||||
@ -258,7 +275,8 @@ export type Transaction =
|
|||||||
| DelegateSubmissionBody
|
| DelegateSubmissionBody
|
||||||
| UndelegateSubmissionBody
|
| UndelegateSubmissionBody
|
||||||
| OrderAmendmentBody
|
| OrderAmendmentBody
|
||||||
| ProposalSubmissionBody;
|
| ProposalSubmissionBody
|
||||||
|
| BatchMarketInstructionSubmissionBody;
|
||||||
|
|
||||||
export interface TransactionResponse {
|
export interface TransactionResponse {
|
||||||
transactionHash: string;
|
transactionHash: string;
|
||||||
|
@ -2,6 +2,7 @@ export * from './context';
|
|||||||
export * from './use-vega-wallet';
|
export * from './use-vega-wallet';
|
||||||
export * from './connectors';
|
export * from './connectors';
|
||||||
export * from './use-vega-transaction';
|
export * from './use-vega-transaction';
|
||||||
|
export * from './use-transaction-result';
|
||||||
export * from './use-eager-connect';
|
export * from './use-eager-connect';
|
||||||
export * from './manage-dialog';
|
export * from './manage-dialog';
|
||||||
export * from './vega-transaction-dialog';
|
export * from './vega-transaction-dialog';
|
||||||
@ -9,3 +10,4 @@ export * from './provider';
|
|||||||
export * from './connect-dialog';
|
export * from './connect-dialog';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './constants';
|
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 type { ReactNode } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useVegaWallet } from './use-vega-wallet';
|
import { useVegaWallet } from './use-vega-wallet';
|
||||||
|
import type { VegaTransactionContentMap } from './vega-transaction-dialog';
|
||||||
import { VegaTransactionDialog } from './vega-transaction-dialog';
|
import { VegaTransactionDialog } from './vega-transaction-dialog';
|
||||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { Transaction } from './connectors';
|
import type { Transaction } from './connectors';
|
||||||
@ -8,10 +9,10 @@ import { ClientErrors } from './connectors';
|
|||||||
import { WalletError } from './connectors';
|
import { WalletError } from './connectors';
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children?: JSX.Element;
|
|
||||||
intent?: Intent;
|
intent?: Intent;
|
||||||
title?: string;
|
title?: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
|
content?: VegaTransactionContentMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum VegaTxStatus {
|
export enum VegaTxStatus {
|
||||||
|
@ -96,25 +96,30 @@ describe('VegaTransactionDialog', () => {
|
|||||||
testBlockExplorerLink('tx-hash');
|
testBlockExplorerLink('tx-hash');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('custom complete', () => {
|
it.each(Object.keys(VegaTxStatus))(
|
||||||
render(
|
'renders custom content for %s',
|
||||||
<VegaTransactionDialog
|
(status) => {
|
||||||
{...props}
|
const title = `${status} title`;
|
||||||
transaction={{
|
const text = `${status} content`;
|
||||||
...props.transaction,
|
const content = {
|
||||||
txHash: 'tx-hash',
|
[status]: <div>{text}</div>,
|
||||||
status: VegaTxStatus.Complete,
|
};
|
||||||
}}
|
render(
|
||||||
title="Custom title"
|
<VegaTransactionDialog
|
||||||
>
|
{...props}
|
||||||
<div>Custom content</div>
|
transaction={{
|
||||||
</VegaTransactionDialog>
|
...props.transaction,
|
||||||
);
|
txHash: 'tx-hash',
|
||||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
status: status as VegaTxStatus,
|
||||||
'Custom title'
|
}}
|
||||||
);
|
title={title}
|
||||||
expect(screen.getByText('Custom content')).toBeInTheDocument();
|
content={content}
|
||||||
});
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('dialog-title')).toHaveTextContent(title);
|
||||||
|
expect(screen.getByText(text)).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function testBlockExplorerLink(txHash: string) {
|
function testBlockExplorerLink(txHash: string) {
|
||||||
expect(screen.getByTestId('tx-block-explorer')).toHaveTextContent(
|
expect(screen.getByTestId('tx-block-explorer')).toHaveTextContent(
|
||||||
|
@ -5,37 +5,32 @@ import type { ReactNode } from 'react';
|
|||||||
import type { VegaTxState } from '../use-vega-transaction';
|
import type { VegaTxState } from '../use-vega-transaction';
|
||||||
import { VegaTxStatus } from '../use-vega-transaction';
|
import { VegaTxStatus } from '../use-vega-transaction';
|
||||||
|
|
||||||
|
export type VegaTransactionContentMap = {
|
||||||
|
[C in VegaTxStatus]?: JSX.Element;
|
||||||
|
};
|
||||||
export interface VegaTransactionDialogProps {
|
export interface VegaTransactionDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onChange: (isOpen: boolean) => void;
|
onChange: (isOpen: boolean) => void;
|
||||||
transaction: VegaTxState;
|
transaction: VegaTxState;
|
||||||
children?: ReactNode;
|
|
||||||
intent?: Intent;
|
intent?: Intent;
|
||||||
title?: string;
|
title?: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
|
content?: VegaTransactionContentMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VegaTransactionDialog = ({
|
export const VegaTransactionDialog = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onChange,
|
onChange,
|
||||||
transaction,
|
transaction,
|
||||||
children,
|
|
||||||
intent,
|
intent,
|
||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
|
content,
|
||||||
}: VegaTransactionDialogProps) => {
|
}: VegaTransactionDialogProps) => {
|
||||||
const computedIntent = intent ? intent : getIntent(transaction);
|
const computedIntent = intent ? intent : getIntent(transaction);
|
||||||
const computedTitle = title ? title : getTitle(transaction);
|
const computedTitle = title ? title : getTitle(transaction);
|
||||||
const computedIcon = icon ? icon : getIcon(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 (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
@ -45,10 +40,42 @@ export const VegaTransactionDialog = ({
|
|||||||
icon={computedIcon}
|
icon={computedIcon}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
{content}
|
<Content transaction={transaction} content={content} />
|
||||||
</Dialog>
|
</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 {
|
interface VegaDialogProps {
|
||||||
transaction: VegaTxState;
|
transaction: VegaTxState;
|
||||||
|
@ -35,17 +35,21 @@ export const WithdrawalDialogs = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<createWithdraw.Dialog>
|
<createWithdraw.Dialog
|
||||||
<WithdrawalFeedback
|
content={{
|
||||||
withdrawal={createWithdraw.withdrawal}
|
Complete: (
|
||||||
transaction={createWithdraw.transaction}
|
<WithdrawalFeedback
|
||||||
availableTimestamp={createWithdraw.availableTimestamp}
|
withdrawal={createWithdraw.withdrawal}
|
||||||
submitWithdraw={(id) => {
|
transaction={createWithdraw.transaction}
|
||||||
createWithdraw.reset();
|
availableTimestamp={createWithdraw.availableTimestamp}
|
||||||
completeWithdraw.submit(id);
|
submitWithdraw={(id) => {
|
||||||
}}
|
createWithdraw.reset();
|
||||||
/>
|
completeWithdraw.submit(id);
|
||||||
</createWithdraw.Dialog>
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<completeWithdraw.Dialog />
|
<completeWithdraw.Dialog />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user