From 59e1f32594de66de1b9b941e00033e9535ec6521 Mon Sep 17 00:00:00 2001 From: Artur Sapek Date: Mon, 16 Sep 2024 12:23:00 -0400 Subject: [PATCH] Polishing routes UI (#2611) * consolidate header; add badges * ETA < 1 minute is green * more polish on routes UI * rm unneeded attr --- wormhole-connect/src/config/routes.ts | 8 +- wormhole-connect/src/icons/CheapestRoute.tsx | 18 +++++ wormhole-connect/src/icons/FastestRoute.tsx | 15 ++++ wormhole-connect/src/theme.ts | 2 +- .../views/v2/Bridge/Routes/SingleRoute.tsx | 81 ++++++++++++------- .../src/views/v2/Bridge/Routes/index.tsx | 56 ++++++++++++- 6 files changed, 144 insertions(+), 36 deletions(-) create mode 100644 wormhole-connect/src/icons/CheapestRoute.tsx create mode 100644 wormhole-connect/src/icons/FastestRoute.tsx diff --git a/wormhole-connect/src/config/routes.ts b/wormhole-connect/src/config/routes.ts index cef527ff4..2c07e81a1 100644 --- a/wormhole-connect/src/config/routes.ts +++ b/wormhole-connect/src/config/routes.ts @@ -112,16 +112,16 @@ export const RoutesConfig: Record = { }, MayanSwapMCTP: { name: 'MayanSwapMCTP', - displayName: 'Mayan Swap (MCTP)', - providedBy: 'Mayan (MCTP)', + displayName: 'Mayan Swap MCTP', + providedBy: 'Mayan MCTP', link: 'https://mayan.finance/', icon: XLabsIcon, pendingMessage: 'Waiting for Wormhole network consensus . . .', }, MayanSwapSWIFT: { name: 'MayanSwapSWIFT', - displayName: 'Mayan Swap (Swift)', - providedBy: 'Mayan (Swift)', + displayName: 'Mayan Swap Swift', + providedBy: 'Mayan Swift', link: 'https://mayan.finance/', icon: XLabsIcon, pendingMessage: 'Waiting for Wormhole network consensus . . .', diff --git a/wormhole-connect/src/icons/CheapestRoute.tsx b/wormhole-connect/src/icons/CheapestRoute.tsx new file mode 100644 index 000000000..859de7812 --- /dev/null +++ b/wormhole-connect/src/icons/CheapestRoute.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { createSvgIcon } from '@mui/material'; + +export default createSvgIcon( + + + + + + , + 'Cheapest', +); diff --git a/wormhole-connect/src/icons/FastestRoute.tsx b/wormhole-connect/src/icons/FastestRoute.tsx new file mode 100644 index 000000000..0cb09e5dc --- /dev/null +++ b/wormhole-connect/src/icons/FastestRoute.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { createSvgIcon } from '@mui/material'; + +export default createSvgIcon( + + + , + 'Fastest', +); diff --git a/wormhole-connect/src/theme.ts b/wormhole-connect/src/theme.ts index 5937d4a3d..53e5d80e2 100644 --- a/wormhole-connect/src/theme.ts +++ b/wormhole-connect/src/theme.ts @@ -201,7 +201,7 @@ export const dark: WormholeConnectTheme = { }, text: { primary: '#ffffff', - secondary: '#667085', + secondary: '#79859e', }, info: { 50: '#97a5b7', diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx index 76518e2c8..16dc4b102 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx @@ -21,6 +21,8 @@ import type { RootState } from 'store'; import { formatBalance } from 'store/transferInput'; import { toFixedDecimals } from 'utils/balance'; import { TokenConfig } from 'config/types'; +import FastestRoute from 'icons/FastestRoute'; +import CheapestRoute from 'icons/CheapestRoute'; const useStyles = makeStyles()((theme: any) => ({ container: { @@ -31,6 +33,20 @@ const useStyles = makeStyles()((theme: any) => ({ width: '100%', maxWidth: '420px', }, + fastestBadge: { + width: '14px', + height: '14px', + position: 'relative', + top: '2px', + fill: theme.palette.primary.main, + }, + cheapestBadge: { + width: '12px', + height: '12px', + position: 'relative', + top: '1px', + fill: theme.palette.primary.main, + }, })); type Props = { @@ -39,6 +55,9 @@ type Props = { error?: string; destinationGasDrop?: number; title?: string; + isFastest?: boolean; + isCheapest?: boolean; + isOnlyChoice?: boolean; onSelect?: (route: string) => void; quote?: routes.Quote; isFetchingQuote: boolean; @@ -118,7 +137,9 @@ const SingleRoute = (props: Props) => { Network cost - {feeValue} + + {feeValue} + ); }, [destToken, isFetchingQuote, quote?.relayFee, tokenPrices]); @@ -165,7 +186,15 @@ const SingleRoute = (props: Props) => { {isFetchingQuote ? ( ) : ( - + {quote?.eta ? millisToHumanString(quote.eta) : 'N/A'} )} @@ -226,20 +255,6 @@ const SingleRoute = (props: Props) => { ); }, [showWarning]); - const isAutomaticRoute = useMemo(() => { - if (!props.route.name) { - return false; - } - - const route = config.routes.get(props.route.name); - - if (!route) { - return false; - } - - return route.AUTOMATIC_DEPOSIT; - }, [props.route.name]); - const providerText = useMemo(() => { if (!sourceToken) { return ''; @@ -274,11 +289,6 @@ const SingleRoute = (props: Props) => { destChain, ]); - const routeTitle = useMemo( - () => (isAutomaticRoute ? 'Automatic route' : 'Manual route'), - [isAutomaticRoute], - ); - const receiveAmount = useMemo(() => { return quote ? amount.whole(quote?.destinationToken.amount) : undefined; }, [quote]); @@ -358,17 +368,27 @@ const SingleRoute = (props: Props) => { return <>; } + const routeCardBadge = useMemo(() => { + if (props.isFastest) { + return ( + <> + + {props.isOnlyChoice ? 'Fast' : 'Fastest'} + + ); + } else if (props.isCheapest && !props.isOnlyChoice) { + return ( + <> + Cheapest + + ); + } else { + return null; + } + }, [props.isFastest, props.isCheapest]); + return (
- - {props.title || routeTitle} - { avatar={} title={routeCardHeader} subheader={routeCardSubHeader} + action={routeCardBadge} /> diff --git a/wormhole-connect/src/views/v2/Bridge/Routes/index.tsx b/wormhole-connect/src/views/v2/Bridge/Routes/index.tsx index 02b5d7547..5e8af2fc5 100644 --- a/wormhole-connect/src/views/v2/Bridge/Routes/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/Routes/index.tsx @@ -4,6 +4,7 @@ import Tooltip from '@mui/material/Tooltip'; import Link from '@mui/material/Link'; import { makeStyles } from 'tss-react/mui'; +import config from 'config'; import { RoutesConfig } from 'config/routes'; import SingleRoute from 'views/v2/Bridge/Routes/SingleRoute'; import AlertBannerV2 from 'components/v2/AlertBanner'; @@ -12,6 +13,7 @@ import type { RootState } from 'store'; import { RouteState } from 'store/transferInput'; import { routes } from '@wormhole-foundation/sdk'; +import { Typography } from '@mui/material'; const useStyles = makeStyles()((theme: any) => ({ connectWallet: { @@ -84,6 +86,45 @@ const Routes = ({ sortedSupportedRoutes, ...props }: Props) => { return selectedRoute ? [selectedRoute] : sortedSupportedRoutes.slice(0, 1); }, [showAll, sortedSupportedRoutes]); + const fastestRoute = useMemo(() => { + return sortedSupportedRoutes.reduce( + (fastest, route) => { + const quote = props.quotes[route.name]; + if (!quote || !quote.success) return fastest; + + if ( + quote.eta !== undefined && + quote.eta < fastest.eta && + quote.eta < 60_000 + ) { + return { name: route.name, eta: quote.eta }; + } else { + return fastest; + } + }, + { name: '', eta: Infinity }, + ); + }, [sortedSupportedRoutes, props.quotes]); + + const cheapestRoute = useMemo(() => { + return sortedSupportedRoutes.reduce( + (cheapest, route) => { + const quote = props.quotes[route.name]; + const rc = config.routes.get(route.name); + // TODO put AUTOMATIC_DEPOSIT into RouteState + if (!quote || !quote.success || !rc.AUTOMATIC_DEPOSIT) return cheapest; + + const amountOut = BigInt(quote.destinationToken.amount.amount); + if (amountOut > cheapest.amountOut) { + return { name: route.name, amountOut }; + } else { + return cheapest; + } + }, + { name: '', amountOut: 0n }, + ); + }, [sortedSupportedRoutes, props.quotes]); + if (walletsConnected && supportedRoutes.length === 0 && Number(amount) > 0) { return ( { return ( <> - {renderRoutes.map(({ name }) => { + + Routes + + {renderRoutes.map(({ name }, index) => { const routeConfig = RoutesConfig[name]; const isSelected = routeConfig.name === props.selectedRoute; const quoteResult = props.quotes[name]; @@ -128,6 +179,9 @@ const Routes = ({ sortedSupportedRoutes, ...props }: Props) => { route={routeConfig} error={quoteError} isSelected={isSelected && !quoteError} + isFastest={name === fastestRoute.name} + isCheapest={name === cheapestRoute.name} + isOnlyChoice={supportedRoutes.length === 1} onSelect={props.onRouteChange} quote={quote} isFetchingQuote={props.isFetchingQuotes}