From 384b06f36c3e84a6b01e56a0ea0e958abb4dedfa Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Wed, 8 Jan 2025 23:02:53 +0300 Subject: [PATCH 1/8] Remove Review Tx view Signed-off-by: Emre Bogazliyanlioglu --- .../src/hooks/useSendTransaction.ts | 288 ++++++++++++++++++ .../views/v2/Bridge/Routes/SingleRoute.tsx | 23 ++ .../src/views/v2/Bridge/index.tsx | 143 +++++---- 3 files changed, 403 insertions(+), 51 deletions(-) create mode 100644 wormhole-connect/src/hooks/useSendTransaction.ts diff --git a/wormhole-connect/src/hooks/useSendTransaction.ts b/wormhole-connect/src/hooks/useSendTransaction.ts new file mode 100644 index 000000000..d0c79d106 --- /dev/null +++ b/wormhole-connect/src/hooks/useSendTransaction.ts @@ -0,0 +1,288 @@ +import { useContext, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Context } from 'sdklegacy'; + +import config from 'config'; +import { RouteContext } from 'contexts/RouteContext'; +import { useUSDamountGetter } from 'hooks/useUSDamountGetter'; +import { + setTxDetails, + setSendTx, + setRoute as setRedeemRoute, + setTimestamp, +} from 'store/redeem'; +import { setRoute as setAppRoute } from 'store/router'; +import { setAmount, setIsTransactionInProgress } from 'store/transferInput'; +import { getTransferDetails } from 'telemetry'; +import { ERR_USER_REJECTED } from 'telemetry/types'; +import { getTokenDecimals, getWrappedToken } from 'utils'; +import { toDecimals } from 'utils/balance'; +import { interpretTransferError } from 'utils/errors'; +import { addTxToLocalStorage } from 'utils/inProgressTxCache'; +import { validate, isTransferValid } from 'utils/transferValidation'; +import { + registerWalletSigner, + switchChain, + TransferWallet, +} from 'utils/wallet'; + +import type { RootState } from 'store'; +import type { RelayerFee } from 'store/relay'; +import type { QuoteResult } from 'routes/operator'; + +type Props = { + quotes: Record; +}; + +type ReturnProps = { + error: string | undefined; + errorInternal: unknown | undefined; + send: () => void; +}; + +const useSendTransaction = (props: Props): ReturnProps => { + const dispatch = useDispatch(); + + const [error, setError] = useState(undefined); + const [errorInternal, setErrorInternal] = useState( + undefined, + ); + + const routeContext = useContext(RouteContext); + + const transferInput = useSelector((state: RootState) => state.transferInput); + + const { + amount, + fromChain: sourceChain, + toChain: destChain, + token: sourceToken, + destToken, + route, + validations, + } = transferInput; + + const wallet = useSelector((state: RootState) => state.wallet); + const { sending: sendingWallet, receiving: receivingWallet } = wallet; + + const relay = useSelector((state: RootState) => state.relay); + const { toNativeToken } = relay; + + const quoteResult = props.quotes[route ?? '']; + const quote = quoteResult?.success ? quoteResult : undefined; + const receiveNativeAmount = quote?.destinationNativeGas; + + const getUSDAmount = useUSDamountGetter(); + + const send = async () => { + setError(undefined); + + if (config.ui.previewMode) { + setError('Connect is in preview mode'); + return; + } + + // Pre-check of required values + if ( + !sourceChain || + !sourceToken || + !destChain || + !destToken || + !amount || + !route || + !quote + ) { + return; + } + + await validate({ transferInput, relay, wallet }, dispatch, () => false); + + const valid = isTransferValid(validations); + + if (!valid || !route) { + return; + } + + const transferDetails = getTransferDetails( + route, + sourceToken, + destToken, + sourceChain, + destChain, + amount, + getUSDAmount, + ); + + // Handle custom transfer validation (if provided by integrator) + if (config.validateTransfer) { + try { + const { isValid, error } = await config.validateTransfer({ + ...transferDetails, + fromWalletAddress: sendingWallet.address, + toWalletAddress: receivingWallet.address, + }); + if (!isValid) { + setError(error ?? 'Transfer validation failed'); + return; + } + } catch (e: unknown) { + setError('Error validating transfer'); + setErrorInternal(e); + console.error(e); + return; + } + } + + dispatch(setIsTransactionInProgress(true)); + + const sourceTokenConfig = config.tokens[sourceToken]; + + try { + const fromConfig = config.chains[sourceChain]; + + if (fromConfig && fromConfig?.context === Context.ETH) { + const chainId = fromConfig.chainId; + + if (typeof chainId !== 'number') { + throw new Error('Invalid EVM chain ID'); + } + + await switchChain(chainId, TransferWallet.SENDING); + await registerWalletSigner(sourceChain, TransferWallet.SENDING); + } + + config.triggerEvent({ + type: 'transfer.initiate', + details: transferDetails, + }); + + const [sdkRoute, receipt] = await config.routes + .get(route) + .send( + sourceTokenConfig, + amount, + sourceChain, + sendingWallet.address, + destChain, + receivingWallet.address, + destToken, + { nativeGas: toNativeToken }, + ); + + const txId = + 'originTxs' in receipt + ? receipt.originTxs[receipt.originTxs.length - 1].txid + : undefined; + + config.triggerEvent({ + type: 'transfer.start', + details: { ...transferDetails, txId }, + }); + + if (!txId) throw new Error("Can't find txid in receipt"); + + let relayerFee: RelayerFee | undefined = undefined; + if (quote.relayFee) { + const { token, amount } = quote.relayFee; + const feeToken = config.sdkConverter.findTokenConfigV1( + token, + Object.values(config.tokens), + ); + + const formattedFee = Number.parseFloat( + toDecimals(amount.amount, amount.decimals, 6), + ); + + relayerFee = { + fee: formattedFee, + tokenKey: feeToken?.key || '', + }; + } + + const txTimestamp = Date.now(); + + const txDetails = { + sendTx: txId, + sender: sendingWallet.address, + amount, + recipient: receivingWallet.address, + toChain: receipt.to, + fromChain: receipt.from, + tokenAddress: getWrappedToken(sourceTokenConfig).tokenId?.address ?? '', + tokenKey: sourceTokenConfig.key, + tokenDecimals: getTokenDecimals( + sourceChain, + getWrappedToken(sourceTokenConfig), + ), + receivedTokenKey: config.tokens[destToken].key, // TODO: possibly wrong (e..g if portico swap fails) + relayerFee, + receiveAmount: quote.destinationToken.amount, + receiveNativeAmount, + eta: quote.eta || 0, + }; + + // Add the new transaction to local storage + addTxToLocalStorage({ + txDetails, + txHash: txId, + timestamp: txTimestamp, + receipt, + route, + }); + + // Set the start time of the transaction + dispatch(setTimestamp(txTimestamp)); + + // TODO: SDKV2 set the tx details using on-chain data + // because they might be different than what we have in memory (relayer fee) + // or we may not have all the data (e.g. block) + // TODO: we don't need all of these details + // The SDK should provide a way to get the details from the chain (e.g. route.lookupSourceTxDetails) + dispatch(setTxDetails(txDetails)); + + // Reset the amount for a successful transaction + dispatch(setAmount('')); + + routeContext.setRoute(sdkRoute); + routeContext.setReceipt(receipt); + + dispatch(setSendTx(txId)); + dispatch(setRedeemRoute(route)); + dispatch(setAppRoute('redeem')); + setError(undefined); + } catch (e: unknown) { + const [uiError, transferError] = interpretTransferError( + e, + transferDetails, + ); + + if (transferError.type === ERR_USER_REJECTED) { + // User intentionally rejected in their wallet. This is not an error in the sense + // that something went wrong. + } else { + console.error('Wormhole Connect: error completing transfer', e); + + // Show error in UI + setError(uiError); + setErrorInternal(e); + + // Trigger transfer error event to integrator + config.triggerEvent({ + type: 'transfer.error', + error: transferError, + details: transferDetails, + }); + } + } finally { + dispatch(setIsTransactionInProgress(false)); + } + }; + + return { + send, + error, + errorInternal, + }; +}; + +export default useSendTransaction; diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index 6dd5436ab..d71e2a20e 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -5,6 +5,7 @@ import Card from '@mui/material/Card'; import CardActionArea from '@mui/material/CardActionArea'; import CardContent from '@mui/material/CardContent'; import CardHeader from '@mui/material/CardHeader'; +import Collapse from '@mui/material/Collapse'; import Divider from '@mui/material/Divider'; import Typography from '@mui/material/Typography'; import Stack from '@mui/material/Stack'; @@ -12,6 +13,7 @@ import { makeStyles } from 'tss-react/mui'; import { amount, routes } from '@wormhole-foundation/sdk'; import config from 'config'; +import { useGasSlider } from 'hooks/useGasSlider'; import ErrorIcon from 'icons/Error'; import WarningIcon from 'icons/Warning'; import TokenIcon from 'icons/TokenIcons'; @@ -29,6 +31,7 @@ import type { RootState } from 'store'; import { TokenConfig } from 'config/types'; import FastestRoute from 'icons/FastestRoute'; import CheapestRoute from 'icons/CheapestRoute'; +import GasSlider from 'views/v2/Bridge/ReviewTransaction/GasSlider'; const HIGH_FEE_THRESHOLD = 20; // dollhairs @@ -105,6 +108,7 @@ const SingleRoute = (props: Props) => { destToken, fromChain: sourceChain, token: sourceToken, + isTransactionInProgress, } = useSelector((state: RootState) => state.transferInput); const { usdPrices: tokenPrices } = useSelector( @@ -113,12 +117,21 @@ const SingleRoute = (props: Props) => { const { name } = props.route; const { quote } = props; + const receiveNativeAmount = quote?.destinationNativeGas; const destTokenConfig = useMemo( () => config.tokens[destToken] as TokenConfig | undefined, [destToken], ); + const { disabled: isGasSliderDisabled, showGasSlider } = useGasSlider({ + destChain, + destToken, + route: props.route.name, + valid: true, + isTransactionInProgress, + }); + const [feePrice, isHighFee, feeTokenConfig]: [ number | undefined, boolean, @@ -632,6 +645,16 @@ const SingleRoute = (props: Props) => { {errorMessage} {warningMessages} + {showGasSlider && ( + + + + )} diff --git a/wormhole-connect/src/views/v2/Bridge/index.tsx b/wormhole-connect/src/views/v2/Bridge/index.tsx index 067f47341..2a5ee7040 100644 --- a/wormhole-connect/src/views/v2/Bridge/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/index.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { makeStyles } from 'tss-react/mui'; import { useMediaQuery, useTheme } from '@mui/material'; +import CircularProgress from '@mui/material/CircularProgress'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; @@ -25,8 +26,8 @@ import { selectFromChain, selectToChain, setToken, - setTransferRoute, setDestToken, + setTransferRoute, } from 'store/transferInput'; import { isTransferValid, useValidate } from 'utils/transferValidation'; import { TransferWallet, useConnectToLastUsedWallet } from 'utils/wallet'; @@ -35,9 +36,9 @@ import AssetPicker from 'views/v2/Bridge/AssetPicker'; import WalletController from 'views/v2/Bridge/WalletConnector/Controller'; import AmountInput from 'views/v2/Bridge/AmountInput'; import Routes from 'views/v2/Bridge/Routes'; -import ReviewTransaction from 'views/v2/Bridge/ReviewTransaction'; import SwapInputs from 'views/v2/Bridge/SwapInputs'; import TxHistoryWidget from 'views/v2/TxHistory/Widget'; +import SendError from 'views/v2/Bridge/ReviewTransaction/SendError'; import { useSortedRoutesWithQuotes } from 'hooks/useSortedRoutesWithQuotes'; import { useFetchTokenPrices } from 'hooks/useFetchTokenPrices'; @@ -45,6 +46,7 @@ import type { Chain } from '@wormhole-foundation/sdk'; import { amount as sdkAmount } from '@wormhole-foundation/sdk'; import { useAmountValidation } from 'hooks/useAmountValidation'; import useGetTokenBalances from 'hooks/useGetTokenBalances'; +import useSendTransaction from 'hooks/useSendTransaction'; const useStyles = makeStyles()((theme) => ({ assetPickerContainer: { @@ -77,7 +79,7 @@ const useStyles = makeStyles()((theme) => ({ alignItems: 'center', width: '100%', }, - reviewTransaction: { + confirmTransaction: { padding: '8px 16px', borderRadius: '8px', height: '48px', @@ -111,9 +113,6 @@ const Bridge = () => { (state: RootState) => state.wallet, ); - const [selectedRoute, setSelectedRoute] = useState(); - const [willReviewTransaction, setWillReviewTransaction] = useState(false); - const { fromChain: sourceChain, toChain: destChain, @@ -125,6 +124,7 @@ const Bridge = () => { supportedSourceTokens, amount, validations, + isTransactionInProgress, } = useSelector((state: RootState) => state.transferInput); const { @@ -142,7 +142,7 @@ const Bridge = () => { destChain, sourceToken, destToken, - route: selectedRoute, + route, }); // Compute and set destination tokens @@ -151,14 +151,20 @@ const Bridge = () => { sourceChain, destChain, sourceToken, - route: selectedRoute, + route, }); + const { + send, + error: sendError, + errorInternal: sendErrorInternal, + } = useSendTransaction({ quotes: quotesMap }); + // Set selectedRoute if the route is auto-selected // After the auto-selection, we set selectedRoute when user clicks on a route in the list useEffect(() => { if (sortedRoutesWithQuotes.length === 0) { - setSelectedRoute(''); + setTransferRoute(''); } else { const preferredRoute = sortedRoutesWithQuotes.find( (route) => route.route === preferredRouteName, @@ -166,15 +172,15 @@ const Bridge = () => { const autoselectedRoute = route ?? preferredRoute?.route ?? sortedRoutesWithQuotes[0].route; const isSelectedRouteValid = - sortedRoutesWithQuotes.findIndex((r) => r.route === selectedRoute) > -1; + sortedRoutesWithQuotes.findIndex((r) => r.route === route) > -1; if (!isSelectedRouteValid) { - setSelectedRoute(''); + setTransferRoute(''); } // If no route is autoselected or we already have a valid selected route, // we should avoid overwriting it - if (!autoselectedRoute || (selectedRoute && isSelectedRouteValid)) { + if (!autoselectedRoute || (route && isSelectedRouteValid)) { return; } @@ -182,9 +188,9 @@ const Bridge = () => { (rs) => rs.route === autoselectedRoute, ); - if (routeData) setSelectedRoute(routeData.route); + if (routeData) setTransferRoute(routeData.route); } - }, [route, sortedRoutesWithQuotes]); + }, [preferredRouteName, route, sortedRoutesWithQuotes]); // Pre-fetch available routes useFetchSupportedRoutes(); @@ -314,12 +320,15 @@ const Bridge = () => { ); }, [ + classes.assetPickerContainer, + classes.assetPickerTitle, sourceChain, supportedSourceChains, sourceToken, supportedSourceTokens, - sendingWallet, isFetchingSupportedSourceTokens, + sendingWallet, + dispatch, ]); // Asset picker for the destination network and token @@ -349,12 +358,16 @@ const Bridge = () => { ); }, [ + classes.assetPickerContainer, + classes.assetPickerTitle, destChain, supportedDestChains, destToken, + sourceToken, supportedDestTokens, - receivingWallet, isFetchingSupportedDestTokens, + receivingWallet, + dispatch, ]); // Header for Bridge view, which includes the title and settings icon. @@ -382,7 +395,7 @@ const Bridge = () => { ); - }, [sendingWallet?.address, config.ui]); + }, [sendingWallet?.address, classes.bridgeHeader, dispatch]); const walletConnector = useMemo(() => { if (sendingWallet?.address && receivingWallet?.address) { @@ -414,36 +427,71 @@ const Bridge = () => { const showRoutes = hasConnectedWallets && hasEnteredAmount && !hasError; - const reviewTransactionDisabled = + const confirmTransactionDisabled = !sourceChain || !sourceToken || !destChain || !destToken || !hasConnectedWallets || - !selectedRoute || + !route || !isValid || isFetchingQuotes || !hasEnteredAmount || + isTransactionInProgress || hasError; // Review transaction button is shown only when everything is ready - const reviewTransactionButton = ( - - ); + const confirmTransactionButton = useMemo(() => { + return ( + + ); + }, [ + confirmTransactionDisabled, + classes.confirmTransaction, + isTransactionInProgress, + theme.palette.primary.contrastText, + mobile, + isFetchingQuotes, + send, + ]); - const reviewButtonTooltip = + const confirmButtonTooltip = !sourceChain || !sourceToken ? 'Please select a source asset' : !destChain || !destToken @@ -452,20 +500,10 @@ const Bridge = () => { ? 'Please enter an amount' : isFetchingQuotes ? 'Loading quotes...' - : !selectedRoute + : !route ? 'Please select a quote' : ''; - if (willReviewTransaction) { - return ( - setWillReviewTransaction(false)} - /> - ); - } - return (
{header} @@ -481,17 +519,20 @@ const Bridge = () => { {showRoutes && ( { + dispatch(setTransferRoute(r)); + }} quotes={quotesMap} isLoading={isFetchingQuotes || isFetchingBalances} hasError={hasError} /> )} + {hasConnectedWallets ? ( - - {reviewTransactionButton} + + {confirmTransactionButton} ) : ( walletConnector From 60aa8e2f349f61dd8db6e332282079c8796431d4 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Wed, 8 Jan 2025 23:03:56 +0300 Subject: [PATCH 2/8] Remove unused file Signed-off-by: Emre Bogazliyanlioglu --- .../v2/Bridge/ReviewTransaction/index.tsx | 424 ------------------ 1 file changed, 424 deletions(-) delete mode 100644 wormhole-connect/src/views/v2/Bridge/ReviewTransaction/index.tsx diff --git a/wormhole-connect/src/views/v2/Bridge/ReviewTransaction/index.tsx b/wormhole-connect/src/views/v2/Bridge/ReviewTransaction/index.tsx deleted file mode 100644 index 3e55ae301..000000000 --- a/wormhole-connect/src/views/v2/Bridge/ReviewTransaction/index.tsx +++ /dev/null @@ -1,424 +0,0 @@ -import React, { useContext, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { makeStyles } from 'tss-react/mui'; -import { useMediaQuery, useTheme } from '@mui/material'; -import CircularProgress from '@mui/material/CircularProgress'; -import Collapse from '@mui/material/Collapse'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import ChevronLeft from '@mui/icons-material/ChevronLeft'; -import IconButton from '@mui/material/IconButton'; -import { getTransferDetails } from 'telemetry'; -import { Context } from 'sdklegacy'; - -import Button from 'components/v2/Button'; -import config from 'config'; -import { addTxToLocalStorage } from 'utils/inProgressTxCache'; -import { RoutesConfig } from 'config/routes'; -import { RouteContext } from 'contexts/RouteContext'; -import { useGasSlider } from 'hooks/useGasSlider'; -import { - setTxDetails, - setSendTx, - setRoute as setRedeemRoute, - setTimestamp, -} from 'store/redeem'; -import { setRoute as setAppRoute } from 'store/router'; -import { setAmount, setIsTransactionInProgress } from 'store/transferInput'; -import { getTokenDecimals, getWrappedToken } from 'utils'; -import { interpretTransferError } from 'utils/errors'; -import { validate, isTransferValid } from 'utils/transferValidation'; -import { - registerWalletSigner, - switchChain, - TransferWallet, -} from 'utils/wallet'; -import GasSlider from 'views/v2/Bridge/ReviewTransaction/GasSlider'; -import SingleRoute from 'views/v2/Bridge/Routes/SingleRoute'; - -import type { RootState } from 'store'; -import { RelayerFee } from 'store/relay'; - -import { amount as sdkAmount } from '@wormhole-foundation/sdk'; -import { toDecimals } from 'utils/balance'; -import { useUSDamountGetter } from 'hooks/useUSDamountGetter'; -import SendError from './SendError'; -import { ERR_USER_REJECTED } from 'telemetry/types'; - -const useStyles = makeStyles()((theme) => ({ - container: { - gap: '16px', - width: '100%', - maxWidth: '420px', - }, - confirmTransaction: { - padding: '8px 16px', - borderRadius: '8px', - margin: 'auto', - maxWidth: '420px', - width: '100%', - }, -})); - -type Props = { - onClose: () => void; - quotes: any; - isFetchingQuotes: boolean; -}; - -const ReviewTransaction = (props: Props) => { - const { classes } = useStyles(); - const dispatch = useDispatch(); - const theme = useTheme(); - - const mobile = useMediaQuery(theme.breakpoints.down('sm')); - - const [sendError, setSendError] = useState(undefined); - const [sendErrorInternal, setSendErrorInternal] = useState( - undefined, - ); - - const routeContext = useContext(RouteContext); - - const transferInput = useSelector((state: RootState) => state.transferInput); - - const { - amount, - fromChain: sourceChain, - toChain: destChain, - token: sourceToken, - destToken, - isTransactionInProgress, - route, - validations, - } = transferInput; - - const wallet = useSelector((state: RootState) => state.wallet); - const { sending: sendingWallet, receiving: receivingWallet } = wallet; - - const relay = useSelector((state: RootState) => state.relay); - const { toNativeToken } = relay; - - const getUSDAmount = useUSDamountGetter(); - - const { disabled: isGasSliderDisabled, showGasSlider } = useGasSlider({ - destChain, - destToken, - route, - valid: true, - isTransactionInProgress, - }); - - const quoteResult = props.quotes[route ?? '']; - const quote = quoteResult?.success ? quoteResult : undefined; - - const receiveNativeAmount = quote?.destinationNativeGas; - - const send = async () => { - setSendError(undefined); - - if (config.ui.previewMode) { - setSendError('Connect is in preview mode'); - return; - } - - // Pre-check of required values - if ( - !sourceChain || - !sourceToken || - !destChain || - !destToken || - !amount || - !route || - !quote - ) { - return; - } - - await validate({ transferInput, relay, wallet }, dispatch, () => false); - - const valid = isTransferValid(validations); - - if (!valid || !route) { - return; - } - - const transferDetails = getTransferDetails( - route, - sourceToken, - destToken, - sourceChain, - destChain, - amount, - getUSDAmount, - ); - - // Handle custom transfer validation (if provided by integrator) - if (config.validateTransfer) { - try { - const { isValid, error } = await config.validateTransfer({ - ...transferDetails, - fromWalletAddress: sendingWallet.address, - toWalletAddress: receivingWallet.address, - }); - if (!isValid) { - setSendError(error ?? 'Transfer validation failed'); - return; - } - } catch (e) { - setSendError('Error validating transfer'); - setSendErrorInternal(e); - console.error(e); - return; - } - } - - dispatch(setIsTransactionInProgress(true)); - - const sourceTokenConfig = config.tokens[sourceToken]; - - try { - const fromConfig = config.chains[sourceChain!]; - - if (fromConfig?.context === Context.ETH) { - const chainId = fromConfig.chainId; - - if (typeof chainId !== 'number') { - throw new Error('Invalid EVM chain ID'); - } - - await switchChain(chainId, TransferWallet.SENDING); - await registerWalletSigner(sourceChain, TransferWallet.SENDING); - } - - config.triggerEvent({ - type: 'transfer.initiate', - details: transferDetails, - }); - - const [sdkRoute, receipt] = await config.routes - .get(route) - .send( - sourceTokenConfig, - amount, - sourceChain, - sendingWallet.address, - destChain, - receivingWallet.address, - destToken, - { nativeGas: toNativeToken }, - ); - - const txId = - 'originTxs' in receipt - ? receipt.originTxs[receipt.originTxs.length - 1].txid - : undefined; - - config.triggerEvent({ - type: 'transfer.start', - details: { ...transferDetails, txId }, - }); - - if (!txId) throw new Error("Can't find txid in receipt"); - - let relayerFee: RelayerFee | undefined = undefined; - if (quote.relayFee) { - const { token, amount } = quote.relayFee; - const feeToken = config.sdkConverter.findTokenConfigV1( - token, - Object.values(config.tokens), - ); - - const formattedFee = Number.parseFloat( - toDecimals(amount.amount, amount.decimals, 6), - ); - - relayerFee = { - fee: formattedFee, - tokenKey: feeToken?.key || '', - }; - } - - const txTimestamp = Date.now(); - const txDetails = { - sendTx: txId, - sender: sendingWallet.address, - amount, - recipient: receivingWallet.address, - toChain: receipt.to, - fromChain: receipt.from, - tokenAddress: getWrappedToken(sourceTokenConfig).tokenId!.address, - tokenKey: sourceTokenConfig.key, - tokenDecimals: getTokenDecimals( - sourceChain, - getWrappedToken(sourceTokenConfig), - ), - receivedTokenKey: config.tokens[destToken].key, // TODO: possibly wrong (e..g if portico swap fails) - relayerFee, - receiveAmount: quote.destinationToken.amount, - receiveNativeAmount, - eta: quote.eta || 0, - }; - - // Add the new transaction to local storage - addTxToLocalStorage({ - txDetails, - txHash: txId, - timestamp: txTimestamp, - receipt, - route, - }); - - // Set the start time of the transaction - dispatch(setTimestamp(txTimestamp)); - - // TODO: SDKV2 set the tx details using on-chain data - // because they might be different than what we have in memory (relayer fee) - // or we may not have all the data (e.g. block) - // TODO: we don't need all of these details - // The SDK should provide a way to get the details from the chain (e.g. route.lookupSourceTxDetails) - dispatch(setTxDetails(txDetails)); - - // Reset the amount for a successful transaction - dispatch(setAmount('')); - - routeContext.setRoute(sdkRoute); - routeContext.setReceipt(receipt); - - dispatch(setSendTx(txId)); - dispatch(setRedeemRoute(route)); - dispatch(setAppRoute('redeem')); - setSendError(undefined); - } catch (e: any) { - const [uiError, transferError] = interpretTransferError( - e, - transferDetails, - ); - - if (transferError.type === ERR_USER_REJECTED) { - // User intentionally rejected in their wallet. This is not an error in the sense - // that something went wrong. - } else { - console.error('Wormhole Connect: error completing transfer', e); - - // Show error in UI - setSendError(uiError); - setSendErrorInternal(e); - - // Trigger transfer error event to integrator - config.triggerEvent({ - type: 'transfer.error', - error: transferError, - details: transferDetails, - }); - } - } finally { - dispatch(setIsTransactionInProgress(false)); - } - }; - - const walletsConnected = useMemo( - () => !!sendingWallet.address && !!receivingWallet.address, - [sendingWallet.address, receivingWallet.address], - ); - - // Review transaction button is shown only when everything is ready - const confirmTransactionButton = useMemo(() => { - if ( - !sourceChain || - !sourceToken || - !destChain || - !destToken || - !route || - !amount - ) { - return null; - } - - return ( - - ); - }, [ - props.isFetchingQuotes, - isTransactionInProgress, - sourceChain, - sourceToken, - destChain, - destToken, - route, - amount, - send, - ]); - - if (!route || !walletsConnected) { - return <>; - } - - return ( - -
- props.onClose?.()} - > - - -
- - {showGasSlider && ( - - - - )} - - {confirmTransactionButton} -
- ); -}; - -export default ReviewTransaction; From b8320224b491f7d6e8027f4cdb4d254b135931ff Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Thu, 9 Jan 2025 09:28:53 +0300 Subject: [PATCH 3/8] Remove unused param from useGasSlider Signed-off-by: Emre Bogazliyanlioglu --- wormhole-connect/src/hooks/useGasSlider.ts | 5 ++--- wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/wormhole-connect/src/hooks/useGasSlider.ts b/wormhole-connect/src/hooks/useGasSlider.ts index dbdde5a19..d642acd16 100644 --- a/wormhole-connect/src/hooks/useGasSlider.ts +++ b/wormhole-connect/src/hooks/useGasSlider.ts @@ -7,7 +7,6 @@ type Props = { destChain: Chain | undefined; destToken: string; route?: string; - valid: boolean; isTransactionInProgress: boolean; }; @@ -17,9 +16,9 @@ export const useGasSlider = ( disabled: boolean; showGasSlider: boolean | undefined; } => { - const { destChain, destToken, route, isTransactionInProgress, valid } = props; + const { destChain, destToken, route, isTransactionInProgress } = props; - const disabled = !valid || isTransactionInProgress; + const disabled = isTransactionInProgress; const toChainConfig = destChain ? config.chains[destChain] : undefined; const gasTokenConfig = toChainConfig ? config.tokens[toChainConfig.gasToken] diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index d71e2a20e..20b2c31df 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -128,7 +128,6 @@ const SingleRoute = (props: Props) => { destChain, destToken, route: props.route.name, - valid: true, isTransactionInProgress, }); From 1959ff62e0821378b917221de63349324a821585 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Thu, 9 Jan 2025 09:44:19 +0300 Subject: [PATCH 4/8] Refactor Signed-off-by: Emre Bogazliyanlioglu --- ...ransaction.ts => useConfirmTransaction.ts} | 24 ++-- .../views/v2/Bridge/AssetPicker/ChainList.tsx | 3 +- .../GasSlider.tsx => GasSlider/index.tsx} | 6 +- .../v2/Bridge/ReviewTransaction/SendError.tsx | 72 ---------- .../views/v2/Bridge/Routes/SingleRoute.tsx | 2 +- .../src/views/v2/Bridge/index.tsx | 130 ++++++++++++------ 6 files changed, 111 insertions(+), 126 deletions(-) rename wormhole-connect/src/hooks/{useSendTransaction.ts => useConfirmTransaction.ts} (92%) rename wormhole-connect/src/views/v2/Bridge/{ReviewTransaction/GasSlider.tsx => GasSlider/index.tsx} (96%) delete mode 100644 wormhole-connect/src/views/v2/Bridge/ReviewTransaction/SendError.tsx diff --git a/wormhole-connect/src/hooks/useSendTransaction.ts b/wormhole-connect/src/hooks/useConfirmTransaction.ts similarity index 92% rename from wormhole-connect/src/hooks/useSendTransaction.ts rename to wormhole-connect/src/hooks/useConfirmTransaction.ts index d0c79d106..114af068a 100644 --- a/wormhole-connect/src/hooks/useSendTransaction.ts +++ b/wormhole-connect/src/hooks/useConfirmTransaction.ts @@ -36,15 +36,18 @@ type Props = { type ReturnProps = { error: string | undefined; - errorInternal: unknown | undefined; - send: () => void; + // errorInternal can be a result of custom validation, hence of any type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + errorInternal: any | undefined; + onConfirm: () => void; }; const useSendTransaction = (props: Props): ReturnProps => { const dispatch = useDispatch(); const [error, setError] = useState(undefined); - const [errorInternal, setErrorInternal] = useState( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [errorInternal, setErrorInternal] = useState( undefined, ); @@ -74,8 +77,11 @@ const useSendTransaction = (props: Props): ReturnProps => { const getUSDAmount = useUSDamountGetter(); - const send = async () => { - setError(undefined); + const onConfirm = async () => { + // Clear previous errors + if (error) { + setError(undefined); + } if (config.ui.previewMode) { setError('Connect is in preview mode'); @@ -95,11 +101,11 @@ const useSendTransaction = (props: Props): ReturnProps => { return; } + // Validate all inputs + // The results of this check will be written back to Redux store (see transferInput.validations). await validate({ transferInput, relay, wallet }, dispatch, () => false); - const valid = isTransferValid(validations); - - if (!valid || !route) { + if (!isTransferValid(validations)) { return; } @@ -279,7 +285,7 @@ const useSendTransaction = (props: Props): ReturnProps => { }; return { - send, + onConfirm, error, errorInternal, }; diff --git a/wormhole-connect/src/views/v2/Bridge/AssetPicker/ChainList.tsx b/wormhole-connect/src/views/v2/Bridge/AssetPicker/ChainList.tsx index 497113507..a8b4e523c 100644 --- a/wormhole-connect/src/views/v2/Bridge/AssetPicker/ChainList.tsx +++ b/wormhole-connect/src/views/v2/Bridge/AssetPicker/ChainList.tsx @@ -115,9 +115,8 @@ const ChainList = (props: Props) => { return ( {topChains.map((chain: ChainConfig) => ( - + onChainSelect(chain.key)} diff --git a/wormhole-connect/src/views/v2/Bridge/ReviewTransaction/GasSlider.tsx b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx similarity index 96% rename from wormhole-connect/src/views/v2/Bridge/ReviewTransaction/GasSlider.tsx rename to wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx index f09741ddb..6d305ecdc 100644 --- a/wormhole-connect/src/views/v2/Bridge/ReviewTransaction/GasSlider.tsx +++ b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx @@ -100,14 +100,14 @@ const GasSlider = (props: { const destChainConfig = config.chains[destChain!]; const nativeGasTokenConfig = config.tokens[destChainConfig!.gasToken]; - const [isGasSliderOpen, setIsGasSliderOpen] = useState(!props.disabled); + const [isGasSliderOpen, setIsGasSliderOpen] = useState(false); const [percentage, setPercentage] = useState(0); const [debouncedPercentage] = useDebounce(percentage, 500); useEffect(() => { dispatch(setToNativeToken(debouncedPercentage / 100)); - }, [debouncedPercentage]); + }, [debouncedPercentage, dispatch]); const nativeGasPrice = useMemo(() => { if (!destChain) { @@ -151,6 +151,7 @@ const GasSlider = (props: { {`Need more gas on ${destChain}?`} { const { checked } = e.target; @@ -172,6 +173,7 @@ const GasSlider = (props: { ({ - copyIcon: { - fontSize: '14px', - }, - doneIcon: { - fontSize: '14px', - color: theme.palette.success.main, - }, -})); - -export default ({ humanError, internalError }: Props) => { - const { classes } = useStyles(); - - const [justCopied, setJustCopied] = useState(false); - - if (humanError === undefined) { - return null; - } - - const getHelp = - internalError && internalError.message && config.ui.getHelpUrl ? ( - - Having trouble?{' '} - { - copyTextToClipboard(internalError.message); - setJustCopied(true); - setTimeout(() => setJustCopied(false), 3000); - }} - > - Copy the error logs{' '} - {justCopied ? ( - - ) : ( - - )} - - {' and '} - - ask for help - - . - - ) : null; - - return ( - - - {getHelp} - - ); -}; diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index 20b2c31df..0d2c05b65 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -31,7 +31,7 @@ import type { RootState } from 'store'; import { TokenConfig } from 'config/types'; import FastestRoute from 'icons/FastestRoute'; import CheapestRoute from 'icons/CheapestRoute'; -import GasSlider from 'views/v2/Bridge/ReviewTransaction/GasSlider'; +import GasSlider from 'views/v2/Bridge/GasSlider'; const HIGH_FEE_THRESHOLD = 20; // dollhairs diff --git a/wormhole-connect/src/views/v2/Bridge/index.tsx b/wormhole-connect/src/views/v2/Bridge/index.tsx index 2a5ee7040..48f9574c2 100644 --- a/wormhole-connect/src/views/v2/Bridge/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/index.tsx @@ -1,26 +1,34 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { makeStyles } from 'tss-react/mui'; import { useMediaQuery, useTheme } from '@mui/material'; +import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; - +import CopyIcon from '@mui/icons-material/ContentCopy'; +import DoneIcon from '@mui/icons-material/Done'; import HistoryIcon from '@mui/icons-material/History'; +import { amount as sdkAmount } from '@wormhole-foundation/sdk'; +import type { Chain } from '@wormhole-foundation/sdk'; -import type { RootState } from 'store'; - +import FooterNavBar from 'components/FooterNavBar'; +import Header, { Alignment } from 'components/Header'; +import PageHeader from 'components/PageHeader'; +import AlertBannerV2 from 'components/v2/AlertBanner'; import Button from 'components/v2/Button'; import config from 'config'; -import { joinClass } from 'utils/style'; -import PoweredByIcon from 'icons/PoweredBy'; -import PageHeader from 'components/PageHeader'; -import Header, { Alignment } from 'components/Header'; -import FooterNavBar from 'components/FooterNavBar'; import useFetchSupportedRoutes from 'hooks/useFetchSupportedRoutes'; import useComputeDestinationTokens from 'hooks/useComputeDestinationTokens'; import useComputeSourceTokens from 'hooks/useComputeSourceTokens'; +import { useSortedRoutesWithQuotes } from 'hooks/useSortedRoutesWithQuotes'; +import { useFetchTokenPrices } from 'hooks/useFetchTokenPrices'; +import { useAmountValidation } from 'hooks/useAmountValidation'; +import useConfirmTransaction from 'hooks/useConfirmTransaction'; +import useGetTokenBalances from 'hooks/useGetTokenBalances'; +import PoweredByIcon from 'icons/PoweredBy'; +import type { RootState } from 'store'; import { setRoute as setAppRoute } from 'store/router'; import { selectFromChain, @@ -29,6 +37,8 @@ import { setDestToken, setTransferRoute, } from 'store/transferInput'; +import { copyTextToClipboard } from 'utils'; +import { joinClass } from 'utils/style'; import { isTransferValid, useValidate } from 'utils/transferValidation'; import { TransferWallet, useConnectToLastUsedWallet } from 'utils/wallet'; import WalletConnector from 'views/v2/Bridge/WalletConnector'; @@ -38,15 +48,6 @@ import AmountInput from 'views/v2/Bridge/AmountInput'; import Routes from 'views/v2/Bridge/Routes'; import SwapInputs from 'views/v2/Bridge/SwapInputs'; import TxHistoryWidget from 'views/v2/TxHistory/Widget'; -import SendError from 'views/v2/Bridge/ReviewTransaction/SendError'; -import { useSortedRoutesWithQuotes } from 'hooks/useSortedRoutesWithQuotes'; -import { useFetchTokenPrices } from 'hooks/useFetchTokenPrices'; - -import type { Chain } from '@wormhole-foundation/sdk'; -import { amount as sdkAmount } from '@wormhole-foundation/sdk'; -import { useAmountValidation } from 'hooks/useAmountValidation'; -import useGetTokenBalances from 'hooks/useGetTokenBalances'; -import useSendTransaction from 'hooks/useSendTransaction'; const useStyles = makeStyles()((theme) => ({ assetPickerContainer: { @@ -69,15 +70,9 @@ const useStyles = makeStyles()((theme) => ({ display: 'flex', alignItems: 'center', }, - ctaContainer: { - marginTop: '8px', - width: '100%', - }, - header: { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - width: '100%', + doneIcon: { + fontSize: '14px', + color: theme.palette.success.main, }, confirmTransaction: { padding: '8px 16px', @@ -87,6 +82,13 @@ const useStyles = makeStyles()((theme) => ({ maxWidth: '420px', width: '100%', }, + copyIcon: { + fontSize: '14px', + }, + ctaContainer: { + marginTop: '8px', + width: '100%', + }, spacer: { display: 'flex', flexDirection: 'column', @@ -106,6 +108,8 @@ const Bridge = () => { const theme = useTheme(); const dispatch = useDispatch(); + const [errorCopied, setErrorCopied] = useState(false); + const mobile = useMediaQuery(theme.breakpoints.down('sm')); // Connected wallets, if any @@ -155,10 +159,10 @@ const Bridge = () => { }); const { - send, - error: sendError, - errorInternal: sendErrorInternal, - } = useSendTransaction({ quotes: quotesMap }); + error: txError, + errorInternal: txErrorInternal, + onConfirm, + } = useConfirmTransaction({ quotes: quotesMap }); // Set selectedRoute if the route is auto-selected // After the auto-selection, we set selectedRoute when user clicks on a route in the list @@ -238,7 +242,7 @@ const Bridge = () => { // All supported chains from the given configuration and any custom override const supportedChains = useMemo( () => config.routes.allSupportedChains(), - [config.chainsArr], + [config.chains], ); // Supported chains for the source network @@ -250,7 +254,7 @@ const Bridge = () => { supportedChains.includes(chain.key) ); }); - }, [config.chainsArr, destChain, supportedChains]); + }, [destChain, supportedChains]); // Supported chains for the destination network const supportedDestChains = useMemo(() => { @@ -260,7 +264,7 @@ const Bridge = () => { !chain.disabledAsDestination && supportedChains.includes(chain.key), ); - }, [config.chainsArr, sourceChain, supportedChains]); + }, [sourceChain, supportedChains]); // Supported tokens for destination chain const supportedDestTokens = useMemo(() => { @@ -291,7 +295,7 @@ const Bridge = () => { } return ; - }, [config.ui]); + }, []); // Asset picker for the source network and token const sourceAssetPicker = useMemo(() => { @@ -419,6 +423,54 @@ const Bridge = () => { ); }, [sourceChain, destChain, sendingWallet, receivingWallet]); + const transactionError = useMemo(() => { + if (!txError) { + return null; + } + + return ( + + + {txErrorInternal && txErrorInternal.message && config.ui.getHelpUrl ? ( + + Having trouble?{' '} + { + copyTextToClipboard(txErrorInternal.message); + setErrorCopied(true); + setTimeout(() => setErrorCopied(false), 3000); + }} + > + Copy the error logs{' '} + {errorCopied ? ( + + ) : ( + + )} + + {' and '} + + ask for help + + . + + ) : null} + + ); + }, [ + classes.copyIcon, + classes.doneIcon, + errorCopied, + txError, + txErrorInternal, + ]); + const hasError = !!amountValidation.error; const hasEnteredAmount = amount && sdkAmount.whole(amount) > 0; @@ -447,9 +499,7 @@ const Bridge = () => { disabled={confirmTransactionDisabled} variant="primary" className={classes.confirmTransaction} - onClick={() => { - send(); - }} + onClick={() => onConfirm()} > {isTransactionInProgress ? ( { theme.palette.primary.contrastText, mobile, isFetchingQuotes, - send, + onConfirm, ]); const confirmButtonTooltip = @@ -528,7 +578,7 @@ const Bridge = () => { hasError={hasError} /> )} - + {transactionError} {hasConnectedWallets ? ( From 5ef14cfab624167c399bee1dda1e49d3f98dc59a Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Thu, 9 Jan 2025 23:27:50 +0300 Subject: [PATCH 5/8] Disable/dim all actionable components when tx in progress Signed-off-by: Emre Bogazliyanlioglu --- .../src/views/v2/Bridge/AmountInput/index.tsx | 33 +++-- .../src/views/v2/Bridge/AssetPicker/index.tsx | 17 ++- .../views/v2/Bridge/Routes/SingleRoute.tsx | 135 ++++++++++-------- .../src/views/v2/Bridge/Routes/index.tsx | 2 +- .../src/views/v2/Bridge/SwapInputs/index.tsx | 1 + .../v2/Bridge/WalletConnector/Controller.tsx | 26 +++- .../src/views/v2/Bridge/index.tsx | 17 ++- 7 files changed, 145 insertions(+), 86 deletions(-) diff --git a/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx b/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx index aff8e6f26..715783ae0 100644 --- a/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx @@ -57,7 +57,7 @@ const DebouncedTextField = memo( setInnerValue(e.target.value); deferredOnChange(e.target.value); }, - [], + [deferredOnChange], ); // Propagate any outside changes to the inner TextField value @@ -133,9 +133,11 @@ const AmountInput = (props: Props) => { amount ? sdkAmount.display(amount) : '', ); - const { fromChain: sourceChain, token: sourceToken } = useSelector( - (state: RootState) => state.transferInput, - ); + const { + fromChain: sourceChain, + token: sourceToken, + isTransactionInProgress, + } = useSelector((state: RootState) => state.transferInput); const { balances, isFetching } = useGetTokenBalances( sendingWallet?.address || '', @@ -159,8 +161,8 @@ const AmountInput = (props: Props) => { ); const isInputDisabled = useMemo( - () => !sourceChain || !sourceToken, - [sourceChain, sourceToken], + () => isTransactionInProgress || !sourceChain || !sourceToken, + [isTransactionInProgress, sourceChain, sourceToken], ); const balance = useMemo(() => { @@ -193,12 +195,21 @@ const AmountInput = (props: Props) => { )} ); - }, [isInputDisabled, balances, tokenBalance, sendingWallet.address]); + }, [ + isInputDisabled, + sendingWallet.address, + classes.balance, + isFetching, + tokenBalance, + ]); - const handleChange = useCallback((newValue: string): void => { - dispatch(setAmount(newValue)); - setAmountInput(newValue); - }, []); + const handleChange = useCallback( + (newValue: string): void => { + dispatch(setAmount(newValue)); + setAmountInput(newValue); + }, + [dispatch], + ); const maxButton = useMemo(() => { const maxButtonDisabled = diff --git a/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx b/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx index 453a11e03..807388f16 100644 --- a/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx @@ -22,6 +22,7 @@ import ChainList from './ChainList'; import TokenList from './TokenList'; import { Chain } from '@wormhole-foundation/sdk'; import AssetBadge from 'components/AssetBadge'; +import { joinClass } from 'utils/style'; const useStyles = makeStyles()((theme: any) => ({ card: { @@ -46,8 +47,8 @@ const useStyles = makeStyles()((theme: any) => ({ justifyContent: 'space-between', }, disabled: { - opacity: '0.4', - cursor: 'not-allowed', + opacity: '0.6', + cursor: 'default', clickEvent: 'none', }, popover: { @@ -70,6 +71,7 @@ type Props = { setChain: (value: Chain) => void; wallet: WalletData; isSource: boolean; + isTransactionInProgress: boolean; }; const AssetPicker = (props: Props) => { @@ -138,12 +140,19 @@ const AssetPicker = (props: Props) => { ); }, [chainConfig, tokenConfig]); + const triggerProps = props.isTransactionInProgress + ? {} + : bindTrigger(popupState); + return ( <> ({ - container: { - width: '100%', - maxWidth: '420px', - marginBottom: '8px', - }, - card: { - borderRadius: '8px', - width: '100%', - maxWidth: '420px', - }, - cardHeader: { - padding: '20px 20px 0px', - }, - cardContent: { - marginTop: '18px', - padding: '0px 20px 20px', - }, - errorIcon: { - color: theme.palette.error.main, - height: '34px', - width: '34px', - marginRight: '24px', - }, - fastestBadge: { - width: '14px', - height: '14px', - position: 'relative', - top: '2px', - marginRight: '4px', - fill: theme.palette.primary.main, - }, - cheapestBadge: { - width: '12px', - height: '12px', - position: 'relative', - top: '1px', - marginRight: '3px', - fill: theme.palette.primary.main, - }, - messageContainer: { - padding: '12px 0px 0px', - }, - warningIcon: { - color: theme.palette.warning.main, - height: '34px', - width: '34px', - marginRight: '12px', - }, -})); +const useStyles = makeStyles<{ isSelected: boolean }>()( + (theme: any, { isSelected }) => ({ + container: { + width: '100%', + maxWidth: '420px', + marginBottom: '8px', + }, + card: { + border: '1px solid', + borderColor: isSelected ? theme.palette.primary.main : 'transparent', + borderRadius: '8px', + width: '100%', + maxWidth: '420px', + }, + cardHeader: { + padding: '20px 20px 0px', + }, + cardContent: { + marginTop: '18px', + padding: '0px 20px 20px', + }, + disabled: { + opacity: '0.6', + cursor: 'default', + clickEvent: 'none', + }, + errorIcon: { + color: theme.palette.error.main, + height: '34px', + width: '34px', + marginRight: '24px', + }, + fastestBadge: { + width: '14px', + height: '14px', + position: 'relative', + top: '2px', + marginRight: '4px', + fill: theme.palette.primary.main, + }, + cheapestBadge: { + width: '12px', + height: '12px', + position: 'relative', + top: '1px', + marginRight: '3px', + fill: theme.palette.primary.main, + }, + messageContainer: { + padding: '12px 0px 0px', + }, + warningIcon: { + color: theme.palette.warning.main, + height: '34px', + width: '34px', + marginRight: '12px', + }, + }), +); type Props = { route: RouteData; @@ -99,7 +109,6 @@ type Props = { }; const SingleRoute = (props: Props) => { - const { classes } = useStyles(); const theme = useTheme(); const routeConfig = config.routes.get(props.route.name); @@ -115,10 +124,12 @@ const SingleRoute = (props: Props) => { (state: RootState) => state.tokenPrices, ); + const { quote, isSelected } = props; const { name } = props.route; - const { quote } = props; const receiveNativeAmount = quote?.destinationNativeGas; + const { classes } = useStyles({ isSelected }); + const destTokenConfig = useMemo( () => config.tokens[destToken] as TokenConfig | undefined, [destToken], @@ -566,7 +577,7 @@ const SingleRoute = (props: Props) => { // 1- If no action handler provided, fall back to default // 2- Otherwise there is an action handler, "pointer" const cursor = useMemo(() => { - if (props.isSelected || typeof props.onSelect !== 'function') { + if (isSelected || typeof props.onSelect !== 'function') { return 'default'; } @@ -575,7 +586,7 @@ const SingleRoute = (props: Props) => { } return 'pointer'; - }, [props.error, props.isSelected, props.onSelect]); + }, [props.error, isSelected, props.onSelect]); const routeCardBadge = useMemo(() => { if (props.isFastest) { @@ -609,18 +620,16 @@ const SingleRoute = (props: Props) => { return (
{ const selectedRoute = routes.find((route) => route === props.selectedRoute); return selectedRoute ? [selectedRoute] : routes.slice(0, 1); - }, [showAll, routes]); + }, [showAll, routes, props.selectedRoute]); const fastestRoute = useMemo(() => { return routes.reduce( diff --git a/wormhole-connect/src/views/v2/Bridge/SwapInputs/index.tsx b/wormhole-connect/src/views/v2/Bridge/SwapInputs/index.tsx index 4b906e499..e8892506f 100644 --- a/wormhole-connect/src/views/v2/Bridge/SwapInputs/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/SwapInputs/index.tsx @@ -34,6 +34,7 @@ function SwapInputs() { } = useSelector((state: RootState) => state.transferInput); const canSwap = + !isTransactionInProgress && fromChain && !config.chains[fromChain]?.disabledAsDestination && toChain && diff --git a/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx b/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx index 2fd69e212..4384d9e49 100644 --- a/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx +++ b/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx @@ -17,6 +17,7 @@ import { RootState } from 'store'; import { disconnectWallet as disconnectFromStore } from 'store/wallet'; import { TransferWallet } from 'utils/wallet'; import { copyTextToClipboard, displayWalletAddress } from 'utils'; +import { joinClass } from 'utils/style'; import DownIcon from 'icons/Down'; import WalletIcons from 'icons/WalletIcons'; @@ -47,6 +48,11 @@ const useStyles = makeStyles()((theme: any) => ({ up: { transform: 'scaleY(-1)', }, + disabled: { + opacity: '0.6', + cursor: 'default', + clickEvent: 'none', + }, dropdown: { backgroundColor: theme.palette.popover.background, display: 'flex', @@ -76,6 +82,10 @@ const ConnectedWallet = (props: Props) => { const { classes } = useStyles(); + const { isTransactionInProgress } = useSelector( + (state: RootState) => state.transferInput, + ); + const wallet = useSelector((state: RootState) => state.wallet[props.type]); const [isOpen, setIsOpen] = useState(false); @@ -89,18 +99,18 @@ const ConnectedWallet = (props: Props) => { const connectWallet = useCallback(() => { popupState?.close(); setIsOpen(true); - }, []); + }, [popupState]); const copyAddress = useCallback(() => { copyTextToClipboard(wallet.address); popupState?.close(); setIsCopied(true); - }, [wallet.address]); + }, [popupState, wallet.address]); const disconnectWallet = useCallback(() => { dispatch(disconnectFromStore(props.type)); popupState?.close(); - }, [props.type]); + }, [dispatch, popupState, props.type]); useEffect(() => { if (isCopied) { @@ -110,13 +120,21 @@ const ConnectedWallet = (props: Props) => { } }, [isCopied]); + const popupTrigger = isTransactionInProgress ? {} : bindTrigger(popupState); + if (!wallet?.address) { return <>; } return ( <> -
+
{ }} wallet={sendingWallet} isSource={true} + isTransactionInProgress={isTransactionInProgress} />
@@ -331,6 +332,7 @@ const Bridge = () => { sourceToken, supportedSourceTokens, isFetchingSupportedSourceTokens, + isTransactionInProgress, sendingWallet, dispatch, ]); @@ -358,6 +360,7 @@ const Bridge = () => { }} wallet={receivingWallet} isSource={false} + isTransactionInProgress={isTransactionInProgress} />
); @@ -370,13 +373,16 @@ const Bridge = () => { sourceToken, supportedDestTokens, isFetchingSupportedDestTokens, + isTransactionInProgress, receivingWallet, dispatch, ]); // Header for Bridge view, which includes the title and settings icon. const bridgeHeader = useMemo(() => { - const isTxHistoryDisabled = !sendingWallet?.address; + const isTxHistoryDisabled = + !sendingWallet?.address || isTransactionInProgress; + return (
{ size={18} /> {
); - }, [sendingWallet?.address, classes.bridgeHeader, dispatch]); + }, [ + classes.bridgeHeader, + dispatch, + isTransactionInProgress, + sendingWallet?.address, + ]); const walletConnector = useMemo(() => { if (sendingWallet?.address && receivingWallet?.address) { From 2ea0bdf4fa998bc7f3ab163949c1786532c73a56 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Thu, 9 Jan 2025 23:44:35 +0300 Subject: [PATCH 6/8] Disable widget when tx in progress Signed-off-by: Emre Bogazliyanlioglu --- wormhole-connect/src/views/v2/Bridge/index.tsx | 4 +++- wormhole-connect/src/views/v2/TxHistory/Widget/Item.tsx | 3 ++- wormhole-connect/src/views/v2/TxHistory/Widget/index.tsx | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/wormhole-connect/src/views/v2/Bridge/index.tsx b/wormhole-connect/src/views/v2/Bridge/index.tsx index 44d0bb350..e5924a431 100644 --- a/wormhole-connect/src/views/v2/Bridge/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/index.tsx @@ -568,7 +568,9 @@ const Bridge = () => { return (
{header} - {config.ui.showInProgressWidget && } + {config.ui.showInProgressWidget && ( + + )} {bridgeHeader} {sourceAssetPicker} {destAssetPicker} diff --git a/wormhole-connect/src/views/v2/TxHistory/Widget/Item.tsx b/wormhole-connect/src/views/v2/TxHistory/Widget/Item.tsx index 12d315b50..47374dea0 100644 --- a/wormhole-connect/src/views/v2/TxHistory/Widget/Item.tsx +++ b/wormhole-connect/src/views/v2/TxHistory/Widget/Item.tsx @@ -77,6 +77,7 @@ const useStyles = makeStyles()((theme: any) => ({ type Props = { data: TransactionLocal; + disabled: boolean; }; const WidgetItem = (props: Props) => { @@ -246,7 +247,7 @@ const WidgetItem = (props: Props) => { diff --git a/wormhole-connect/src/views/v2/TxHistory/Widget/index.tsx b/wormhole-connect/src/views/v2/TxHistory/Widget/index.tsx index 267c55327..78e363ae0 100644 --- a/wormhole-connect/src/views/v2/TxHistory/Widget/index.tsx +++ b/wormhole-connect/src/views/v2/TxHistory/Widget/index.tsx @@ -35,7 +35,7 @@ const useStyles = makeStyles()((theme) => ({ }, })); -const TxHistoryWidget = () => { +const TxHistoryWidget = (props: { disabled: boolean }) => { const { classes } = useStyles(); const theme = useTheme(); @@ -58,7 +58,7 @@ const TxHistoryWidget = () => {
{transactions.map((tx) => ( - + ))}
); From fb64be4c9f9a958d339deb583120df57c003e346 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Mon, 13 Jan 2025 15:22:17 +0300 Subject: [PATCH 7/8] Replace invalid clickEvent CSS props with pointerEvents Signed-off-by: Emre Bogazliyanlioglu --- wormhole-connect/src/components/Button.tsx | 2 +- wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx | 2 +- wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx | 2 +- .../src/views/v2/Bridge/WalletConnector/Controller.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/wormhole-connect/src/components/Button.tsx b/wormhole-connect/src/components/Button.tsx index fe30bef6f..81f8b6f08 100644 --- a/wormhole-connect/src/components/Button.tsx +++ b/wormhole-connect/src/components/Button.tsx @@ -17,7 +17,7 @@ const useStyles = makeStyles()((theme: any) => ({ }, disabled: { cursor: 'not-allowed', - clickEvents: 'none', + pointerEvents: 'none', backgroundColor: theme.palette.button.disabled + ' !important', color: theme.palette.button.disabledText + ' !important', }, diff --git a/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx b/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx index 807388f16..cef4c4566 100644 --- a/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx @@ -49,7 +49,7 @@ const useStyles = makeStyles()((theme: any) => ({ disabled: { opacity: '0.6', cursor: 'default', - clickEvent: 'none', + pointerEvents: 'none', }, popover: { marginTop: '4px', diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index 545be48b6..2d1585338 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -60,7 +60,7 @@ const useStyles = makeStyles<{ isSelected: boolean }>()( disabled: { opacity: '0.6', cursor: 'default', - clickEvent: 'none', + pointerEvents: 'none', }, errorIcon: { color: theme.palette.error.main, diff --git a/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx b/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx index 4384d9e49..07e69fbcb 100644 --- a/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx +++ b/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx @@ -51,7 +51,7 @@ const useStyles = makeStyles()((theme: any) => ({ disabled: { opacity: '0.6', cursor: 'default', - clickEvent: 'none', + pointerEvents: 'none', }, dropdown: { backgroundColor: theme.palette.popover.background, From 6ccdd770d22634d3df0ae695fcd097711977e999 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Mon, 13 Jan 2025 16:10:41 +0300 Subject: [PATCH 8/8] Fix gas slider thumb glitch when expanding collapsible container Signed-off-by: Emre Bogazliyanlioglu --- wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx index 6d305ecdc..b27e0ac87 100644 --- a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx @@ -37,6 +37,7 @@ const useStyles = makeStyles()(() => ({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', + width: '100%', }, })); @@ -49,8 +50,11 @@ const StyledSlider = styled(Slider, { shouldForwardProp: (prop) => !['baseColor', 'railColor'].includes(prop.toString()), })(({ baseColor, railColor, theme }) => ({ + alignSelf: 'start', color: baseColor, height: 8, + left: '10px', + width: 'calc(100% - 20px)', '& .MuiSlider-rail': { height: '8px', backgroundColor: railColor, @@ -68,7 +72,7 @@ const StyledSlider = styled(Slider, { const StyledSwitch = styled(Switch)(({ theme }) => ({ padding: '9px 12px', - right: `-12px`, // reposition towards right to negate switch padding + right: `-9px`, // reposition towards right to negate switch padding '& .MuiSwitch-switchBase.Mui-checked': { color: theme.palette.primary.main, },