Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor AmountInput and Routes error handling. #2608

109 changes: 109 additions & 0 deletions wormhole-connect/src/hooks/useAmountValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { amount as sdkAmount, routes } from '@wormhole-foundation/sdk';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { QuoteResult } from 'routes/operator';
import { RootState } from 'store';
import { RouteState } from 'store/transferInput';

type HookReturn = {
error?: string;
warning?: string;
};

type Props = {
balance?: string | null;
routes: RouteState[];
quotesMap: Record<string, QuoteResult | undefined>;
tokenSymbol: string;
isLoading: boolean;
};

export const useAmountValidation = (props: Props): HookReturn => {
const { amount } = useSelector((state: RootState) => state.transferInput);

// Min amount available
const minAmount = useMemo(
() =>
Object.values(props.quotesMap).reduce((minAmount, quoteResult) => {
if (quoteResult?.success) {
return minAmount;
}

const minAmountError = quoteResult?.error as routes.MinAmountError;

if (!minAmountError?.min) {
artursapek marked this conversation as resolved.
Show resolved Hide resolved
return minAmount;
}

if (!minAmount) {
return minAmountError.min;
}

const minAmountNum = parseFloat(minAmountError.min.amount);
const existingMin = parseFloat(minAmount.amount);
artursapek marked this conversation as resolved.
Show resolved Hide resolved
if (minAmountNum < existingMin) {
return minAmountError.min;
} else {
return minAmount;
}
}, undefined as sdkAmount.Amount | undefined),
[props.quotesMap],
);

const allRoutesFailed = useMemo(
() => props.routes.every((route) => !props.quotesMap[route.name]?.success),
[props.routes, props.quotesMap],
);

// Don't show errors when no amount is set or it's loading
if (amount === '' || props.isLoading) {
return {};
}

const numAmount = Number.parseFloat(amount);
// Input errors
if (Number.isNaN(numAmount)) {
return {
error: 'Amount must be a number.',
};
}
if (numAmount <= 0) {
artursapek marked this conversation as resolved.
Show resolved Hide resolved
return {
error: 'Amount must be greater than 0.',
};
}

// Balance errors
if (props.balance) {
const balanceNum = Number.parseFloat(props.balance.replace(',', ''));
if (numAmount > balanceNum) {
return {
error: 'Amount exceeds available balance.',
};
}
}

// All quotes fail.
if (allRoutesFailed) {
if (minAmount) {
const amountDisplay = sdkAmount.display(minAmount);
return {
error: `Amount too small (min ~${amountDisplay} ${props.tokenSymbol})`,
};
} else {
return {
error: 'No routes found for this transaction.',
};
}
}

// MinQuote warnings information
if (minAmount) {
const amountDisplay = sdkAmount.display(minAmount);
return {
warning: `More routes available for amounts exceeding ${amountDisplay} ${props.tokenSymbol}`,
};
}

return {};
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { RootState } from 'store';
import config from 'config';
import { getTokenDetails } from 'telemetry';

const useAvailableRoutes = (): void => {
const useFetchSupportedRoutes = (): void => {
const dispatch = useDispatch();

const { token, destToken, fromChain, toChain, amount } = useSelector(
Expand Down Expand Up @@ -92,4 +92,4 @@ const useAvailableRoutes = (): void => {
]);
};

export default useAvailableRoutes;
export default useFetchSupportedRoutes;
4 changes: 1 addition & 3 deletions wormhole-connect/src/hooks/useRoutesQuotesBulk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
circle,
amount,
} from '@wormhole-foundation/sdk';
import { QuoteParams } from 'routes/operator';
import { QuoteParams, QuoteResult } from 'routes/operator';
import { calculateUSDPriceRaw } from 'utils';

import config from 'config';
Expand All @@ -23,8 +23,6 @@ type Params = {
nativeGas: number;
};

type QuoteResult = routes.QuoteResult<routes.Options>;

type HookReturn = {
quotesMap: Record<string, QuoteResult | undefined>;
isFetching: boolean;
Expand Down
124 changes: 124 additions & 0 deletions wormhole-connect/src/hooks/useSortedRoutesWithQuotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { RootState } from 'store';
import { routes, amount as sdkAmount } from '@wormhole-foundation/sdk';
import useRoutesQuotesBulk from 'hooks/useRoutesQuotesBulk';
import config from 'config';
import { RouteState } from 'store/transferInput';

type Quote = routes.Quote<
routes.Options,
routes.ValidatedTransferParams<routes.Options>
>;

export type RouteWithQuote = {
route: RouteState;
quote: Quote;
};

type HookReturn = {
allSupportedRoutes: RouteState[];
sortedRoutes: RouteState[];
sortedRoutesWithQuotes: RouteWithQuote[];
quotesMap: ReturnType<typeof useRoutesQuotesBulk>['quotesMap'];
isFetchingQuotes: boolean;
};

export const useSortedRoutesWithQuotes = (): HookReturn => {
const { amount, routeStates, fromChain, token, toChain, destToken } =
useSelector((state: RootState) => state.transferInput);
const { toNativeToken } = useSelector((state: RootState) => state.relay);

const supportedRoutes = useMemo(
() => (routeStates || []).filter((rs) => rs.supported),
[routeStates],
);

const supportedRoutesNames = useMemo(
() => supportedRoutes.map((r) => r.name),
[supportedRoutes],
);

const { quotesMap, isFetching } = useRoutesQuotesBulk(supportedRoutesNames, {
amount,
sourceChain: fromChain,
sourceToken: token,
destChain: toChain,
destToken,
nativeGas: toNativeToken,
});

const routesWithQuotes = useMemo(() => {
return supportedRoutes
.map((route) => {
const quote = quotesMap[route.name];
if (quote?.success) {
return {
route,
quote,
};
} else {
return undefined;
}
})
.filter(Boolean) as RouteWithQuote[];
artursapek marked this conversation as resolved.
Show resolved Hide resolved
// Safe to cast, as falsy values are filtered
}, [supportedRoutes, quotesMap]);

// Only routes with quotes are sorted.
const sortedRoutesWithQuotes = useMemo(() => {
return [...routesWithQuotes].sort((routeA, routeB) => {
const routeConfigA = config.routes.get(routeA.route.name);
const routeConfigB = config.routes.get(routeB.route.name);

// 1. Prioritize automatic routes
if (routeConfigA.AUTOMATIC_DEPOSIT && !routeConfigB.AUTOMATIC_DEPOSIT) {
return -1;
} else if (
!routeConfigA.AUTOMATIC_DEPOSIT &&
routeConfigB.AUTOMATIC_DEPOSIT
) {
return 1;
}

// 2. Prioritize estimated time
if (routeA.quote.eta && routeB.quote.eta) {
if (routeA.quote.eta > routeB.quote.eta) {
return 1;
} else if (routeA.quote.eta < routeB.quote.eta) {
return -1;
}
}

// 3. Compare relay fees
if (routeA.quote.relayFee && routeB.quote.relayFee) {
artursapek marked this conversation as resolved.
Show resolved Hide resolved
const relayFeeA = sdkAmount.whole(routeA.quote.relayFee.amount);
const relayFeeB = sdkAmount.whole(routeB.quote.relayFee.amount);
if (relayFeeA > relayFeeB) {
return 1;
} else if (relayFeeA < relayFeeB) {
return -1;
}
}

// Don't swap when routes match by all criteria or don't have quotas
return 0;
});
}, [routesWithQuotes]);

const sortedRoutes = useMemo(
() => sortedRoutesWithQuotes.map((r) => r.route),
[sortedRoutesWithQuotes],
);

return useMemo(
() => ({
allSupportedRoutes: supportedRoutes,
sortedRoutes,
sortedRoutesWithQuotes,
quotesMap,
isFetchingQuotes: isFetching,
}),
[supportedRoutes, sortedRoutesWithQuotes, quotesMap, isFetching],
);
};
85 changes: 0 additions & 85 deletions wormhole-connect/src/hooks/useSortedSupportedRoutes.ts

This file was deleted.

4 changes: 2 additions & 2 deletions wormhole-connect/src/views/Bridge/RouteOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import ArrowRightIcon from 'icons/ArrowRight';
import Options from 'components/Options';
import Price from 'components/Price';
import { finality, Chain } from '@wormhole-foundation/sdk';
import useSupportedRoutes from 'hooks/useSupportedRoutes';
import useFetchSupportedRoutes from 'hooks/useFetchSupportedRoutes';

const useStyles = makeStyles()((theme: any) => ({
link: {
Expand Down Expand Up @@ -353,7 +353,7 @@ function RouteOptions() {
(state: RootState) => state.transferInput,
);

useSupportedRoutes();
useFetchSupportedRoutes();

const onSelect = useCallback(
(value: string) => {
Expand Down
Loading
Loading