From 9fbec6ecad620f5a0085eb0325c81dde21cd4d84 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Wed, 8 Jan 2025 23:02:53 +0300 Subject: [PATCH 01/16] Remove Review Tx view Signed-off-by: Emre Bogazliyanlioglu --- wormhole-connect/src/hooks/useGasSlider.ts | 2 +- .../src/hooks/useSendTransaction.ts | 288 ++++++++++++++++++ .../views/v2/Bridge/Routes/SingleRoute.tsx | 38 ++- .../src/views/v2/Bridge/index.tsx | 148 ++++++--- 4 files changed, 419 insertions(+), 57 deletions(-) create mode 100644 wormhole-connect/src/hooks/useSendTransaction.ts diff --git a/wormhole-connect/src/hooks/useGasSlider.ts b/wormhole-connect/src/hooks/useGasSlider.ts index 120a72bb3..005538620 100644 --- a/wormhole-connect/src/hooks/useGasSlider.ts +++ b/wormhole-connect/src/hooks/useGasSlider.ts @@ -5,7 +5,7 @@ import { Chain } from '@wormhole-foundation/sdk'; type Props = { destChain: Chain | undefined; - destToken: string; + destToken: string | undefined; route?: string; valid: boolean; isTransactionInProgress: boolean; 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 56e1ccd95..b8bf6b4d1 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'; @@ -30,6 +32,7 @@ import { useGetTokens } from 'hooks/useGetTokens'; import { useTokens } from 'contexts/TokensContext'; import { Token } from 'config/tokens'; import { opacify } from 'utils/theme'; +import GasSlider from 'views/v2/Bridge/ReviewTransaction/GasSlider'; const HIGH_FEE_THRESHOLD = 20; // dollhairs @@ -105,17 +108,28 @@ const SingleRoute = (props: Props) => { const theme = useTheme(); const routeConfig = config.routes.get(props.route); - const { toChain: destChain, fromChain: sourceChain } = useSelector( - (state: RootState) => state.transferInput, - ); + const { + toChain: destChain, + fromChain: sourceChain, + isTransactionInProgress, + } = useSelector((state: RootState) => state.transferInput); const { getTokenPrice, isFetchingTokenPrices } = useTokens(); const { quote } = props; + const receiveNativeAmount = quote?.destinationNativeGas; const { sourceToken, destToken } = useGetTokens(); - const [feePrice, isHighFee, feeToken]: [ + const { disabled: isGasSliderDisabled, showGasSlider } = useGasSlider({ + destChain, + destToken: destToken?.key, + route: props.route, + valid: true, + isTransactionInProgress, + }); + + const [feePrice, isHighFee, feeTokenConfig]: [ number | undefined, boolean, Token | undefined, @@ -140,7 +154,7 @@ const SingleRoute = (props: Props) => { return <>You pay gas on {destChain}; } - if (!quote || !feePrice || !feeToken) { + if (!quote || !feePrice || !feeTokenConfig) { return <>; } @@ -148,7 +162,7 @@ const SingleRoute = (props: Props) => { let feeValue = `${amount.display( amount.truncate(quote!.relayFee!.amount, 6), - )} ${feeToken.display} (${feePriceFormatted})`; + )} ${feeTokenConfig.display} (${feePriceFormatted})`; // Wesley made me do it // Them PMs :-/ @@ -179,7 +193,7 @@ const SingleRoute = (props: Props) => { }, [ destChain, feePrice, - feeToken, + feeTokenConfig, props.route, quote, routeConfig.AUTOMATIC_DEPOSIT, @@ -609,6 +623,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 e78a84976..22cf81b63 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'; @@ -50,6 +51,7 @@ import { useGetTokens } from 'hooks/useGetTokens'; import { Token } from 'config/tokens'; import { useTokens } from 'contexts/TokensContext'; +import useSendTransaction from 'hooks/useSendTransaction'; const useStyles = makeStyles()((theme: any) => ({ assetPickerContainer: { @@ -82,6 +84,14 @@ const useStyles = makeStyles()((theme: any) => ({ alignItems: 'center', width: '100%', }, + confirmTransaction: { + padding: '8px 16px', + borderRadius: '8px', + height: '48px', + margin: 'auto', + maxWidth: '420px', + width: '100%', + }, spacer: { display: 'flex', flexDirection: 'column', @@ -110,9 +120,6 @@ const Bridge = () => { (state: RootState) => state.wallet, ); - const [selectedRoute, setSelectedRoute] = useState(); - const [willReviewTransaction, setWillReviewTransaction] = useState(false); - const { fromChain: sourceChain, toChain: destChain, @@ -121,6 +128,7 @@ const Bridge = () => { supportedSourceTokens, amount, validations, + isTransactionInProgress, } = useSelector((state: RootState) => state.transferInput); const { sourceToken, destToken } = useGetTokens(); @@ -140,7 +148,7 @@ const Bridge = () => { destChain, sourceToken, destToken, - route: selectedRoute, + route, }); // Compute and set destination tokens @@ -149,14 +157,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, @@ -164,15 +178,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; } @@ -180,9 +194,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(); @@ -308,14 +322,17 @@ const Bridge = () => { ); }, [ + classes.assetPickerContainer, + classes.assetPickerTitle, sourceChain, supportedSourceChains, sourceToken, sourceTokens, lastTokenCacheUpdate, supportedSourceTokens, - sendingWallet, isFetchingSupportedSourceTokens, + sendingWallet, + dispatch, ]); // Asset picker for the destination network and token @@ -347,12 +364,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. @@ -380,7 +401,7 @@ const Bridge = () => { ); - }, [sendingWallet?.address, config.ui]); + }, [sendingWallet?.address, classes.bridgeHeader, dispatch]); const walletConnector = useMemo(() => { if (sendingWallet?.address && receivingWallet?.address) { @@ -422,36 +443,72 @@ const Bridge = () => { const showRoutes = hasConnectedWallets && isWalletCompatible && hasEnteredAmount && !hasError; - const reviewTransactionDisabled = + const confirmTransactionDisabled = !sourceChain || !sourceToken || !destChain || !destToken || !hasConnectedWallets || !isWalletCompatible || - !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 @@ -460,20 +517,10 @@ const Bridge = () => { ? 'Please enter an amount' : isFetchingQuotes ? 'Loading quotes...' - : !selectedRoute + : !route ? 'Please select a quote' : ''; - if (willReviewTransaction) { - return ( - setWillReviewTransaction(false)} - /> - ); - } - return (
{header} @@ -492,17 +539,20 @@ const Bridge = () => { {showRoutes && ( { + dispatch(setTransferRoute(r)); + }} quotes={quotesMap} isLoading={isFetchingQuotes || isFetchingBalances} hasError={hasError} /> )} + {hasConnectedWallets ? ( - - {reviewTransactionButton} + + {confirmTransactionButton} ) : ( walletConnector From 3cfd0bea5dd8dad8b3e84d202b4be4e09d2565be Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Thu, 9 Jan 2025 09:28:53 +0300 Subject: [PATCH 02/16] 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 005538620..f7bfd15a9 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 | undefined; 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.getGasToken(toChainConfig.sdkName) diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index b8bf6b4d1..83fbf9422 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -125,7 +125,6 @@ const SingleRoute = (props: Props) => { destChain, destToken: destToken?.key, route: props.route, - valid: true, isTransactionInProgress, }); From bf3996a942edd1fbce4dab2ff82fafb3f6f0ff0b Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Thu, 9 Jan 2025 09:44:19 +0300 Subject: [PATCH 03/16] Refactor Signed-off-by: Emre Bogazliyanlioglu --- ...ransaction.ts => useConfirmTransaction.ts} | 53 ++- .../GasSlider.tsx => GasSlider/index.tsx} | 4 +- .../v2/Bridge/ReviewTransaction/SendError.tsx | 72 --- .../v2/Bridge/ReviewTransaction/index.tsx | 416 ------------------ .../views/v2/Bridge/Routes/SingleRoute.tsx | 4 + .../src/views/v2/Bridge/index.tsx | 127 ++++-- 6 files changed, 121 insertions(+), 555 deletions(-) rename wormhole-connect/src/hooks/{useSendTransaction.ts => useConfirmTransaction.ts} (87%) rename wormhole-connect/src/views/v2/Bridge/{ReviewTransaction/GasSlider.tsx => GasSlider/index.tsx} (97%) delete mode 100644 wormhole-connect/src/views/v2/Bridge/ReviewTransaction/SendError.tsx delete mode 100644 wormhole-connect/src/views/v2/Bridge/ReviewTransaction/index.tsx diff --git a/wormhole-connect/src/hooks/useSendTransaction.ts b/wormhole-connect/src/hooks/useConfirmTransaction.ts similarity index 87% rename from wormhole-connect/src/hooks/useSendTransaction.ts rename to wormhole-connect/src/hooks/useConfirmTransaction.ts index d0c79d106..feba71e2c 100644 --- a/wormhole-connect/src/hooks/useSendTransaction.ts +++ b/wormhole-connect/src/hooks/useConfirmTransaction.ts @@ -5,6 +5,7 @@ import { Context } from 'sdklegacy'; import config from 'config'; import { RouteContext } from 'contexts/RouteContext'; import { useUSDamountGetter } from 'hooks/useUSDamountGetter'; +import { useGetTokens } from 'hooks/useGetTokens'; import { setTxDetails, setSendTx, @@ -15,7 +16,6 @@ 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'; @@ -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, ); @@ -56,12 +59,12 @@ const useSendTransaction = (props: Props): ReturnProps => { amount, fromChain: sourceChain, toChain: destChain, - token: sourceToken, - destToken, route, validations, } = transferInput; + const { sourceToken, destToken } = useGetTokens(); + const wallet = useSelector((state: RootState) => state.wallet); const { sending: sendingWallet, receiving: receivingWallet } = wallet; @@ -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; } @@ -135,8 +141,6 @@ const useSendTransaction = (props: Props): ReturnProps => { dispatch(setIsTransactionInProgress(true)); - const sourceTokenConfig = config.tokens[sourceToken]; - try { const fromConfig = config.chains[sourceChain]; @@ -159,7 +163,7 @@ const useSendTransaction = (props: Props): ReturnProps => { const [sdkRoute, receipt] = await config.routes .get(route) .send( - sourceTokenConfig, + sourceToken, amount, sourceChain, sendingWallet.address, @@ -184,10 +188,7 @@ const useSendTransaction = (props: Props): ReturnProps => { let relayerFee: RelayerFee | undefined = undefined; if (quote.relayFee) { const { token, amount } = quote.relayFee; - const feeToken = config.sdkConverter.findTokenConfigV1( - token, - Object.values(config.tokens), - ); + const feeToken = config.tokens.get(token); const formattedFee = Number.parseFloat( toDecimals(amount.amount, amount.decimals, 6), @@ -195,7 +196,7 @@ const useSendTransaction = (props: Props): ReturnProps => { relayerFee = { fee: formattedFee, - tokenKey: feeToken?.key || '', + token: feeToken?.tuple, }; } @@ -208,13 +209,11 @@ const useSendTransaction = (props: Props): ReturnProps => { 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) + receivedToken: destToken.tuple, + token: sourceToken.tuple, + tokenAddress: sourceToken.tuple[1], + tokenKey: sourceToken.key, + tokenDecimals: sourceToken.decimals, relayerFee, receiveAmount: quote.destinationToken.amount, receiveNativeAmount, @@ -279,7 +278,7 @@ const useSendTransaction = (props: Props): ReturnProps => { }; return { - send, + onConfirm, error, errorInternal, }; diff --git a/wormhole-connect/src/views/v2/Bridge/ReviewTransaction/GasSlider.tsx b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx similarity index 97% rename from wormhole-connect/src/views/v2/Bridge/ReviewTransaction/GasSlider.tsx rename to wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx index b8520a8a4..a3c4dc2fa 100644 --- a/wormhole-connect/src/views/v2/Bridge/ReviewTransaction/GasSlider.tsx +++ b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx @@ -99,7 +99,7 @@ const GasSlider = (props: { const destChainConfig = config.chains[destChain!]; const nativeGasToken = config.tokens.getGasToken(destChain!); - const [isGasSliderOpen, setIsGasSliderOpen] = useState(!props.disabled); + const [isGasSliderOpen, setIsGasSliderOpen] = useState(false); const [percentage, setPercentage] = useState(0); const [debouncedPercentage] = useDebounce(percentage, 500); @@ -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/ReviewTransaction/index.tsx b/wormhole-connect/src/views/v2/Bridge/ReviewTransaction/index.tsx deleted file mode 100644 index 1a1158567..000000000 --- a/wormhole-connect/src/views/v2/Bridge/ReviewTransaction/index.tsx +++ /dev/null @@ -1,416 +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 { 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 { 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'; -import { useGetTokens } from 'hooks/useGetTokens'; - -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, - 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 { sourceToken, destToken } = useGetTokens(); - - const { disabled: isGasSliderDisabled, showGasSlider } = useGasSlider({ - destChain, - destToken: destToken!.key, - 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)); - - 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( - sourceToken, - 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.tokens.get(token); - - const formattedFee = Number.parseFloat( - toDecimals(amount.amount, amount.decimals, 6), - ); - - relayerFee = { - fee: formattedFee, - token: feeToken?.tuple, - }; - } - - const txTimestamp = Date.now(); - const txDetails = { - sendTx: txId, - sender: sendingWallet.address, - amount, - recipient: receivingWallet.address, - toChain: receipt.to, - fromChain: receipt.from, - tokenAddress: getWrappedToken(sourceToken).tokenId!.address.toString(), - token: sourceToken.tuple, - tokenDecimals: sourceToken.decimals, - receivedToken: destToken.tuple, // 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; diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index 83fbf9422..7e66b7ea3 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -31,8 +31,12 @@ import CheapestRoute from 'icons/CheapestRoute'; import { useGetTokens } from 'hooks/useGetTokens'; import { useTokens } from 'contexts/TokensContext'; import { Token } from 'config/tokens'; +<<<<<<< HEAD import { opacify } from 'utils/theme'; import GasSlider from 'views/v2/Bridge/ReviewTransaction/GasSlider'; +======= +import GasSlider from 'views/v2/Bridge/GasSlider'; +>>>>>>> f16701a1 (Refactor) 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 22cf81b63..f76031d24 100644 --- a/wormhole-connect/src/views/v2/Bridge/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/index.tsx @@ -1,26 +1,33 @@ -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 { 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 +36,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,20 +47,12 @@ 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 { useWalletCompatibility } from 'hooks/useWalletCompatibility'; -import useGetTokenBalances from 'hooks/useGetTokenBalances'; import { useGetTokens } from 'hooks/useGetTokens'; import { Token } from 'config/tokens'; import { useTokens } from 'contexts/TokensContext'; -import useSendTransaction from 'hooks/useSendTransaction'; const useStyles = makeStyles()((theme: any) => ({ assetPickerContainer: { @@ -74,15 +75,9 @@ const useStyles = makeStyles()((theme: any) => ({ 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', @@ -92,6 +87,13 @@ const useStyles = makeStyles()((theme: any) => ({ maxWidth: '420px', width: '100%', }, + copyIcon: { + fontSize: '14px', + }, + ctaContainer: { + marginTop: '8px', + width: '100%', + }, spacer: { display: 'flex', flexDirection: 'column', @@ -112,6 +114,7 @@ const Bridge = () => { const dispatch = useDispatch(); const { lastTokenCacheUpdate } = useTokens(); + const [errorCopied, setErrorCopied] = useState(false); const mobile = useMediaQuery(theme.breakpoints.down('sm')); @@ -161,10 +164,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 @@ -243,7 +246,7 @@ const Bridge = () => { // All supported chains from the given configuration and any custom override const supportedChains = useMemo( () => config.routes.allSupportedChains(), - [config.chainsArr], + [config.chains], ); const sourceTokens = useMemo(() => { @@ -263,7 +266,7 @@ const Bridge = () => { supportedChains.includes(chain.key) ); }); - }, [config.chainsArr, destChain, supportedChains]); + }, [destChain, supportedChains]); // Supported chains for the destination network const supportedDestChains = useMemo(() => { @@ -273,7 +276,7 @@ const Bridge = () => { !chain.disabledAsDestination && supportedChains.includes(chain.key), ); - }, [config.chainsArr, sourceChain, supportedChains]); + }, [sourceChain, supportedChains]); // Connect bridge header, which renders any custom overrides for the header const header = useMemo(() => { @@ -291,7 +294,7 @@ const Bridge = () => { } return ; - }, [config.ui]); + }, []); // Asset picker for the source network and token const sourceAssetPicker = useMemo(() => { @@ -434,6 +437,54 @@ const Bridge = () => { routes: sortedRoutes, }); + 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; @@ -464,9 +515,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 = @@ -548,7 +597,7 @@ const Bridge = () => { hasError={hasError} /> )} - + {transactionError} {hasConnectedWallets ? ( From 6648e239f79da1fedf8f6d82812193c6085e7c97 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Thu, 9 Jan 2025 23:27:50 +0300 Subject: [PATCH 04/16] Disable/dim all actionable components when tx in progress Signed-off-by: Emre Bogazliyanlioglu --- .../src/views/v2/Bridge/AmountInput/index.tsx | 22 ++++++--- .../src/views/v2/Bridge/AssetPicker/index.tsx | 26 ++++++++--- .../views/v2/Bridge/Routes/SingleRoute.tsx | 46 +++++++++++-------- .../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, 101 insertions(+), 39 deletions(-) diff --git a/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx b/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx index b87ebf15d..0bef6cd5b 100644 --- a/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx @@ -161,6 +161,10 @@ const AmountInput = (props: Props) => { amount ? sdkAmount.display(amount) : '', ); + const { fromChain: sourceChain, isTransactionInProgress } = useSelector( + (state: RootState) => state.transferInput, + ); + const { sourceToken } = useGetTokens(); const { getTokenPrice } = useTokens(); @@ -177,8 +181,8 @@ const AmountInput = (props: Props) => { }, [amount]); const isInputDisabled = useMemo( - () => !props.sourceChain || !sourceToken, - [props.sourceChain, sourceToken], + () => isTransactionInProgress || !sourceChain || !sourceToken, + [isTransactionInProgress, sourceChain, sourceToken], ); const balance = useMemo(() => { @@ -211,16 +215,20 @@ const AmountInput = (props: Props) => { ); }, [ - classes.balance, isInputDisabled, + sendingWallet.address, + classes.balance, props.isFetchingTokenBalance, props.tokenBalance, - sendingWallet.address, ]); - const handleChange = useCallback((newValue: string): void => { - setAmountInput(newValue); - }, []); + const handleChange = useCallback( + (newValue: string): void => { + dispatch(setAmount(newValue)); + setAmountInput(newValue); + }, + [dispatch], + ); const tokenPriceAdornment = useMemo(() => { const price = calculateUSDPrice( diff --git a/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx b/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx index 21068ad97..874b0e4fa 100644 --- a/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx @@ -3,7 +3,11 @@ import { makeStyles } from 'tss-react/mui'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Popover from '@mui/material/Popover'; -import { usePopupState, bindPopover } from 'material-ui-popup-state/hooks'; +import { + usePopupState, + bindPopover, + bindTrigger, +} from 'material-ui-popup-state/hooks'; import Typography from '@mui/material/Typography'; import DownIcon from '@mui/icons-material/ExpandMore'; @@ -20,6 +24,7 @@ import { Chain } from '@wormhole-foundation/sdk'; import AssetBadge from 'components/AssetBadge'; import { Token } from 'config/tokens'; import { Backdrop } from '@mui/material'; +import { joinClass } from 'utils/style'; const useStyles = makeStyles()((theme: any) => ({ inputArea: { @@ -50,8 +55,8 @@ const useStyles = makeStyles()((theme: any) => ({ justifyContent: 'space-between', }, disabled: { - opacity: '0.4', - cursor: 'not-allowed', + opacity: '0.6', + cursor: 'default', clickEvent: 'none', }, popover: { @@ -81,6 +86,7 @@ type Props = { setChain: (value: Chain) => void; wallet: WalletData; isSource: boolean; + isTransactionInProgress: boolean; }; const AssetPicker = (props: Props) => { @@ -161,15 +167,23 @@ const AssetPicker = (props: Props) => { ); }, [chainConfig, props.token]); + const triggerProps = props.isTransactionInProgress + ? {} + : bindTrigger(popupState); + return ( <> >>>>>> f16701a1 (Refactor) const HIGH_FEE_THRESHOLD = 20; // dollhairs @@ -93,6 +90,11 @@ const useStyles = makeStyles()((theme: any) => ({ width: '34px', marginRight: '12px', }, + disabled: { + opacity: '0.6', + cursor: 'default', + clickEvent: 'none', + }, })); type Props = { @@ -118,9 +120,9 @@ const SingleRoute = (props: Props) => { isTransactionInProgress, } = useSelector((state: RootState) => state.transferInput); - const { getTokenPrice, isFetchingTokenPrices } = useTokens(); + const { getTokenPrice, lastTokenPriceUpdate } = useTokens(); - const { quote } = props; + const { quote, isSelected } = props; const receiveNativeAmount = quote?.destinationNativeGas; const { sourceToken, destToken } = useGetTokens(); @@ -150,7 +152,7 @@ const SingleRoute = (props: Props) => { } return [feePrice, feePrice > HIGH_FEE_THRESHOLD, feeToken]; - }, [quote?.relayFee]); + }, [getTokenPrice, quote?.relayFee]); const relayerFee = useMemo(() => { if (!routeConfig.AUTOMATIC_DEPOSIT) { @@ -250,12 +252,17 @@ const SingleRoute = (props: Props) => { >{`${gasTokenAmount} ${nativeGasToken.symbol}${gasTokenPriceStr}`} ); + // ES Lint complains that lastTokenPriceUpdate is unused/unnecessary here... but that's wrong. + // We want to recompute the price after we update conversion rates. + // + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ destChain, props.destinationGasDrop, - theme.palette.text.primary, + getTokenPrice, + lastTokenPriceUpdate, theme.palette.text.secondary, - isFetchingTokenPrices, + theme.palette.text.primary, ]); const timeToDestination = useMemo( @@ -536,6 +543,7 @@ const SingleRoute = (props: Props) => { }, [ destChain, destToken, + getTokenPrice, props.error, providerText, receiveAmount, @@ -546,7 +554,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'; } @@ -555,7 +563,7 @@ const SingleRoute = (props: Props) => { } return 'pointer'; - }, [props.error, props.isSelected, props.onSelect]); + }, [props.error, isSelected, props.onSelect]); const routeCardBadge = useMemo(() => { if (props.isFastest) { @@ -589,20 +597,22 @@ 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 0bdcbb4e5..6f5349e18 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 7fb1ac905..9e02fb5f4 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 WalletIcons from 'icons/WalletIcons'; import config from 'config'; @@ -38,6 +39,11 @@ const useStyles = makeStyles()((theme: any) => ({ color: theme.palette.textSecondary, marginLeft: '8px', }, + disabled: { + opacity: '0.6', + cursor: 'default', + clickEvent: 'none', + }, dropdown: { backgroundColor: theme.palette.popover.background, display: 'flex', @@ -67,6 +73,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); @@ -80,18 +90,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) { @@ -101,13 +111,21 @@ const ConnectedWallet = (props: Props) => { } }, [isCopied]); + const popupTrigger = isTransactionInProgress ? {} : bindTrigger(popupState); + if (!wallet?.address) { return <>; } return ( <> -
+
{ }} wallet={sendingWallet} isSource={true} + isTransactionInProgress={isTransactionInProgress} />
@@ -334,6 +335,7 @@ const Bridge = () => { lastTokenCacheUpdate, supportedSourceTokens, isFetchingSupportedSourceTokens, + isTransactionInProgress, sendingWallet, dispatch, ]); @@ -363,6 +365,7 @@ const Bridge = () => { }} wallet={receivingWallet} isSource={false} + isTransactionInProgress={isTransactionInProgress} />
); @@ -375,13 +378,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 5b0b71bf107e0f7c5d006611021cb59785d99c48 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Thu, 9 Jan 2025 23:44:35 +0300 Subject: [PATCH 05/16] 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 177b23b5b..6d6aa9cc9 100644 --- a/wormhole-connect/src/views/v2/Bridge/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/index.tsx @@ -584,7 +584,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 4b1d33ff8..dff6471d9 100644 --- a/wormhole-connect/src/views/v2/TxHistory/Widget/Item.tsx +++ b/wormhole-connect/src/views/v2/TxHistory/Widget/Item.tsx @@ -81,6 +81,7 @@ const useStyles = makeStyles()((theme: any) => ({ type Props = { data: TransactionLocal; + disabled: boolean; }; const WidgetItem = (props: Props) => { @@ -270,7 +271,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 b7951b74d..5cbbbc889 100644 --- a/wormhole-connect/src/views/v2/TxHistory/Widget/index.tsx +++ b/wormhole-connect/src/views/v2/TxHistory/Widget/index.tsx @@ -36,7 +36,7 @@ const useStyles = makeStyles()((theme) => ({ }, })); -const TxHistoryWidget = () => { +const TxHistoryWidget = (props: { disabled: boolean }) => { const { classes } = useStyles(); const theme = useTheme(); @@ -77,7 +77,7 @@ const TxHistoryWidget = () => {
{transactions.map((tx) => ( - + ))}
); From 037021822a6cc7ae85acf76cc86aebe2458a85a2 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Mon, 13 Jan 2025 15:22:17 +0300 Subject: [PATCH 06/16] 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 874b0e4fa..8322fe910 100644 --- a/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/AssetPicker/index.tsx @@ -57,7 +57,7 @@ const useStyles = makeStyles()((theme: any) => ({ disabled: { opacity: '0.6', cursor: 'default', - clickEvent: 'none', + pointerEvents: 'none', }, popover: { marginLeft: '-1px', diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index 869fd2dad..502bb3df8 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -93,7 +93,7 @@ const useStyles = makeStyles()((theme: any) => ({ disabled: { opacity: '0.6', cursor: 'default', - clickEvent: 'none', + pointerEvents: 'none', }, })); diff --git a/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx b/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx index 9e02fb5f4..85d1358c2 100644 --- a/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx +++ b/wormhole-connect/src/views/v2/Bridge/WalletConnector/Controller.tsx @@ -42,7 +42,7 @@ const useStyles = makeStyles()((theme: any) => ({ disabled: { opacity: '0.6', cursor: 'default', - clickEvent: 'none', + pointerEvents: 'none', }, dropdown: { backgroundColor: theme.palette.popover.background, From 3d53e2f36db4c24954f2c4df840a1960df485406 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Mon, 13 Jan 2025 16:10:41 +0300 Subject: [PATCH 07/16] 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 a3c4dc2fa..67e79554b 100644 --- a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx @@ -38,6 +38,7 @@ const useStyles = makeStyles()(() => ({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', + width: '100%', }, })); @@ -50,8 +51,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, @@ -69,7 +73,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, }, From f3f77f2ed7267766b2bd25e27b8218048a0b4901 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Tue, 18 Feb 2025 15:51:41 -0800 Subject: [PATCH 08/16] Fix eslint warnings Signed-off-by: Emre Bogazliyanlioglu --- .../AssetPicker/SearchableList/index.tsx | 6 ++++-- .../src/views/v2/Bridge/GasSlider/index.tsx | 6 ++++-- .../views/v2/Bridge/Routes/SingleRoute.tsx | 8 +++++--- .../src/views/v2/Bridge/SwapInputs/index.tsx | 20 ++++--------------- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/wormhole-connect/src/views/v2/Bridge/AssetPicker/SearchableList/index.tsx b/wormhole-connect/src/views/v2/Bridge/AssetPicker/SearchableList/index.tsx index d30fea9b7..2797a4eed 100644 --- a/wormhole-connect/src/views/v2/Bridge/AssetPicker/SearchableList/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/AssetPicker/SearchableList/index.tsx @@ -37,9 +37,11 @@ function SearchableList(props: SearchableListProps): ReactNode { const scrollbarClass = useCustomScrollbar(); const [query, setQuery] = useState(''); + const { items, filterFn } = props; + const filteredList = useMemo(() => { - return props.items.filter((item) => props.filterFn(item, query)); - }, [props.items, props.filterFn, query]); + return items.filter((item) => filterFn(item, query)); + }, [items, filterFn, query]); return ( diff --git a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx index 67e79554b..a2e427283 100644 --- a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx @@ -98,7 +98,7 @@ const GasSlider = (props: { (state: RootState) => state.transferInput, ); - const { getTokenPrice, isFetchingTokenPrices } = useTokens(); + const { getTokenPrice, lastTokenPriceUpdate } = useTokens(); const destChainConfig = config.chains[destChain!]; const nativeGasToken = config.tokens.getGasToken(destChain!); @@ -132,9 +132,11 @@ const GasSlider = (props: { {`${tokenAmount} ${nativeGasToken.symbol} ${tokenPrice}`}
); + // We want to recompute the price after we update conversion rates (lastTokenPriceUpdate). + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ nativeGasToken, - isFetchingTokenPrices, + lastTokenPriceUpdate, props.destinationGasDrop, destChain, ]); diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index 502bb3df8..77ecad86c 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -252,12 +252,11 @@ const SingleRoute = (props: Props) => { >{`${gasTokenAmount} ${nativeGasToken.symbol}${gasTokenPriceStr}`} ); - // ES Lint complains that lastTokenPriceUpdate is unused/unnecessary here... but that's wrong. - // We want to recompute the price after we update conversion rates. - // + // We want to recompute the price after we update conversion rates (lastTokenPriceUpdate). // eslint-disable-next-line react-hooks/exhaustive-deps }, [ destChain, + lastTokenPriceUpdate, props.destinationGasDrop, getTokenPrice, lastTokenPriceUpdate, @@ -540,10 +539,13 @@ const SingleRoute = (props: Props) => { component="div" >{`${usdValue} ${providerText}`} ); + // We want to recompute the price after we update conversion rates (lastTokenPriceUpdate). + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ destChain, destToken, getTokenPrice, + lastTokenPriceUpdate, props.error, providerText, receiveAmount, diff --git a/wormhole-connect/src/views/v2/Bridge/SwapInputs/index.tsx b/wormhole-connect/src/views/v2/Bridge/SwapInputs/index.tsx index 6f5349e18..b115439b1 100644 --- a/wormhole-connect/src/views/v2/Bridge/SwapInputs/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/SwapInputs/index.tsx @@ -25,13 +25,9 @@ function SwapInputs() { const dispatch = useDispatch(); const [rotateAnimation, setRotateAnimation] = useState(''); - const { - isTransactionInProgress, - fromChain, - toChain, - destToken, - token: sourceToken, - } = useSelector((state: RootState) => state.transferInput); + const { isTransactionInProgress, fromChain, toChain } = useSelector( + (state: RootState) => state.transferInput, + ); const canSwap = !isTransactionInProgress && @@ -50,15 +46,7 @@ function SwapInputs() { dispatch(swapInputs()); dispatch(swapWallets()); dispatch(setAmount('')); - }, [ - fromChain, - toChain, - sourceToken, - destToken, - canSwap, - isTransactionInProgress, - dispatch, - ]); + }, [canSwap, isTransactionInProgress, dispatch]); const { classes } = useStyles(); From 86a7cdc512eb5d62f8e44878ef1f19399d632dfd Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Tue, 18 Feb 2025 16:03:42 -0800 Subject: [PATCH 09/16] Fix old token config references Signed-off-by: Emre Bogazliyanlioglu --- wormhole-connect/src/hooks/useConfirmTransaction.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/wormhole-connect/src/hooks/useConfirmTransaction.ts b/wormhole-connect/src/hooks/useConfirmTransaction.ts index feba71e2c..ae1199523 100644 --- a/wormhole-connect/src/hooks/useConfirmTransaction.ts +++ b/wormhole-connect/src/hooks/useConfirmTransaction.ts @@ -105,7 +105,9 @@ const useSendTransaction = (props: Props): ReturnProps => { // The results of this check will be written back to Redux store (see transferInput.validations). await validate({ transferInput, relay, wallet }, dispatch, () => false); - if (!isTransferValid(validations)) { + const valid = isTransferValid(validations); + + if (!valid || !route) { return; } @@ -142,9 +144,9 @@ const useSendTransaction = (props: Props): ReturnProps => { dispatch(setIsTransactionInProgress(true)); try { - const fromConfig = config.chains[sourceChain]; + const fromConfig = config.chains[sourceChain!]; - if (fromConfig && fromConfig?.context === Context.ETH) { + if (fromConfig?.context === Context.ETH) { const chainId = fromConfig.chainId; if (typeof chainId !== 'number') { @@ -201,7 +203,6 @@ const useSendTransaction = (props: Props): ReturnProps => { } const txTimestamp = Date.now(); - const txDetails = { sendTx: txId, sender: sendingWallet.address, From 1570b1eff2453d0944275897f6ef48d398c52d20 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Tue, 18 Feb 2025 16:30:04 -0800 Subject: [PATCH 10/16] Add missing dispatches Signed-off-by: Emre Bogazliyanlioglu --- wormhole-connect/src/views/v2/Bridge/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/wormhole-connect/src/views/v2/Bridge/index.tsx b/wormhole-connect/src/views/v2/Bridge/index.tsx index 6d6aa9cc9..58f4c2cd8 100644 --- a/wormhole-connect/src/views/v2/Bridge/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/index.tsx @@ -173,18 +173,20 @@ const Bridge = () => { // After the auto-selection, we set selectedRoute when user clicks on a route in the list useEffect(() => { if (sortedRoutesWithQuotes.length === 0) { - setTransferRoute(''); + dispatch(setTransferRoute('')); } else { const preferredRoute = sortedRoutesWithQuotes.find( (route) => route.route === preferredRouteName, ); const autoselectedRoute = route ?? preferredRoute?.route ?? sortedRoutesWithQuotes[0].route; + const isSelectedRouteValid = - sortedRoutesWithQuotes.findIndex((r) => r.route === route) > -1; + sortedRoutesWithQuotes.findIndex((r) => r.route === autoselectedRoute) > + -1; if (!isSelectedRouteValid) { - setTransferRoute(''); + dispatch(setTransferRoute('')); } // If no route is autoselected or we already have a valid selected route, @@ -197,7 +199,7 @@ const Bridge = () => { (rs) => rs.route === autoselectedRoute, ); - if (routeData) setTransferRoute(routeData.route); + if (routeData) dispatch(setTransferRoute(routeData.route)); } }, [preferredRouteName, route, sortedRoutesWithQuotes]); From c90165ccc38af436015f5843d501541f3ba5eb52 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Wed, 19 Feb 2025 14:01:22 -0800 Subject: [PATCH 11/16] Fix lint errors Signed-off-by: Emre Bogazliyanlioglu --- wormhole-connect/src/hooks/useConfirmTransaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wormhole-connect/src/hooks/useConfirmTransaction.ts b/wormhole-connect/src/hooks/useConfirmTransaction.ts index ae1199523..1bbcfda6e 100644 --- a/wormhole-connect/src/hooks/useConfirmTransaction.ts +++ b/wormhole-connect/src/hooks/useConfirmTransaction.ts @@ -144,7 +144,7 @@ const useSendTransaction = (props: Props): ReturnProps => { dispatch(setIsTransactionInProgress(true)); try { - const fromConfig = config.chains[sourceChain!]; + const fromConfig = config.chains[sourceChain]; if (fromConfig?.context === Context.ETH) { const chainId = fromConfig.chainId; From 1299896a9645d99cedd189e8039677ef31ece981 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Wed, 19 Feb 2025 14:57:02 -0800 Subject: [PATCH 12/16] Fix naming Signed-off-by: Emre Bogazliyanlioglu --- wormhole-connect/src/hooks/useConfirmTransaction.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wormhole-connect/src/hooks/useConfirmTransaction.ts b/wormhole-connect/src/hooks/useConfirmTransaction.ts index 1bbcfda6e..ada97388e 100644 --- a/wormhole-connect/src/hooks/useConfirmTransaction.ts +++ b/wormhole-connect/src/hooks/useConfirmTransaction.ts @@ -42,10 +42,11 @@ type ReturnProps = { onConfirm: () => void; }; -const useSendTransaction = (props: Props): ReturnProps => { +const useConfirmTransaction = (props: Props): ReturnProps => { const dispatch = useDispatch(); const [error, setError] = useState(undefined); + // errorInternal can be a result of custom validation, hence of any type. // eslint-disable-next-line @typescript-eslint/no-explicit-any const [errorInternal, setErrorInternal] = useState( undefined, @@ -285,4 +286,4 @@ const useSendTransaction = (props: Props): ReturnProps => { }; }; -export default useSendTransaction; +export default useConfirmTransaction; From bff585d9f9cd1cb61f7aa0ca2b91703f3ada0683 Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Fri, 21 Feb 2025 17:46:54 -0800 Subject: [PATCH 13/16] Update gas top-up control Signed-off-by: Emre Bogazliyanlioglu --- .../src/views/v2/Bridge/GasSlider/index.tsx | 114 +++++++++--------- .../views/v2/Bridge/Routes/SingleRoute.tsx | 19 +-- 2 files changed, 67 insertions(+), 66 deletions(-) diff --git a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx index a2e427283..1080f9ab1 100644 --- a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx @@ -7,10 +7,11 @@ import { useTheme } from '@mui/material'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Collapse from '@mui/material/Collapse'; -import Slider from '@mui/material/Slider'; import Stack from '@mui/material/Stack'; import Switch from '@mui/material/Switch'; import { styled } from '@mui/material/styles'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Typography from '@mui/material/Typography'; import { amount } from '@wormhole-foundation/sdk'; @@ -40,34 +41,8 @@ const useStyles = makeStyles()(() => ({ alignItems: 'center', width: '100%', }, -})); - -type SliderProps = { - baseColor: string; - railColor: string; -}; - -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, - opacity: 0.1, - }, - '& .MuiSlider-track': { - height: '8px', - }, - '& .MuiSlider-thumb': { - height: 20, - width: 20, - backgroundColor: theme.palette.primary.main, + gasButton: { + borderRadius: '8px', }, })); @@ -105,6 +80,7 @@ const GasSlider = (props: { const [isGasSliderOpen, setIsGasSliderOpen] = useState(false); const [percentage, setPercentage] = useState(0); + const [gasSelection, setGasSelection] = useState(); const [debouncedPercentage] = useDebounce(percentage, 500); @@ -154,7 +130,7 @@ const GasSlider = (props: { alignItems="center" justifyContent="space-between" > - {`Need more gas on ${destChain}?`} + {`Need extra ${nativeGasToken.symbol} on ${destChain}?`} -
- - {`Use the slider to buy extra ${nativeGasToken.symbol} for future transactions.`} - -
- `${percentage}%`} - valueLabelDisplay="auto" - onChange={(e: any) => setPercentage(e.target.value)} - /> -
- - Additional gas - - - {nativeGasPrice} - -
+ + { + setGasSelection(selection); + setPercentage( + selection === 'small' + ? 25 + : selection === 'medium' + ? 50 + : selection === 'large' + ? 100 + : 0, + ); + }} + > + + Small + + + Medium + + + Large + + +
+ + Additional gas + + + {nativeGasPrice} +
-
+ diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index 77ecad86c..4f4cc27ab 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -639,14 +639,17 @@ const SingleRoute = (props: Props) => { {warningMessages} {showGasSlider && ( - - - + <> + + + + + )} From d9a50acdf6da22c186ff0d759ea31188ce51148c Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Mon, 24 Feb 2025 17:08:31 -0800 Subject: [PATCH 14/16] Revert "Update gas top-up control" This reverts commit f4aff981f925257a161afba89043151edc78294e. --- .../src/views/v2/Bridge/GasSlider/index.tsx | 114 +++++++++--------- .../views/v2/Bridge/Routes/SingleRoute.tsx | 19 ++- 2 files changed, 66 insertions(+), 67 deletions(-) diff --git a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx index 1080f9ab1..9655d3f39 100644 --- a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx @@ -7,11 +7,10 @@ import { useTheme } from '@mui/material'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Collapse from '@mui/material/Collapse'; +import Slider from '@mui/material/Slider'; import Stack from '@mui/material/Stack'; import Switch from '@mui/material/Switch'; import { styled } from '@mui/material/styles'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Typography from '@mui/material/Typography'; import { amount } from '@wormhole-foundation/sdk'; @@ -41,8 +40,34 @@ const useStyles = makeStyles()(() => ({ alignItems: 'center', width: '100%', }, - gasButton: { - borderRadius: '8px', +})); + +type SliderProps = { + baseColor: string; + railColor: string; +}; + +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, + opacity: 0.1, + }, + '& .MuiSlider-track': { + height: '8px', + }, + '& .MuiSlider-thumb': { + height: 20, + width: 20, + backgroundColor: theme.palette.primary.main, }, })); @@ -80,7 +105,6 @@ const GasSlider = (props: { const [isGasSliderOpen, setIsGasSliderOpen] = useState(false); const [percentage, setPercentage] = useState(0); - const [gasSelection, setGasSelection] = useState(); const [debouncedPercentage] = useDebounce(percentage, 500); @@ -130,7 +154,7 @@ const GasSlider = (props: { alignItems="center" justifyContent="space-between" > - {`Need extra ${nativeGasToken.symbol} on ${destChain}?`} + {`Need more gas on ${destChain}?`} - - { - setGasSelection(selection); - setPercentage( - selection === 'small' - ? 25 - : selection === 'medium' - ? 50 - : selection === 'large' - ? 100 - : 0, - ); - }} - > - - Small - - - Medium - - - Large - - -
- - Additional gas - - - {nativeGasPrice} - +
+ + {`Use the slider to buy extra ${nativeGasToken.symbol} for future transactions.`} + +
+ `${percentage}%`} + valueLabelDisplay="auto" + onChange={(e: any) => setPercentage(e.target.value)} + /> +
+ + Additional gas + + + {nativeGasPrice} + +
- +
diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index 4f4cc27ab..77ecad86c 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -639,17 +639,14 @@ const SingleRoute = (props: Props) => { {warningMessages} {showGasSlider && ( - <> - - - - - + + + )} From a55aa88a16b3ff5350871d3531e43f523a5ad85f Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Mon, 24 Feb 2025 17:17:26 -0800 Subject: [PATCH 15/16] Slider styling Signed-off-by: Emre Bogazliyanlioglu --- .../src/views/v2/Bridge/GasSlider/index.tsx | 10 ++++++++-- .../views/v2/Bridge/Routes/SingleRoute.tsx | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx index 9655d3f39..4ab4b98f9 100644 --- a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx @@ -28,6 +28,12 @@ const useStyles = makeStyles()(() => ({ overflow: 'visible', padding: '0 4px', }, + cardContent: { + paddingTop: '8px', + ':last-child': { + padding: '16px 20px', + }, + }, container: { display: 'flex', flexDirection: 'column', @@ -148,7 +154,7 @@ const GasSlider = (props: { return ( - + { {warningMessages} {showGasSlider && ( - - - + <> + + + + + )} From a68af7b4d74cfbbb2444eba8384fb2ad82e291de Mon Sep 17 00:00:00 2001 From: Emre Bogazliyanlioglu Date: Tue, 25 Feb 2025 15:21:12 -0800 Subject: [PATCH 16/16] Refactor Signed-off-by: Emre Bogazliyanlioglu --- .../src/views/v2/Bridge/GasSlider/index.tsx | 114 ++++++++---------- 1 file changed, 50 insertions(+), 64 deletions(-) diff --git a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx index 4ab4b98f9..e8fe914cf 100644 --- a/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/GasSlider/index.tsx @@ -4,8 +4,6 @@ import { makeStyles } from 'tss-react/mui'; import { useDebounce } from 'use-debounce'; import { useTheme } from '@mui/material'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; import Collapse from '@mui/material/Collapse'; import Slider from '@mui/material/Slider'; import Stack from '@mui/material/Stack'; @@ -21,18 +19,12 @@ import { setToNativeToken } from 'store/relay'; import { useTokens } from 'contexts/TokensContext'; const useStyles = makeStyles()(() => ({ - card: { + content: { width: '100%', cursor: 'pointer', maxWidth: '420px', overflow: 'visible', - padding: '0 4px', - }, - cardContent: { - paddingTop: '8px', - ':last-child': { - padding: '16px 20px', - }, + padding: '16px 20px', }, container: { display: 'flex', @@ -153,62 +145,56 @@ const GasSlider = (props: { } return ( - - - - {`Need more gas on ${destChain}?`} - { - const { checked } = e.target; - - setIsGasSliderOpen(checked); - - if (!checked) { - setPercentage(0); - dispatch(setToNativeToken(0)); - } - }} - /> - - -
- - {`Use the slider to buy extra ${nativeGasToken.symbol} for future transactions.`} - -
- `${percentage}%`} - valueLabelDisplay="auto" - onChange={(e: any) => setPercentage(e.target.value)} - /> -
- - Additional gas - - - {nativeGasPrice} - -
+
+ + {`Need more gas on ${destChain}?`} + { + const { checked } = e.target; + + setIsGasSliderOpen(checked); + + if (!checked) { + setPercentage(0); + dispatch(setToNativeToken(0)); + } + }} + /> + + +
+ + {`Use the slider to buy extra ${nativeGasToken.symbol} for future transactions.`} + +
+ `${percentage}%`} + valueLabelDisplay="auto" + onChange={(e: any) => setPercentage(e.target.value)} + /> +
+ + Additional gas + + + {nativeGasPrice} +
- - - +
+
+
); };