import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import first from 'lodash/first';
import compact from 'lodash/compact';
import type {
BatchMarketInstructionSubmissionBody,
OrderAmendment,
OrderTxUpdateFieldsFragment,
OrderCancellationBody,
OrderSubmission,
VegaStoredTxState,
WithdrawalBusEventFieldsFragment,
} from '@vegaprotocol/wallet';
import {
isTransferTransaction,
isBatchMarketInstructionsTransaction,
ClientErrors,
useReconnectVegaWallet,
WalletError,
isOrderAmendmentTransaction,
isOrderCancellationTransaction,
isOrderSubmissionTransaction,
isWithdrawTransaction,
useVegaTransactionStore,
VegaTxStatus,
} from '@vegaprotocol/wallet';
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
import { Button, ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
import {
addDecimalsFormatNumber,
formatNumber,
Size,
t,
toBigNum,
truncateByChars,
} from '@vegaprotocol/react-helpers';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
import { useMarketList } from '@vegaprotocol/market-list';
import type { Side } from '@vegaprotocol/types';
import { OrderStatusMapping } from '@vegaprotocol/types';
const intentMap: { [s in VegaTxStatus]: Intent } = {
Default: Intent.Primary,
Requested: Intent.Warning,
Pending: Intent.Warning,
Error: Intent.Danger,
Complete: Intent.Success,
};
const getIntent = (tx: VegaStoredTxState) => {
// Transaction can be successful
// But the order can be rejected by the network
if (tx.order?.rejectionReason) {
return Intent.Danger;
}
return intentMap[tx.status];
};
const isClosePositionTransaction = (tx: VegaStoredTxState) => {
if (isBatchMarketInstructionsTransaction(tx.body)) {
const amendments =
tx.body.batchMarketInstructions.amendments &&
tx.body.batchMarketInstructions.amendments?.length > 0;
const cancellation =
tx.body.batchMarketInstructions.cancellations?.length === 1 &&
tx.body.batchMarketInstructions.cancellations[0].orderId === '' &&
tx.body.batchMarketInstructions.cancellations[0];
const submission =
cancellation &&
tx.body.batchMarketInstructions.submissions?.length === 1 &&
tx.body.batchMarketInstructions.submissions[0].marketId ===
cancellation.marketId;
return !amendments && cancellation && submission;
}
return false;
};
const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
const withdraw = isWithdrawTransaction(tx.body);
const submitOrder = isOrderSubmissionTransaction(tx.body);
const cancelOrder = isOrderCancellationTransaction(tx.body);
const editOrder = isOrderAmendmentTransaction(tx.body);
const batchMarketInstructions = isBatchMarketInstructionsTransaction(tx.body);
const transfer = isTransferTransaction(tx.body);
return (
withdraw ||
submitOrder ||
cancelOrder ||
editOrder ||
batchMarketInstructions ||
transfer
);
};
const Details = ({
children,
title = '',
}: {
children: ReactNode;
title?: string;
}) => (
);
type SizeAtPriceProps = {
side: Side;
size: string;
price: string | undefined;
meta: { positionDecimalPlaces: number; decimalPlaces: number; asset: string };
};
const SizeAtPrice = ({ side, size, price, meta }: SizeAtPriceProps) => {
return (
<>
{' '}
{price && price !== '0' && meta.decimalPlaces
? `@ ${addDecimalsFormatNumber(price, meta.decimalPlaces)} ${
meta.asset
}`
: `@ ~ ${meta.asset}`}
>
);
};
const SubmitOrderDetails = ({
data,
order,
}: {
data: OrderSubmission;
order?: OrderTxUpdateFieldsFragment;
}) => {
const { data: markets } = useMarketList();
const market = markets?.find((m) => m.id === order?.marketId);
if (!market) return null;
const price = order ? order.price : data.price;
const size = order ? order.size : data.size;
const side = order ? order.side : data.side;
return (
{order
? t(
`Submit order - ${OrderStatusMapping[order.status].toLowerCase()}`
)
: t('Submit order')}
{market?.tradableInstrument.instrument.code}
{order && order.rejectionReason && (
{getRejectionReason(order)}
)}
);
};
const EditOrderDetails = ({
data,
order,
}: {
data: OrderAmendment;
order?: OrderTxUpdateFieldsFragment;
}) => {
const { data: orderById } = useOrderByIdQuery({
variables: { orderId: data.orderId },
});
const { data: markets } = useMarketList();
const originalOrder = orderById?.orderByID;
if (!originalOrder) return null;
const market = markets?.find((m) => m.id === originalOrder.market.id);
if (!market) return null;
const original = (
);
const edited = (
);
return (
{order
? t(`Edit order - ${OrderStatusMapping[order.status].toLowerCase()}`)
: t('Edit order')}
{market?.tradableInstrument.instrument.code}
{original}
{edited}
{order && order.rejectionReason && (
{getRejectionReason(order)}
)}
);
};
const CancelOrderDetails = ({
orderId,
order,
}: {
orderId: string;
order?: OrderTxUpdateFieldsFragment;
}) => {
const { data: orderById } = useOrderByIdQuery({
variables: { orderId },
});
const { data: markets } = useMarketList();
const originalOrder = orderById?.orderByID;
if (!originalOrder) return null;
const market = markets?.find((m) => m.id === originalOrder.market.id);
if (!market) return null;
const original = (
);
return (
{order
? t(
`Cancel order - ${OrderStatusMapping[order.status].toLowerCase()}`
)
: t('Cancel order')}
{market?.tradableInstrument.instrument.code}
{original}
{order && order.rejectionReason && (
{getRejectionReason(order)}
)}
);
};
export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
const { data: assets } = useAssetsDataProvider();
const { data: markets } = useMarketList();
if (isWithdrawTransaction(tx.body)) {
const transactionDetails = tx.body;
const asset = assets?.find(
(a) => a.id === transactionDetails.withdrawSubmission.asset
);
if (asset) {
const num = formatNumber(
toBigNum(transactionDetails.withdrawSubmission.amount, asset.decimals),
asset.decimals
);
return (
{t('Withdraw')} {num} {asset.symbol}
);
}
}
if (isOrderSubmissionTransaction(tx.body)) {
return (
);
}
if (isOrderCancellationTransaction(tx.body)) {
// CANCEL ALL (from Portfolio)
if (
tx.body.orderCancellation.marketId === undefined &&
tx.body.orderCancellation.orderId === undefined
) {
return {t('Cancel all orders')} ;
}
// CANCEL
if (
tx.body.orderCancellation.orderId &&
tx.body.orderCancellation.marketId
) {
return (
);
}
// CANCEL ALL (from Trading)
if (tx.body.orderCancellation.marketId) {
const marketName = markets?.find(
(m) =>
m.id === (tx.body as OrderCancellationBody).orderCancellation.marketId
)?.tradableInstrument.instrument.code;
return (
{marketName
? `${t('Cancel all orders for')} ${marketName}`
: t('Cancel all orders')}
);
}
}
if (isOrderAmendmentTransaction(tx.body)) {
return (
);
}
if (isClosePositionTransaction(tx)) {
const transaction = tx.body as BatchMarketInstructionSubmissionBody;
const marketId = first(
transaction.batchMarketInstructions.cancellations
)?.marketId;
const market = marketId && markets?.find((m) => m.id === marketId);
if (market) {
return (
{t('Close position for')} {market.tradableInstrument.instrument.code}
);
}
}
if (isBatchMarketInstructionsTransaction(tx.body)) {
return {t('Batch market instruction')} ;
}
if (isTransferTransaction(tx.body)) {
const { amount, to, asset } = tx.body.transfer;
const transferAsset = assets?.find((a) => a.id === asset);
// only render if we have an asset to avoid unformatted amounts showing
if (transferAsset) {
const value = addDecimalsFormatNumber(amount, transferAsset.decimals);
return (
{t('Transfer')}
{t('To')} {truncateByChars(to)}
{value} {transferAsset.symbol}
);
}
}
return null;
};
type VegaTxToastContentProps = { tx: VegaStoredTxState };
const VegaTxRequestedToastContent = ({ tx }: VegaTxToastContentProps) => (
{t('Action required')}
{t(
'Please go to your Vega wallet application and approve or reject the transaction.'
)}
);
const VegaTxPendingToastContentProps = ({ tx }: VegaTxToastContentProps) => {
const explorerLink = useLinks(DApp.Explorer);
return (
{t('Awaiting confirmation')}
{t('Please wait for your transaction to be confirmed')}
{tx.txHash && (
{t('View in block explorer')}
)}
);
};
const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
const { createEthWithdrawalApproval } = useEthWithdrawApprovalsStore(
(state) => ({
createEthWithdrawalApproval: state.create,
})
);
const explorerLink = useLinks(DApp.Explorer);
if (isWithdrawTransaction(tx.body)) {
const completeWithdrawalButton = tx.withdrawal && (
);
return (
{t('Funds unlocked')}
{t('Your funds have been unlocked for withdrawal')}
{tx.txHash && (
{t('View in block explorer')}
)}
{completeWithdrawalButton}
);
}
if (isOrderSubmissionTransaction(tx.body) && tx.order?.rejectionReason) {
return (
{t('Order rejected')}
{t('Your order was rejected.')}
{tx.txHash && (
{t('View in block explorer')}
)}
);
}
if (isTransferTransaction(tx.body)) {
return (
{t('Transfer complete')}
{t('Your transaction has been confirmed ')}
);
}
return (
{t('Confirmed')}
{t('Your transaction has been confirmed ')}
{tx.txHash && (
{t('View in block explorer')}
)}
);
};
const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
let label = t('Error occurred');
let errorMessage = `${tx.error?.message} ${
tx.error instanceof WalletError && tx.error?.data
? `: ${tx.error?.data}`
: ''
}`;
const reconnectVegaWallet = useReconnectVegaWallet();
const orderRejection = tx.order && getRejectionReason(tx.order);
const walletNoConnectionCodes = [
ClientErrors.NO_SERVICE.code,
ClientErrors.NO_CLIENT.code,
];
const walletError =
tx.error instanceof WalletError &&
walletNoConnectionCodes.includes(tx.error.code);
if (orderRejection) {
label = t('Order rejected');
errorMessage = orderRejection;
}
if (walletError) {
label = t('Wallet disconnected');
errorMessage = t('The connection to your Vega Wallet has been lost.');
}
return (
{label}
{errorMessage}
{walletError && (
)}
);
};
export const useVegaTransactionToasts = () => {
const vegaTransactions = useVegaTransactionStore((state) =>
state.transactions.filter((transaction) => transaction?.dialogOpen)
);
const dismissVegaTransaction = useVegaTransactionStore(
(state) => state.dismiss
);
const fromVegaTransaction = useCallback(
(tx: VegaStoredTxState): Toast => {
let content: ToastContent;
if (tx.status === VegaTxStatus.Requested) {
content = ;
}
if (tx.status === VegaTxStatus.Pending) {
content = ;
}
if (tx.status === VegaTxStatus.Complete) {
content = ;
}
if (tx.status === VegaTxStatus.Error) {
content = ;
}
return {
id: `vega-${tx.id}`,
intent: getIntent(tx),
onClose: () => dismissVegaTransaction(tx.id),
loader: tx.status === VegaTxStatus.Pending,
content,
};
},
[dismissVegaTransaction]
);
const toasts = useMemo(() => {
return [
...compact(vegaTransactions)
.filter((tx) => isTransactionTypeSupported(tx))
.map(fromVegaTransaction),
];
}, [fromVegaTransaction, vegaTransactions]);
return toasts;
};