diff --git a/wormhole-connect/src/components/AlertBanner.tsx b/wormhole-connect/src/components/AlertBanner.tsx index 63e2c13a1..bd03a1926 100644 --- a/wormhole-connect/src/components/AlertBanner.tsx +++ b/wormhole-connect/src/components/AlertBanner.tsx @@ -2,6 +2,7 @@ import { Collapse } from '@mui/material'; import React from 'react'; import { makeStyles } from 'tss-react/mui'; import AlertIcon from 'icons/Alert'; +import InfoIcon from 'icons/Info'; import { OPACITY, joinClass } from 'utils/style'; const useStyles = makeStyles()((theme: any) => ({ @@ -21,6 +22,9 @@ const useStyles = makeStyles()((theme: any) => ({ warning: { backgroundColor: theme.palette.warning[500] + OPACITY[25], }, + info: { + backgroundColor: theme.palette.info[500] + OPACITY[25], + }, })); type Props = { @@ -28,6 +32,7 @@ type Props = { content: React.ReactNode | undefined; warning?: boolean; error?: boolean; + info?: boolean; margin?: string; testId?: string; }; @@ -42,11 +47,12 @@ function AlertBanner(props: Props) { classes.base, !!props.warning && classes.warning, !!props.error && classes.error, + !!props.info && classes.info, ])} style={{ margin: props.margin || 0 }} data-testid={props.testId} > - + {props.info ? : } {props.content} diff --git a/wormhole-connect/src/store/transferInput.ts b/wormhole-connect/src/store/transferInput.ts index 1d2d61c56..a8ca645ee 100644 --- a/wormhole-connect/src/store/transferInput.ts +++ b/wormhole-connect/src/store/transferInput.ts @@ -132,6 +132,7 @@ export interface TransferInputState { supportedDestTokens: TokenConfig[]; manualAddressTarget: boolean; showManualAddressInput: boolean; + resolvingRoutes: boolean; } // This is a function because config might have changed since we last cleared this store @@ -174,6 +175,7 @@ function getInitialState(): TransferInputState { supportedDestTokens: [], manualAddressTarget: false, showManualAddressInput: config.manualTargetAddress || false, + resolvingRoutes: false, }; } @@ -297,6 +299,12 @@ export const transferInputSlice = createSlice({ name: 'transfer', initialState: getInitialState(), reducers: { + setResolvingRoutes( + state: TransferInputState, + { payload }: PayloadAction, + ) { + state.resolvingRoutes = payload; + }, // validations setValidations: ( state: TransferInputState, @@ -574,6 +582,7 @@ export const { swapChains, setManualAddressTarget, showManualAddressInput, + setResolvingRoutes, } = transferInputSlice.actions; export default transferInputSlice.reducer; diff --git a/wormhole-connect/src/utils/transferValidation.ts b/wormhole-connect/src/utils/transferValidation.ts index d3e6d75e9..192b55b1d 100644 --- a/wormhole-connect/src/utils/transferValidation.ts +++ b/wormhole-connect/src/utils/transferValidation.ts @@ -183,7 +183,9 @@ export const validateToNativeAmt = ( export const validateRoute = ( route: Route | undefined, availableRoutes: string[] | undefined, + resolvingRoutes = false, ): ValidationErr => { + if (resolvingRoutes) return ''; if (!route || !availableRoutes || !availableRoutes.includes(route)) { return 'No bridge or swap route available for selected tokens'; } @@ -295,6 +297,7 @@ export const validateAll = async ( routeStates, receiveAmount, manualAddressTarget, + resolvingRoutes, } = transferData; const { maxSwapAmt, toNativeToken } = relayData; const { sending, receiving } = walletData; @@ -326,7 +329,7 @@ export const validateAll = async ( maxSendAmount, isCctpTx, ), - route: validateRoute(route, availableRoutes), + route: validateRoute(route, availableRoutes, resolvingRoutes), toNativeToken: '', foreignAsset: validateForeignAsset(foreignAsset), relayerFee: '', diff --git a/wormhole-connect/src/views/Bridge/Bridge.tsx b/wormhole-connect/src/views/Bridge/Bridge.tsx index 909b68b67..896e1b6a1 100644 --- a/wormhole-connect/src/views/Bridge/Bridge.tsx +++ b/wormhole-connect/src/views/Bridge/Bridge.tsx @@ -53,6 +53,7 @@ import { isNttRoute } from 'routes/utils'; import { useConnectToLastUsedWallet } from 'utils/wallet'; import { USDTBridge } from 'routes/porticoBridge/usdtBridge'; import { isAutomatic } from 'utils/route'; +import AlertBanner from 'components/AlertBanner'; const useStyles = makeStyles()((_theme) => ({ spacer: { @@ -105,6 +106,7 @@ function Bridge() { isTransactionInProgress, amount, manualAddressTarget, + resolvingRoutes, }: TransferInputState = useSelector( (state: RootState) => state.transferInput, ); @@ -351,6 +353,13 @@ function Bridge() { + { let isActive = true; - if (!fromChain || !toChain || !token || !destToken) return; - const getAvailable = async () => { + const getAvailable = async (fromChain: ChainName, toChain: ChainName) => { const routes: RouteState[] = []; for (const value of config.routes) { const r = value as Route; - const available = await RouteOperator.isRouteAvailable( - r, - token, - destToken, - debouncedAmount, - fromChain, - toChain, - ); - - const supported = await RouteOperator.isRouteSupported( - r, - token, - destToken, - debouncedAmount, - fromChain, - toChain, - ); - + // don't await, we want to resolve all routes in parallel + const [available, supported] = await Promise.all([ + RouteOperator.isRouteAvailable( + r, + token, + destToken, + debouncedAmount, + fromChain, + toChain, + ), + RouteOperator.isRouteSupported( + r, + token, + destToken, + debouncedAmount, + fromChain, + toChain, + ), + ]); routes.push({ name: r, supported, availability: available }); } if (isActive) { dispatch(setRoutes(routes)); } + dispatch(setResolvingRoutes(false)); }; - getAvailable(); + + if (!fromChain || !toChain || !token || !destToken) { + dispatch(setRoutes([])); // reset routes if we don't have all the info + } else { + dispatch(setRoutes([])); // defensive remove routes until we have all the info to decide if the routes are availables between renders + dispatch(setResolvingRoutes(true)); + getAvailable(fromChain, toChain); + } return () => { isActive = false;