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; }) => (
{children}
); 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; };