Skip to content

Commit

Permalink
Refresh quotes every 20 seconds (#2650)
Browse files Browse the repository at this point in the history
* refresh quotes every 20 seconds

* improve UI during loading states

* handle fetching state better in ReviewTransaction

* fix comment

* singular

* don't refresh quotes while user is initiating tx

* add dependency
  • Loading branch information
artursapek authored Sep 18, 2024
1 parent 8e6e388 commit 78a5f09
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 102 deletions.
44 changes: 37 additions & 7 deletions wormhole-connect/src/hooks/useRoutesQuotesBulk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,27 @@ type HookReturn = {
isFetching: boolean;
};

const QUOTE_REFRESH_INTERVAL = 20_000;

const MAYAN_BETA_LIMIT = 10_000; // USD
const MAYAN_BETA_PROTOCOLS = ['MCTP', 'SWIFT'];

const useRoutesQuotesBulk = (routes: string[], params: Params): HookReturn => {
const [nonce, setNonce] = useState(new Date().valueOf());
const [refreshTimeout, setRefreshTimeout] = useState<null | ReturnType<
typeof setTimeout
>>(null);

const [isFetching, setIsFetching] = useState(false);
const [quotes, setQuotes] = useState<QuoteResult[]>([]);

// TODO temporary
// Calculate USD amount for temporary $1000 Mayan limit
const tokenConfig = config.tokens[params.sourceToken];
const { usdPrices } = useSelector((state: RootState) => state.tokenPrices);
const { isTransactionInProgress } = useSelector(
(state: RootState) => state.transferInput,
);
const usdAmount = calculateUSDPriceRaw(
params.amount,
usdPrices.data,
Expand All @@ -60,16 +70,34 @@ const useRoutesQuotesBulk = (routes: string[], params: Params): HookReturn => {
// Forcing TS to infer that fields are non-optional
const rParams = params as Required<QuoteParams>;

setIsFetching(true);
config.routes.getQuotes(routes, rParams).then((quoteResults) => {
if (!unmounted) {
setQuotes(quoteResults);
setIsFetching(false);
}
});
const onComplete = () => {
// Refresh quotes in 20 seconds
const refreshTimeout = setTimeout(
() => setNonce(new Date().valueOf()),
QUOTE_REFRESH_INTERVAL,
);
setRefreshTimeout(refreshTimeout);
};

if (isTransactionInProgress) {
// Don't fetch new quotes if the user has committed to one and has initiated a transaction
onComplete();
} else {
setIsFetching(true);
config.routes.getQuotes(routes, rParams).then((quoteResults) => {
if (!unmounted) {
setQuotes(quoteResults);
setIsFetching(false);
onComplete();
}
});
}

return () => {
unmounted = true;
if (refreshTimeout) {
clearTimeout(refreshTimeout);
}
};
}, [
routes.join(),
Expand All @@ -79,6 +107,8 @@ const useRoutesQuotesBulk = (routes: string[], params: Params): HookReturn => {
params.destToken,
params.amount,
params.nativeGas,
nonce,
isTransactionInProgress,
]);

const quotesMap = useMemo(
Expand Down
29 changes: 15 additions & 14 deletions wormhole-connect/src/views/v2/Bridge/ReviewTransaction/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import GasSlider from 'views/v2/Bridge/ReviewTransaction/GasSlider';
import SingleRoute from 'views/v2/Bridge/Routes/SingleRoute';

import type { RootState } from 'store';
import useRoutesQuotesBulk from 'hooks/useRoutesQuotesBulk';
import { RelayerFee } from 'store/relay';

import { amount as sdkAmount } from '@wormhole-foundation/sdk';
Expand All @@ -61,6 +60,8 @@ const useStyles = makeStyles()((theme) => ({

type Props = {
onClose: () => void;
quotes: any;
isFetchingQuotes: boolean;
};

const ReviewTransaction = (props: Props) => {
Expand Down Expand Up @@ -103,16 +104,7 @@ const ReviewTransaction = (props: Props) => {
isTransactionInProgress,
});

const routes = useMemo(() => (route ? [route] : []), []);
const { quotesMap, isFetching } = useRoutesQuotesBulk(routes, {
amount,
sourceChain,
sourceToken,
destChain,
destToken,
nativeGas: toNativeToken,
});
const quoteResult = quotesMap[route ?? ''];
const quoteResult = props.quotes[route ?? ''];
const quote = quoteResult?.success ? quoteResult : undefined;

const receiveNativeAmount = quote?.destinationNativeGas
Expand Down Expand Up @@ -324,7 +316,7 @@ const ReviewTransaction = (props: Props) => {

return (
<Button
disabled={isFetching || isTransactionInProgress}
disabled={props.isFetchingQuotes || isTransactionInProgress}
variant="primary"
className={classes.confirmTransaction}
onClick={() => send()}
Expand All @@ -339,6 +331,16 @@ const ReviewTransaction = (props: Props) => {
<CircularProgress color="secondary" size={16} />
{mobile ? 'Preparing' : 'Preparing transaction'}
</Typography>
) : !isTransactionInProgress && props.isFetchingQuotes ? (
<Typography
display="flex"
alignItems="center"
gap={1}
textTransform="none"
>
<CircularProgress color="secondary" size={16} />
{mobile ? 'Refreshing' : 'Refreshing quote'}
</Typography>
) : (
<Typography textTransform="none">
{mobile ? 'Confirm' : 'Confirm transaction'}
Expand All @@ -347,7 +349,7 @@ const ReviewTransaction = (props: Props) => {
</Button>
);
}, [
isFetching,
props.isFetchingQuotes,
isTransactionInProgress,
sourceChain,
sourceToken,
Expand Down Expand Up @@ -375,7 +377,6 @@ const ReviewTransaction = (props: Props) => {
destinationGasDrop={receiveNativeAmount}
title="You will receive"
quote={quote}
isFetchingQuote={isFetching}
/>
<Collapse in={showGasSlider}>
<GasSlider
Expand Down
55 changes: 18 additions & 37 deletions wormhole-connect/src/views/v2/Bridge/Routes/SingleRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { CircularProgress, useTheme } from '@mui/material';
import { useTheme } from '@mui/material';
import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import CardContent from '@mui/material/CardContent';
Expand Down Expand Up @@ -62,7 +62,6 @@ type Props = {
isOnlyChoice?: boolean;
onSelect?: (route: string) => void;
quote?: routes.Quote<routes.Options>;
isFetchingQuote: boolean;
};

const SingleRoute = (props: Props) => {
Expand All @@ -82,7 +81,7 @@ const SingleRoute = (props: Props) => {
);

const { name } = props.route;
const { quote, isFetchingQuote } = props;
const { quote } = props;

const destTokenConfig = useMemo(
() => config.tokens[destToken] as TokenConfig | undefined,
Expand Down Expand Up @@ -121,9 +120,7 @@ const SingleRoute = (props: Props) => {
return <></>;
}

let feeValue = isFetchingQuote ? (
<CircularProgress size={14} />
) : (
let feeValue = (
<Typography fontSize={14}>{`${toFixedDecimals(relayFee.toString(), 4)} ${
feeTokenConfig.symbol
} (${feePrice})`}</Typography>
Expand All @@ -144,7 +141,7 @@ const SingleRoute = (props: Props) => {
</Typography>
</Stack>
);
}, [destToken, isFetchingQuote, quote?.relayFee, tokenPrices]);
}, [destToken, quote?.relayFee, tokenPrices]);

const destinationGas = useMemo(() => {
if (!destChain || !props.destinationGasDrop) {
Expand All @@ -169,14 +166,10 @@ const SingleRoute = (props: Props) => {
<Typography color={theme.palette.text.secondary} fontSize={14}>
Gas top up
</Typography>
{isFetchingQuote ? (
<CircularProgress size={14} />
) : (
<Typography fontSize={14}>{gasTokenPrice}</Typography>
)}
<Typography fontSize={14}>{gasTokenPrice}</Typography>
</Stack>
);
}, [destChain, isFetchingQuote, props.destinationGasDrop]);
}, [destChain, props.destinationGasDrop]);

const timeToDestination = useMemo(
() => (
Expand All @@ -185,24 +178,20 @@ const SingleRoute = (props: Props) => {
Time to destination
</Typography>

{isFetchingQuote ? (
<CircularProgress size={14} />
) : (
<Typography
fontSize={14}
sx={{
color:
quote?.eta && quote.eta < 60 * 1000
? theme.palette.success.main
: theme.palette.text.secondary,
}}
>
{quote?.eta ? millisToHumanString(quote.eta) : 'N/A'}
</Typography>
)}
<Typography
fontSize={14}
sx={{
color:
quote?.eta && quote.eta < 60 * 1000
? theme.palette.success.main
: theme.palette.text.secondary,
}}
>
{quote?.eta ? millisToHumanString(quote.eta) : 'N/A'}
</Typography>
</>
),
[quote?.eta, isFetchingQuote],
[quote?.eta],
);

const isManual = useMemo(() => {
Expand Down Expand Up @@ -310,10 +299,6 @@ const SingleRoute = (props: Props) => {
return <Typography color="error">Route is unavailable</Typography>;
}

if (props.isFetchingQuote) {
return <CircularProgress size={18} />;
}

if (receiveAmount === undefined || !destTokenConfig) {
return null;
}
Expand All @@ -330,10 +315,6 @@ const SingleRoute = (props: Props) => {
return null;
}

if (props.isFetchingQuote) {
return <CircularProgress size={18} />;
}

if (receiveAmount === undefined) {
return null;
}
Expand Down
89 changes: 48 additions & 41 deletions wormhole-connect/src/views/v2/Bridge/Routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import AlertBannerV2 from 'components/v2/AlertBanner';
import type { RootState } from 'store';
import { RouteState } from 'store/transferInput';
import { routes } from '@wormhole-foundation/sdk';
import { CircularProgress } from '@mui/material';
import { Box, CircularProgress, Skeleton } from '@mui/material';

const useStyles = makeStyles()((theme: any) => ({
connectWallet: {
Expand Down Expand Up @@ -144,10 +144,6 @@ const Routes = ({ ...props }: Props) => {
return null;
}

if (props.isLoading) {
return <CircularProgress />;
}

if (walletsConnected && !(Number(amount) > 0)) {
return (
<Tooltip title="Please enter the amount to view available routes">
Expand All @@ -160,42 +156,53 @@ const Routes = ({ ...props }: Props) => {

return (
<>
<Typography
fontSize={16}
paddingBottom={0}
marginTop="8px"
marginBottom={0}
width="100%"
textAlign="left"
>
Routes
</Typography>
{renderRoutes.map(({ name }, index) => {
const routeConfig = RoutesConfig[name];
const isSelected = routeConfig.name === props.selectedRoute;
const quoteResult = props.quotes[name];
const quote = quoteResult?.success ? quoteResult : undefined;
// Default message added as precaution, as 'Error' type cannot be trusted
const quoteError =
quoteResult?.success === false
? quoteResult?.error?.message ??
`Error while getting a quote for ${name}.`
: undefined;
return (
<SingleRoute
key={name}
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.isLoading}
/>
);
})}
<Box sx={{ display: 'flex', width: '100%' }}>
<Typography
align="left"
fontSize={16}
paddingBottom={0}
marginTop="8px"
marginBottom={0}
width="100%"
textAlign="left"
>
Routes
</Typography>
{props.isLoading ? (
<CircularProgress sx={{ 'align-self': 'flex-end' }} size={20} />
) : null}
</Box>

{props.isLoading && renderRoutes.length === 0 ? (
<Skeleton variant="rounded" height={153} width="100%" />
) : (
renderRoutes.map(({ name }, index) => {
const routeConfig = RoutesConfig[name];
const isSelected = routeConfig.name === props.selectedRoute;
const quoteResult = props.quotes[name];
const quote = quoteResult?.success ? quoteResult : undefined;
// Default message added as precaution, as 'Error' type cannot be trusted
const quoteError =
quoteResult?.success === false
? quoteResult?.error?.message ??
`Error while getting a quote for ${name}.`
: undefined;
return (
<SingleRoute
key={name}
route={routeConfig}
error={quoteError}
isSelected={isSelected && !quoteError}
isFastest={name === fastestRoute.name}
isCheapest={name === cheapestRoute.name}
isOnlyChoice={supportedRoutes.length === 1}
onSelect={props.onRouteChange}
quote={quote}
/>
);
})
)}

{props.routes.length > 1 && (
<Link
onClick={() => setShowAll((prev) => !prev)}
Expand Down
Loading

0 comments on commit 78a5f09

Please sign in to comment.