import { useCallback } 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 { ToastHeading } from '@vegaprotocol/ui-toolkit'; import { Panel } from '@vegaprotocol/ui-toolkit'; import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit'; import { useToasts } from '@vegaprotocol/ui-toolkit'; import { Button, ExternalLink, Intent } from '@vegaprotocol/ui-toolkit'; import { addDecimalsFormatNumber, formatNumber, toBigNum, truncateByChars, } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { useAssetsDataProvider } from '@vegaprotocol/assets'; import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3'; import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment'; import { getOrderToastIntent, getOrderToastTitle, getRejectionReason, useOrderByIdQuery, } from '@vegaprotocol/orders'; import { useMarketList } from '@vegaprotocol/market-list'; import type { Side } from '@vegaprotocol/types'; import { OrderStatusMapping } from '@vegaprotocol/types'; import { Size } from '@vegaprotocol/react-helpers'; const intentMap: { [s in VegaTxStatus]: Intent } = { Default: Intent.Primary, Requested: Intent.Warning, Pending: Intent.Warning, Error: Intent.Danger, Complete: Intent.Success, }; 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 ); }; 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}

); }; const EditOrderDetails = ({ data, order, }: { data: OrderAmendment; order?: OrderTxUpdateFieldsFragment; }) => { const { data: orderById } = useOrderByIdQuery({ variables: { orderId: data.orderId }, }); const { data: markets } = useMarketList(); const originalOrder = order || orderById?.orderByID; const marketId = order?.marketId || orderById?.orderByID.market.id; if (!originalOrder) return null; const market = markets?.find((m) => m.id === marketId); 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}

); }; 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}

); }; 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 (tx.order && tx.order.rejectionReason) { const rejectionReason = getRejectionReason(tx.order) || tx.order.rejectionReason || ''; return ( <> {getOrderToastTitle(tx.order.status)} {rejectionReason ? (

{t('Your order has been rejected because: %s', [rejectionReason])}

) : (

{t('Your order has been rejected.')}

)} {tx.txHash && (

{t('View in block explorer')}

)} ); } if (isOrderSubmissionTransaction(tx.body) && tx.order?.rejectionReason) { return (

{getOrderToastTitle(tx.order.status)}

{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 ')}

{tx.txHash && (

{t('View in block explorer')}

)}
); } return ( <> {tx.order?.status ? getOrderToastTitle(tx.order.status) : 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 = getOrderToastTitle(tx.order?.status) || t('Order rejected'); errorMessage = t('Your order has been rejected because: %s', [ orderRejection || tx.order?.rejectionReason || ' ', ]); } if (walletError) { label = t('Wallet disconnected'); errorMessage = t('The connection to your Vega Wallet has been lost.'); } return ( <> {label}

{errorMessage}

{walletError && ( )} ); }; const isFinal = (tx: VegaStoredTxState) => [VegaTxStatus.Error, VegaTxStatus.Complete].includes(tx.status); export const useVegaTransactionToasts = () => { const [setToast, removeToast] = useToasts((store) => [ store.setToast, store.remove, ]); const [dismissTx, deleteTx] = useVegaTransactionStore((state) => [ state.dismiss, state.delete, ]); const onClose = useCallback( (tx: VegaStoredTxState) => () => { const safeToDelete = isFinal(tx); if (safeToDelete) { deleteTx(tx.id); } else { dismissTx(tx.id); } removeToast(`vega-${tx.id}`); }, [deleteTx, dismissTx, removeToast] ); const fromVegaTransaction = (tx: VegaStoredTxState): Toast => { const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined; const { intent, content } = getVegaTransactionContentIntent(tx); return { id: `vega-${tx.id}`, intent, onClose: onClose(tx), loader: tx.status === VegaTxStatus.Pending, content, closeAfter, }; }; useVegaTransactionStore.subscribe( (state) => compact( state.transactions.filter( (tx) => tx?.dialogOpen && isTransactionTypeSupported(tx) ) ), (txs) => { txs.forEach((tx) => setToast(fromVegaTransaction(tx))); } ); }; export const getVegaTransactionContentIntent = (tx: VegaStoredTxState) => { 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 = ; } // Transaction can be successful but the order can be rejected by the network const intent = (tx.order && getOrderToastIntent(tx.order.status)) || intentMap[tx.status]; return { intent, content }; };