fix(trading): order tx toasts title and intent should be set by status (#3368)
This commit is contained in:
parent
363ca9c6e1
commit
ed78261aad
@ -39,10 +39,14 @@ import { t } from '@vegaprotocol/i18n';
|
|||||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||||
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
||||||
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
|
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
|
||||||
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
|
import {
|
||||||
|
getOrderToastIntent,
|
||||||
|
getOrderToastTitle,
|
||||||
|
getRejectionReason,
|
||||||
|
useOrderByIdQuery,
|
||||||
|
} from '@vegaprotocol/orders';
|
||||||
import { useMarketList } from '@vegaprotocol/market-list';
|
import { useMarketList } from '@vegaprotocol/market-list';
|
||||||
import type { Side } from '@vegaprotocol/types';
|
import type { Side } from '@vegaprotocol/types';
|
||||||
import { OrderStatus } from '@vegaprotocol/types';
|
|
||||||
import { OrderStatusMapping } from '@vegaprotocol/types';
|
import { OrderStatusMapping } from '@vegaprotocol/types';
|
||||||
import { Size } from '@vegaprotocol/react-helpers';
|
import { Size } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
@ -478,12 +482,10 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
|||||||
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
|
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToastHeading>{t('Order rejected')}</ToastHeading>
|
<ToastHeading>{getOrderToastTitle(tx.order.status)}</ToastHeading>
|
||||||
{rejectionReason || tx.order.rejectionReason ? (
|
{rejectionReason ? (
|
||||||
<p>
|
<p>
|
||||||
{t('Your order has been rejected because: %s', [
|
{t('Your order has been rejected because: %s', [rejectionReason])}
|
||||||
rejectionReason || tx.order.rejectionReason,
|
|
||||||
])}
|
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p>{t('Your order has been rejected.')}</p>
|
<p>{t('Your order has been rejected.')}</p>
|
||||||
@ -506,7 +508,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
|||||||
if (isOrderSubmissionTransaction(tx.body) && tx.order?.rejectionReason) {
|
if (isOrderSubmissionTransaction(tx.body) && tx.order?.rejectionReason) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold">{t('Order rejected')}</h3>
|
<h3 className="font-bold">{getOrderToastTitle(tx.order.status)}</h3>
|
||||||
<p>{t('Your order was rejected.')}</p>
|
<p>{t('Your order was rejected.')}</p>
|
||||||
{tx.txHash && (
|
{tx.txHash && (
|
||||||
<p className="break-all">
|
<p className="break-all">
|
||||||
@ -580,7 +582,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
|
|||||||
tx.error instanceof WalletError &&
|
tx.error instanceof WalletError &&
|
||||||
walletNoConnectionCodes.includes(tx.error.code);
|
walletNoConnectionCodes.includes(tx.error.code);
|
||||||
if (orderRejection) {
|
if (orderRejection) {
|
||||||
label = t('Order rejected');
|
label = getOrderToastTitle(tx.order?.status) || t('Order rejected');
|
||||||
errorMessage = t('Your order has been rejected because: %s', [
|
errorMessage = t('Your order has been rejected because: %s', [
|
||||||
orderRejection || tx.order?.rejectionReason || ' ',
|
orderRejection || tx.order?.rejectionReason || ' ',
|
||||||
]);
|
]);
|
||||||
@ -649,13 +651,8 @@ export const useVegaTransactionToasts = () => {
|
|||||||
|
|
||||||
// Transaction can be successful but the order can be rejected by the network
|
// Transaction can be successful but the order can be rejected by the network
|
||||||
const intent =
|
const intent =
|
||||||
(tx.order &&
|
(tx.order && getOrderToastIntent(tx.order.status)) ||
|
||||||
[OrderStatus.STATUS_REJECTED, OrderStatus.STATUS_STOPPED].includes(
|
intentMap[tx.status];
|
||||||
tx.order.status
|
|
||||||
)) ||
|
|
||||||
tx.order?.rejectionReason
|
|
||||||
? Intent.Danger
|
|
||||||
: intentMap[tx.status];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `vega-${tx.id}`,
|
id: `vega-${tx.id}`,
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
export * from './__generated__/OrdersSubscription';
|
export * from './__generated__/OrdersSubscription';
|
||||||
export * from './use-has-active-order';
|
export * from './use-has-active-order';
|
||||||
export * from './use-order-cancel';
|
|
||||||
export * from './use-order-edit';
|
|
||||||
export * from './use-order-submit';
|
|
||||||
export * from './use-order-update';
|
export * from './use-order-update';
|
||||||
export * from './use-pending-orders-volume';
|
export * from './use-pending-orders-volume';
|
||||||
export * from './use-order-store';
|
export * from './use-order-store';
|
||||||
|
@ -1,138 +0,0 @@
|
|||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { act, renderHook } from '@testing-library/react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
|
|
||||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
|
||||||
import { useOrderCancel } from './use-order-cancel';
|
|
||||||
import type { OrderSubSubscription } from './';
|
|
||||||
import { OrderSubDocument } from './';
|
|
||||||
import * as Schema from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
const defaultWalletContext = {
|
|
||||||
pubKey: null,
|
|
||||||
pubKeys: [],
|
|
||||||
isReadOnly: false,
|
|
||||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
|
||||||
connect: jest.fn(),
|
|
||||||
disconnect: jest.fn(),
|
|
||||||
selectPubKey: jest.fn(),
|
|
||||||
connector: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
function setup(context?: Partial<VegaWalletContextShape>) {
|
|
||||||
const mocks: MockedResponse<OrderSubSubscription> = {
|
|
||||||
request: {
|
|
||||||
query: OrderSubDocument,
|
|
||||||
variables: {
|
|
||||||
partyId: context?.pubKey || '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
orders: [
|
|
||||||
{
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
|
||||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
|
||||||
rejectionReason: null,
|
|
||||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
size: '10',
|
|
||||||
price: '300000',
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
marketId: 'market-id',
|
|
||||||
__typename: 'OrderUpdate',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const filterMocks: MockedResponse<OrderSubSubscription> = {
|
|
||||||
request: {
|
|
||||||
query: OrderSubDocument,
|
|
||||||
variables: {
|
|
||||||
partyId: context?.pubKey || '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
orders: [
|
|
||||||
{
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
|
||||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
|
||||||
rejectionReason: null,
|
|
||||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
size: '10',
|
|
||||||
price: '300000',
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
marketId: 'market-id',
|
|
||||||
__typename: 'OrderUpdate',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
||||||
<MockedProvider mocks={[mocks, filterMocks]}>
|
|
||||||
<VegaWalletContext.Provider
|
|
||||||
value={{ ...defaultWalletContext, ...context }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</VegaWalletContext.Provider>
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
return renderHook(() => useOrderCancel(), { wrapper });
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('useOrderCancel', () => {
|
|
||||||
it('has the correct default state', () => {
|
|
||||||
const { result } = setup();
|
|
||||||
expect(typeof result.current.cancel).toEqual('function');
|
|
||||||
expect(typeof result.current.reset).toEqual('function');
|
|
||||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
|
||||||
expect(result.current.transaction.txHash).toEqual(null);
|
|
||||||
expect(result.current.transaction.error).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sendTx if no keypair', () => {
|
|
||||||
const mockSendTx = jest.fn();
|
|
||||||
const { result } = setup({
|
|
||||||
sendTx: mockSendTx,
|
|
||||||
pubKeys: [],
|
|
||||||
pubKey: null,
|
|
||||||
});
|
|
||||||
act(() => {
|
|
||||||
result.current.cancel({ orderId: 'order-id', marketId: 'market-id' });
|
|
||||||
});
|
|
||||||
expect(mockSendTx).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should cancel a correctly formatted order', async () => {
|
|
||||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
|
||||||
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
|
|
||||||
const { result } = setup({
|
|
||||||
sendTx: mockSendTx,
|
|
||||||
pubKeys: [pubKeyObj],
|
|
||||||
pubKey: pubKeyObj.publicKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const args = {
|
|
||||||
orderId: 'order-id',
|
|
||||||
marketId: 'market-id',
|
|
||||||
};
|
|
||||||
act(() => {
|
|
||||||
result.current.cancel(args);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
|
|
||||||
orderCancellation: args,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,83 +0,0 @@
|
|||||||
import { useCallback, useState } from 'react';
|
|
||||||
import {
|
|
||||||
useVegaWallet,
|
|
||||||
useVegaTransaction,
|
|
||||||
useTransactionResult,
|
|
||||||
} from '@vegaprotocol/wallet';
|
|
||||||
import type {
|
|
||||||
OrderCancellationBody,
|
|
||||||
TransactionResult,
|
|
||||||
} from '@vegaprotocol/wallet';
|
|
||||||
import type { OrderSubFieldsFragment } from './';
|
|
||||||
import * as Sentry from '@sentry/react';
|
|
||||||
import { useOrderUpdate } from './use-order-update';
|
|
||||||
|
|
||||||
export const useOrderCancel = () => {
|
|
||||||
const { pubKey } = useVegaWallet();
|
|
||||||
|
|
||||||
const [cancelledOrder, setCancelledOrder] =
|
|
||||||
useState<OrderSubFieldsFragment | null>(null);
|
|
||||||
const [transactionResult, setTransactionResult] =
|
|
||||||
useState<TransactionResult>();
|
|
||||||
|
|
||||||
const {
|
|
||||||
send,
|
|
||||||
transaction,
|
|
||||||
reset: resetTransaction,
|
|
||||||
setComplete,
|
|
||||||
Dialog,
|
|
||||||
} = useVegaTransaction();
|
|
||||||
|
|
||||||
const waitForOrderUpdate = useOrderUpdate(transaction);
|
|
||||||
const waitForTransactionResult = useTransactionResult();
|
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
|
||||||
resetTransaction();
|
|
||||||
setCancelledOrder(null);
|
|
||||||
}, [resetTransaction]);
|
|
||||||
|
|
||||||
const cancel = useCallback(
|
|
||||||
async (orderCancellation: OrderCancellationBody['orderCancellation']) => {
|
|
||||||
if (!pubKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCancelledOrder(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await send(pubKey, {
|
|
||||||
orderCancellation,
|
|
||||||
});
|
|
||||||
if (orderCancellation.orderId) {
|
|
||||||
const cancelledOrder = await waitForOrderUpdate(
|
|
||||||
orderCancellation.orderId,
|
|
||||||
pubKey
|
|
||||||
);
|
|
||||||
setCancelledOrder(cancelledOrder);
|
|
||||||
setComplete();
|
|
||||||
} else if (res) {
|
|
||||||
const txResult = await waitForTransactionResult(
|
|
||||||
res.transactionHash,
|
|
||||||
pubKey
|
|
||||||
);
|
|
||||||
setTransactionResult(txResult);
|
|
||||||
setComplete();
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
Sentry.captureException(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[pubKey, send, setComplete, waitForOrderUpdate, waitForTransactionResult]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
transaction,
|
|
||||||
transactionResult,
|
|
||||||
cancelledOrder,
|
|
||||||
Dialog,
|
|
||||||
cancel,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,179 +0,0 @@
|
|||||||
import { act, renderHook } from '@testing-library/react';
|
|
||||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
|
||||||
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { useOrderEdit } from './use-order-edit';
|
|
||||||
import type { OrderSubSubscription } from './__generated__/OrdersSubscription';
|
|
||||||
import { OrderSubDocument } from './__generated__/OrdersSubscription';
|
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import type { Order } from '../components';
|
|
||||||
import { generateOrder } from '../components';
|
|
||||||
import * as Schema from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
const defaultWalletContext = {
|
|
||||||
pubKey: null,
|
|
||||||
pubKeys: [],
|
|
||||||
isReadOnly: false,
|
|
||||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
|
||||||
connect: jest.fn(),
|
|
||||||
disconnect: jest.fn(),
|
|
||||||
selectPubKey: jest.fn(),
|
|
||||||
connector: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
function setup(order: Order, context?: Partial<VegaWalletContextShape>) {
|
|
||||||
const mocks: MockedResponse<OrderSubSubscription> = {
|
|
||||||
request: {
|
|
||||||
query: OrderSubDocument,
|
|
||||||
variables: {
|
|
||||||
partyId: context?.pubKey || '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
orders: [
|
|
||||||
{
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
|
||||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
|
||||||
rejectionReason: null,
|
|
||||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
size: '10',
|
|
||||||
price: '300000',
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
marketId: 'market-id',
|
|
||||||
__typename: 'OrderUpdate',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const filterMocks: MockedResponse<OrderSubSubscription> = {
|
|
||||||
request: {
|
|
||||||
query: OrderSubDocument,
|
|
||||||
variables: {
|
|
||||||
partyId: context?.pubKey || '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
orders: [
|
|
||||||
{
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
|
||||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
|
||||||
rejectionReason: null,
|
|
||||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
size: '10',
|
|
||||||
price: '300000',
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
marketId: 'market-id',
|
|
||||||
__typename: 'OrderUpdate',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
||||||
<MockedProvider mocks={[mocks, filterMocks]}>
|
|
||||||
<VegaWalletContext.Provider
|
|
||||||
value={{ ...defaultWalletContext, ...context }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</VegaWalletContext.Provider>
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
return renderHook(() => useOrderEdit(order), { wrapper });
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('useOrderEdit', () => {
|
|
||||||
it('should edit a correctly formatted order if there is no size', async () => {
|
|
||||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
|
||||||
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
|
|
||||||
const order = generateOrder({
|
|
||||||
price: '123456789',
|
|
||||||
market: { decimalPlaces: 2 },
|
|
||||||
});
|
|
||||||
const { result } = setup(order, {
|
|
||||||
sendTx: mockSendTx,
|
|
||||||
pubKeys: [pubKeyObj],
|
|
||||||
pubKey: pubKeyObj.publicKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.edit({ price: '1234567.89' });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
|
|
||||||
orderAmendment: {
|
|
||||||
orderId: order.id,
|
|
||||||
// eslint-disable-next-line
|
|
||||||
marketId: order.market!.id,
|
|
||||||
timeInForce: order.timeInForce,
|
|
||||||
price: '123456789', // Decimal removed
|
|
||||||
sizeDelta: 0,
|
|
||||||
expiresAt: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should edit a correctly formatted order', async () => {
|
|
||||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
|
||||||
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
|
|
||||||
const order = generateOrder({
|
|
||||||
price: '123456789',
|
|
||||||
market: { decimalPlaces: 2 },
|
|
||||||
});
|
|
||||||
const { result } = setup(order, {
|
|
||||||
sendTx: mockSendTx,
|
|
||||||
pubKeys: [pubKeyObj],
|
|
||||||
pubKey: pubKeyObj.publicKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.edit({ price: '1234567.89', size: '20' });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
|
|
||||||
orderAmendment: {
|
|
||||||
orderId: order.id,
|
|
||||||
// eslint-disable-next-line
|
|
||||||
marketId: order.market!.id,
|
|
||||||
timeInForce: order.timeInForce,
|
|
||||||
price: '123456789', // Decimal removed
|
|
||||||
sizeDelta: 1990,
|
|
||||||
expiresAt: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has the correct default state', () => {
|
|
||||||
const order = generateOrder();
|
|
||||||
const { result } = setup(order);
|
|
||||||
expect(typeof result.current.edit).toEqual('function');
|
|
||||||
expect(typeof result.current.reset).toEqual('function');
|
|
||||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
|
||||||
expect(result.current.transaction.txHash).toEqual(null);
|
|
||||||
expect(result.current.transaction.error).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sendTx if no keypair', async () => {
|
|
||||||
const order = generateOrder();
|
|
||||||
const mockSendTx = jest.fn();
|
|
||||||
const { result } = setup(order, {
|
|
||||||
sendTx: mockSendTx,
|
|
||||||
pubKeys: [],
|
|
||||||
pubKey: null,
|
|
||||||
});
|
|
||||||
await act(async () => {
|
|
||||||
result.current.edit(order);
|
|
||||||
});
|
|
||||||
expect(mockSendTx).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,82 +0,0 @@
|
|||||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
|
||||||
import { useState, useCallback } from 'react';
|
|
||||||
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
|
||||||
import type { OrderSubFieldsFragment } from './';
|
|
||||||
import * as Sentry from '@sentry/react';
|
|
||||||
import type { Order } from '../components';
|
|
||||||
import { useOrderUpdate } from './use-order-update';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
|
|
||||||
export interface EditOrderArgs {
|
|
||||||
price: string;
|
|
||||||
size?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useOrderEdit = (order: Order | null) => {
|
|
||||||
const { pubKey } = useVegaWallet();
|
|
||||||
|
|
||||||
const [updatedOrder, setUpdatedOrder] =
|
|
||||||
useState<OrderSubFieldsFragment | null>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
send,
|
|
||||||
transaction,
|
|
||||||
reset: resetTransaction,
|
|
||||||
setComplete,
|
|
||||||
Dialog,
|
|
||||||
} = useVegaTransaction();
|
|
||||||
|
|
||||||
const waitForOrderUpdate = useOrderUpdate(transaction);
|
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
|
||||||
resetTransaction();
|
|
||||||
setUpdatedOrder(null);
|
|
||||||
}, [resetTransaction]);
|
|
||||||
|
|
||||||
const edit = useCallback(
|
|
||||||
async (args: EditOrderArgs) => {
|
|
||||||
if (!pubKey || !order || !order.market) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUpdatedOrder(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await send(pubKey, {
|
|
||||||
orderAmendment: {
|
|
||||||
orderId: order.id,
|
|
||||||
marketId: order.market.id,
|
|
||||||
price: removeDecimal(args.price, order.market.decimalPlaces),
|
|
||||||
timeInForce: order.timeInForce,
|
|
||||||
sizeDelta: args.size
|
|
||||||
? new BigNumber(
|
|
||||||
removeDecimal(args.size, order.market.positionDecimalPlaces)
|
|
||||||
)
|
|
||||||
.minus(order.size)
|
|
||||||
.toNumber()
|
|
||||||
: 0,
|
|
||||||
expiresAt: order.expiresAt
|
|
||||||
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedOrder = await waitForOrderUpdate(order.id, pubKey);
|
|
||||||
setUpdatedOrder(updatedOrder);
|
|
||||||
setComplete();
|
|
||||||
} catch (e) {
|
|
||||||
Sentry.captureException(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[pubKey, send, order, setComplete, waitForOrderUpdate]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
transaction,
|
|
||||||
updatedOrder,
|
|
||||||
Dialog,
|
|
||||||
edit,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,205 +0,0 @@
|
|||||||
import { act, renderHook } from '@testing-library/react';
|
|
||||||
import type { PubKey, VegaWalletContextShape } from '@vegaprotocol/wallet';
|
|
||||||
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
|
|
||||||
import * as Schema from '@vegaprotocol/types';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
|
||||||
import { useOrderSubmit } from './use-order-submit';
|
|
||||||
import type { OrderSubSubscription } from './';
|
|
||||||
import { OrderSubDocument } from './';
|
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
|
|
||||||
const marketId = 'market-id';
|
|
||||||
|
|
||||||
const defaultWalletContext = {
|
|
||||||
pubKey: null,
|
|
||||||
pubKeys: [],
|
|
||||||
isReadOnly: false,
|
|
||||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
|
||||||
connect: jest.fn(),
|
|
||||||
disconnect: jest.fn(),
|
|
||||||
selectPubKey: jest.fn(),
|
|
||||||
connector: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
function setup(context?: Partial<VegaWalletContextShape>) {
|
|
||||||
const mocks: MockedResponse<OrderSubSubscription> = {
|
|
||||||
request: {
|
|
||||||
query: OrderSubDocument,
|
|
||||||
variables: {
|
|
||||||
partyId: context?.pubKey || '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
orders: [
|
|
||||||
{
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
|
||||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
|
||||||
rejectionReason: null,
|
|
||||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
size: '10',
|
|
||||||
price: '300000',
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
marketId: 'market-id',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const filterMocks: MockedResponse<OrderSubSubscription> = {
|
|
||||||
request: {
|
|
||||||
query: OrderSubDocument,
|
|
||||||
variables: {
|
|
||||||
partyId: context?.pubKey || '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
orders: [
|
|
||||||
{
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
|
||||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
|
||||||
rejectionReason: null,
|
|
||||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
|
||||||
size: '10',
|
|
||||||
price: '300000',
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
marketId: 'market-id',
|
|
||||||
__typename: 'OrderUpdate',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
||||||
<MockedProvider mocks={[mocks, filterMocks]}>
|
|
||||||
<VegaWalletContext.Provider
|
|
||||||
value={{ ...defaultWalletContext, ...context }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</VegaWalletContext.Provider>
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
return renderHook(() => useOrderSubmit(), { wrapper });
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('useOrderSubmit', () => {
|
|
||||||
it('should submit a correctly formatted order on GTT', async () => {
|
|
||||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
|
||||||
const pubKey = '0x123';
|
|
||||||
const { result } = setup({
|
|
||||||
sendTx: mockSendTx,
|
|
||||||
pubKeys: [{ publicKey: pubKey, name: 'test key 1' }],
|
|
||||||
pubKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const order = {
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
size: '10',
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
price: '123456789',
|
|
||||||
expiresAt: new Date('2022-01-01').toISOString(),
|
|
||||||
};
|
|
||||||
await act(async () => {
|
|
||||||
result.current.submit({ ...order, marketId });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockSendTx).toHaveBeenCalledWith(pubKey, {
|
|
||||||
orderSubmission: {
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
marketId,
|
|
||||||
size: '10',
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
|
||||||
price: '123456789',
|
|
||||||
expiresAt: new Date('2022-01-01').toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should submit a correctly formatted order on GTC', async () => {
|
|
||||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
|
||||||
const publicKeyObj: PubKey = {
|
|
||||||
publicKey: '0x123',
|
|
||||||
name: 'test key 1',
|
|
||||||
};
|
|
||||||
const { result } = setup({
|
|
||||||
sendTx: mockSendTx,
|
|
||||||
pubKeys: [publicKeyObj],
|
|
||||||
pubKey: publicKeyObj.publicKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const order = {
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
size: '10',
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
price: '123456789',
|
|
||||||
expiresAt: new Date('2022-01-01').toISOString(),
|
|
||||||
};
|
|
||||||
await act(async () => {
|
|
||||||
result.current.submit({ ...order, marketId });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockSendTx).toHaveBeenCalledWith(publicKeyObj.publicKey, {
|
|
||||||
orderSubmission: {
|
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
|
||||||
marketId,
|
|
||||||
size: '10',
|
|
||||||
side: Schema.Side.SIDE_BUY,
|
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
price: '123456789',
|
|
||||||
expiresAt: new Date('2022-01-01').toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has the correct default state', () => {
|
|
||||||
const { result } = setup();
|
|
||||||
expect(typeof result.current.submit).toEqual('function');
|
|
||||||
expect(typeof result.current.reset).toEqual('function');
|
|
||||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
|
||||||
expect(result.current.transaction.txHash).toEqual(null);
|
|
||||||
expect(result.current.transaction.error).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sendTx if no keypair', async () => {
|
|
||||||
const mockSendTx = jest.fn();
|
|
||||||
const { result } = setup({
|
|
||||||
sendTx: mockSendTx,
|
|
||||||
pubKeys: [],
|
|
||||||
pubKey: null,
|
|
||||||
});
|
|
||||||
await act(async () => {
|
|
||||||
result.current.submit({} as OrderSubmissionBody['orderSubmission']);
|
|
||||||
});
|
|
||||||
expect(mockSendTx).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sendTx side is not specified', async () => {
|
|
||||||
const mockSendTx = jest.fn();
|
|
||||||
const publicKeyObj: PubKey = {
|
|
||||||
publicKey: '0x123',
|
|
||||||
name: 'test key 1',
|
|
||||||
};
|
|
||||||
const { result } = setup({
|
|
||||||
sendTx: mockSendTx,
|
|
||||||
pubKeys: [publicKeyObj],
|
|
||||||
pubKey: publicKeyObj.publicKey,
|
|
||||||
});
|
|
||||||
await act(async () => {
|
|
||||||
result.current.submit({} as OrderSubmissionBody['orderSubmission']);
|
|
||||||
});
|
|
||||||
expect(mockSendTx).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,142 +0,0 @@
|
|||||||
import { useCallback, useState } from 'react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import type { OrderSubFieldsFragment } from './__generated__/OrdersSubscription';
|
|
||||||
import {
|
|
||||||
useVegaWallet,
|
|
||||||
useVegaTransaction,
|
|
||||||
determineId,
|
|
||||||
} from '@vegaprotocol/wallet';
|
|
||||||
import * as Sentry from '@sentry/react';
|
|
||||||
import { useOrderUpdate } from './use-order-update';
|
|
||||||
import * as Schema from '@vegaprotocol/types';
|
|
||||||
import { Icon, Intent } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
|
||||||
|
|
||||||
export const getOrderDialogTitle = (
|
|
||||||
status?: Schema.OrderStatus
|
|
||||||
): string | undefined => {
|
|
||||||
if (!status) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case Schema.OrderStatus.STATUS_ACTIVE:
|
|
||||||
return t('Order submitted');
|
|
||||||
case Schema.OrderStatus.STATUS_FILLED:
|
|
||||||
return t('Order filled');
|
|
||||||
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
|
||||||
return t('Order partially filled');
|
|
||||||
case Schema.OrderStatus.STATUS_PARKED:
|
|
||||||
return t('Order parked');
|
|
||||||
case Schema.OrderStatus.STATUS_STOPPED:
|
|
||||||
return t('Order stopped');
|
|
||||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
|
||||||
return t('Order cancelled');
|
|
||||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
|
||||||
return t('Order expired');
|
|
||||||
case Schema.OrderStatus.STATUS_REJECTED:
|
|
||||||
return t('Order rejected');
|
|
||||||
default:
|
|
||||||
return t('Submission failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOrderDialogIntent = (
|
|
||||||
status?: Schema.OrderStatus
|
|
||||||
): Intent | undefined => {
|
|
||||||
if (!status) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (status) {
|
|
||||||
case Schema.OrderStatus.STATUS_PARKED:
|
|
||||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
|
||||||
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
|
||||||
return Intent.Warning;
|
|
||||||
case Schema.OrderStatus.STATUS_REJECTED:
|
|
||||||
case Schema.OrderStatus.STATUS_STOPPED:
|
|
||||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
|
||||||
return Intent.Danger;
|
|
||||||
case Schema.OrderStatus.STATUS_FILLED:
|
|
||||||
case Schema.OrderStatus.STATUS_ACTIVE:
|
|
||||||
return Intent.Success;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOrderDialogIcon = (
|
|
||||||
status?: Schema.OrderStatus
|
|
||||||
): ReactNode | undefined => {
|
|
||||||
if (!status) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case Schema.OrderStatus.STATUS_PARKED:
|
|
||||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
|
||||||
return <Icon name="warning-sign" size={16} />;
|
|
||||||
case Schema.OrderStatus.STATUS_REJECTED:
|
|
||||||
case Schema.OrderStatus.STATUS_STOPPED:
|
|
||||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
|
||||||
return <Icon name="error" size={16} />;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useOrderSubmit = () => {
|
|
||||||
const { pubKey } = useVegaWallet();
|
|
||||||
|
|
||||||
const {
|
|
||||||
send,
|
|
||||||
transaction,
|
|
||||||
reset: resetTransaction,
|
|
||||||
setComplete,
|
|
||||||
Dialog,
|
|
||||||
} = useVegaTransaction();
|
|
||||||
|
|
||||||
const waitForOrderUpdate = useOrderUpdate(transaction);
|
|
||||||
|
|
||||||
const [finalizedOrder, setFinalizedOrder] =
|
|
||||||
useState<OrderSubFieldsFragment | null>(null);
|
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
|
||||||
resetTransaction();
|
|
||||||
setFinalizedOrder(null);
|
|
||||||
}, [resetTransaction]);
|
|
||||||
|
|
||||||
const submit = useCallback(
|
|
||||||
async (orderSubmission: OrderSubmissionBody['orderSubmission']) => {
|
|
||||||
if (!pubKey || !orderSubmission.side) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFinalizedOrder(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await send(pubKey, { orderSubmission });
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
const orderId = determineId(res.signature);
|
|
||||||
if (orderId) {
|
|
||||||
const order = await waitForOrderUpdate(orderId, pubKey);
|
|
||||||
setFinalizedOrder(order);
|
|
||||||
setComplete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Sentry.captureException(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[pubKey, send, setComplete, waitForOrderUpdate]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
transaction,
|
|
||||||
finalizedOrder,
|
|
||||||
Dialog,
|
|
||||||
submit,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,6 +1,7 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import type { OrderSubFieldsFragment } from './order-hooks';
|
import type { OrderSubFieldsFragment } from './order-hooks';
|
||||||
|
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
// More detail in https://docs.vega.xyz/mainnet/graphql/enums/order-time-in-force
|
// More detail in https://docs.vega.xyz/mainnet/graphql/enums/order-time-in-force
|
||||||
export const timeInForceLabel = (tif: string) => {
|
export const timeInForceLabel = (tif: string) => {
|
||||||
@ -38,3 +39,55 @@ export const getRejectionReason = (
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getOrderToastTitle = (
|
||||||
|
status?: Schema.OrderStatus
|
||||||
|
): string | undefined => {
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case Schema.OrderStatus.STATUS_ACTIVE:
|
||||||
|
return t('Order submitted');
|
||||||
|
case Schema.OrderStatus.STATUS_FILLED:
|
||||||
|
return t('Order filled');
|
||||||
|
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
||||||
|
return t('Order partially filled');
|
||||||
|
case Schema.OrderStatus.STATUS_PARKED:
|
||||||
|
return t('Order parked');
|
||||||
|
case Schema.OrderStatus.STATUS_STOPPED:
|
||||||
|
return t('Order stopped');
|
||||||
|
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||||
|
return t('Order cancelled');
|
||||||
|
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||||
|
return t('Order expired');
|
||||||
|
case Schema.OrderStatus.STATUS_REJECTED:
|
||||||
|
return t('Order rejected');
|
||||||
|
default:
|
||||||
|
return t('Submission failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOrderToastIntent = (
|
||||||
|
status?: Schema.OrderStatus
|
||||||
|
): Intent | undefined => {
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case Schema.OrderStatus.STATUS_PARKED:
|
||||||
|
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||||
|
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
||||||
|
return Intent.Warning;
|
||||||
|
case Schema.OrderStatus.STATUS_REJECTED:
|
||||||
|
case Schema.OrderStatus.STATUS_STOPPED:
|
||||||
|
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||||
|
return Intent.Danger;
|
||||||
|
case Schema.OrderStatus.STATUS_FILLED:
|
||||||
|
case Schema.OrderStatus.STATUS_ACTIVE:
|
||||||
|
return Intent.Success;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user