diff --git a/libs/oeth/swap/src/actions/swapCurve/index.ts b/libs/oeth/swap/src/actions/swapCurve/index.ts
index 9599b55fb..798ade3ce 100644
--- a/libs/oeth/swap/src/actions/swapCurve/index.ts
+++ b/libs/oeth/swap/src/actions/swapCurve/index.ts
@@ -116,7 +116,7 @@ const allowance: Allowance = async ({ tokenIn, tokenOut, curve }) => {
return 0n;
}
- if (isNilOrEmpty(tokenIn.address) || isNilOrEmpty(tokenOut.address)) {
+ if (isNilOrEmpty(tokenIn.address)) {
return maxUint256;
}
diff --git a/libs/oeth/swap/src/components/BestRoutes.tsx b/libs/oeth/swap/src/components/BestRoutes.tsx
index 7521661fb..8de1bc56e 100644
--- a/libs/oeth/swap/src/components/BestRoutes.tsx
+++ b/libs/oeth/swap/src/components/BestRoutes.tsx
@@ -1,33 +1,33 @@
-import { Box } from '@mui/material';
+import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
import { useHandleSelectSwapRoute } from '../hooks';
import { useSwapState } from '../state';
import { routeEq } from '../utils';
import { SwapRouteCard } from './SwapRouteCard';
-import type { BoxProps } from '@mui/material';
+import type { Grid2Props } from '@mui/material';
-export function BestRoutes(props: BoxProps) {
- const [{ swapRoutes, selectedSwapRoute }] = useSwapState();
+export type BestRoutesProps = { isLoading: boolean } & Grid2Props;
+
+export function BestRoutes(props: Grid2Props) {
+ const [{ swapRoutes, selectedSwapRoute, isSwapRoutesLoading }] =
+ useSwapState();
const handleSelectSwapRoute = useHandleSelectSwapRoute();
return (
-
+
{swapRoutes.slice(0, 2).map((route, index) => (
-
+
+
+
))}
-
+
);
}
diff --git a/libs/oeth/swap/src/components/SwapRoute.tsx b/libs/oeth/swap/src/components/SwapRoute.tsx
index 1268210c2..2894f5be9 100644
--- a/libs/oeth/swap/src/components/SwapRoute.tsx
+++ b/libs/oeth/swap/src/components/SwapRoute.tsx
@@ -1,4 +1,4 @@
-import { Skeleton, Stack, Typography } from '@mui/material';
+import { Collapse, Skeleton, Stack, Typography } from '@mui/material';
import { Card, cardStyles } from '@origin/shared/components';
import { useIntl } from 'react-intl';
@@ -9,31 +9,6 @@ import { SwapRouteAccordion } from './SwapRouteAccordion';
import type { CardProps } from '@mui/material';
-import type { EstimatedSwapRoute } from '../types';
-
-interface Swap {
- type: 'swap';
-}
-export interface Redeem {
- type: 'redeem';
- tokenAbbreviation: string;
- waitTime: string;
-}
-
-export type Route = {
- name: string;
- icon: string | string[];
- quantity: number;
- value: number;
- rate: number;
- transactionCost: number;
-} & (Swap | Redeem);
-
-export type SwapRouteProps = {
- isLoading: boolean;
- routes: EstimatedSwapRoute[];
-};
-
export function SwapRoute(props: Omit) {
const intl = useIntl();
const [{ swapRoutes, isSwapRoutesLoading }] = useSwapState();
@@ -52,27 +27,26 @@ export function SwapRoute(props: Omit) {
}}
title={
isSwapRoutesLoading ? (
- theme.typography.pxToRem(12),
- display: 'flex',
- alignItems: 'center',
- }}
+ ({ color: theme.palette.primary.contrastText })}
>
theme.palette.primary.contrastText,
}}
/>
- {intl.formatMessage({
- defaultMessage: 'Finding the best route...',
- })}
-
+
+ {intl.formatMessage({
+ defaultMessage: 'Finding the best route...',
+ })}
+
+
) : (
) {
}),
}}
>
- {hasContent ? (
- <>
-
-
- >
- ) : undefined}
+
+
+ {swapRoutes.length > 2 && }
+
);
}
diff --git a/libs/oeth/swap/src/components/SwapRouteCard.tsx b/libs/oeth/swap/src/components/SwapRouteCard.tsx
index ba2c3c5ba..d48213bd8 100644
--- a/libs/oeth/swap/src/components/SwapRouteCard.tsx
+++ b/libs/oeth/swap/src/components/SwapRouteCard.tsx
@@ -1,26 +1,44 @@
-import { alpha, Box, Card, CardHeader, Stack, Typography } from '@mui/material';
+import {
+ alpha,
+ Box,
+ Card,
+ CardHeader,
+ Skeleton,
+ Stack,
+ Typography,
+} from '@mui/material';
+import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
import { tokens } from '@origin/shared/contracts';
import { usePrices } from '@origin/shared/providers';
-import { currencyFormat, quantityFormat } from '@origin/shared/utils';
+import {
+ currencyFormat,
+ formatAmount,
+ quantityFormat,
+} from '@origin/shared/utils';
import { useIntl } from 'react-intl';
import { formatUnits } from 'viem';
import { routeActionLabel, routeActionLogos } from '../constants';
+import type { CardProps } from '@mui/material';
+
import type { EstimatedSwapRoute } from '../types';
export type SwapRouteCardProps = {
isSelected: boolean;
isBest: boolean;
+ isLoading: boolean;
onSelect: (route: EstimatedSwapRoute) => void;
route: EstimatedSwapRoute;
-};
+} & Omit;
export function SwapRouteCard({
isSelected,
isBest,
+ isLoading,
onSelect,
route,
+ ...rest
}: SwapRouteCardProps) {
const intl = useIntl();
const { data: prices } = usePrices();
@@ -35,6 +53,7 @@ export function SwapRouteCard({
return (
`1px solid ${theme.palette.grey[800]}`,
borderRadius: 1,
+ height: 1,
...(isSelected
? {
background: `linear-gradient(var(--mui-palette-grey-800), var(--mui-palette-grey-800)) padding-box,
@@ -64,6 +84,7 @@ export function SwapRouteCard({
)} 100%) border-box;`,
},
}),
+ ...rest?.sx,
}}
role="button"
onClick={() => onSelect(route)}
@@ -71,75 +92,58 @@ export function SwapRouteCard({
-
-
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
- {intl.formatNumber(estimatedAmount, quantityFormat)}
-
- ({intl.formatNumber(convertedAmount, currencyFormat)})
-
+ {isLoading ? (
+
+ ) : (
+ formatAmount(route.estimatedAmount, route.tokenOut.decimals)
+ )}
+
+
+
+ {isLoading ? (
+
+ ) : (
+ `(${intl.formatNumber(convertedAmount, currencyFormat)})`
+ )}
+
+
- {isBest ? (
- theme.shape.borderRadius,
- background: (theme) => theme.palette.background.gradient1,
- color: 'primary.contrastText',
- fontSize: (theme) => theme.typography.pxToRem(12),
- top: (theme) => theme.spacing(-3),
- right: (theme) => theme.spacing(-2),
- paddingInline: 1,
- }}
- >
- {intl.formatMessage({ defaultMessage: 'Best' })}
-
- ) : undefined}
-
-
-
- ({intl.formatNumber(estimatedAmount, quantityFormat)})
-
- >
+ {isBest && (
+ theme.shape.borderRadius,
+ background: (theme) => theme.palette.background.gradient1,
+ color: 'primary.contrastText',
+ fontSize: (theme) => theme.typography.pxToRem(12),
+ top: (theme) => theme.spacing(-3),
+ right: (theme) => theme.spacing(-2),
+ paddingInline: 1,
+ }}
+ >
+ {intl.formatMessage({ defaultMessage: 'Best' })}
+
+ )}
+
}
>
@@ -148,65 +152,47 @@ export function SwapRouteCard({
variant="body2"
sx={{ marginBlock: { xs: 1.5, md: 1 } }}
>
- {intl.formatMessage(routeActionLabel[route.action])}
+ {isLoading ? (
+
+ ) : (
+ intl.formatMessage(routeActionLabel[route.action])
+ )}
- {intl.formatMessage({ defaultMessage: 'Rate:' })}
-
- 1:{route.rate}
-
+
+ {intl.formatMessage({ defaultMessage: 'Rate:' })}
+
+
+ {isLoading ? (
+
+ ) : (
+ `1:${intl.formatNumber(route.rate, quantityFormat)}`
+ )}
+
-
- {intl.formatMessage({
- defaultMessage: 'Gas:',
- })}
-
-
- ~{intl.formatNumber(gas, currencyFormat)}
-
+
+ {intl.formatMessage({ defaultMessage: 'Gas:' })}
+
+
+ {isLoading ? (
+
+ ) : (
+ `~${intl.formatNumber(gas, currencyFormat)}`
+ )}
+
- {/*route.type === 'redeem' ? (
-
-
- {intl.formatMessage({
- defaultMessage: 'Wait time:',
- })}
-
-
- ~{route.waitTime}
-
-
- ) : undefined */}
);
diff --git a/libs/oeth/swap/src/components/TokenSelectModal.tsx b/libs/oeth/swap/src/components/TokenSelectModal.tsx
index dd454311c..fee0e5feb 100644
--- a/libs/oeth/swap/src/components/TokenSelectModal.tsx
+++ b/libs/oeth/swap/src/components/TokenSelectModal.tsx
@@ -142,7 +142,7 @@ function TokenListItem({ token, ...rest }: TokenListItemProps) {
{isBalanceLoading ? (
) : (
- formatAmount(balance.value, balance.decimals)
+ formatAmount(balance?.value, balance?.decimals)
)}
diff --git a/libs/oeth/swap/src/hooks.ts b/libs/oeth/swap/src/hooks.ts
index 3f4c514e0..62cbcd889 100644
--- a/libs/oeth/swap/src/hooks.ts
+++ b/libs/oeth/swap/src/hooks.ts
@@ -22,8 +22,6 @@ export const useHandleAmountInChange = () => {
setSwapState(
produce((state) => {
state.amountIn = amount;
- state.isAmountOutLoading = amount !== 0n;
- state.isPriceOutLoading = amount !== 0n;
state.isSwapRoutesLoading = amount !== 0n;
}),
);
@@ -173,13 +171,19 @@ export const useHandleApprove = () => {
const curve = useCurve();
const queryClient = useQueryClient();
const pushNotification = usePushNotification();
- const [{ amountIn, selectedSwapRoute, tokenIn, tokenOut }] = useSwapState();
+ const [{ amountIn, selectedSwapRoute, tokenIn, tokenOut }, setSwapState] =
+ useSwapState();
return useCallback(async () => {
if (isNilOrEmpty(selectedSwapRoute)) {
return;
}
+ setSwapState(
+ produce((draft) => {
+ draft.isApprovalLoading = true;
+ }),
+ );
await swapActions[selectedSwapRoute.action].approve({
tokenIn,
tokenOut,
@@ -198,18 +202,33 @@ export const useHandleApprove = () => {
title: intl.formatMessage({ defaultMessage: 'Approval complete' }),
severity: 'success',
});
+ setSwapState(
+ produce((draft) => {
+ draft.isApprovalLoading = false;
+ }),
+ );
},
onError: () => {
pushNotification({
title: intl.formatMessage({ defaultMessage: 'Approval failed' }),
severity: 'error',
});
+ setSwapState(
+ produce((draft) => {
+ draft.isApprovalLoading = false;
+ }),
+ );
},
onReject: () => {
pushNotification({
title: intl.formatMessage({ defaultMessage: 'Approval cancelled' }),
severity: 'info',
});
+ setSwapState(
+ produce((draft) => {
+ draft.isApprovalLoading = false;
+ }),
+ );
},
});
}, [
@@ -219,6 +238,7 @@ export const useHandleApprove = () => {
pushNotification,
queryClient,
selectedSwapRoute,
+ setSwapState,
tokenIn,
tokenOut,
]);
@@ -231,6 +251,7 @@ export const useHandleSwap = () => {
const pushNotification = usePushNotification();
const [
{ amountIn, amountOut, selectedSwapRoute, slippage, tokenIn, tokenOut },
+ setSwapState,
] = useSwapState();
return useCallback(async () => {
@@ -238,6 +259,11 @@ export const useHandleSwap = () => {
return;
}
+ setSwapState(
+ produce((draft) => {
+ draft.isSwapLoading = true;
+ }),
+ );
await swapActions[selectedSwapRoute.action].swap({
tokenIn,
tokenOut,
@@ -259,19 +285,33 @@ export const useHandleSwap = () => {
title: intl.formatMessage({ defaultMessage: 'Swap complete' }),
severity: 'success',
});
+ setSwapState(
+ produce((draft) => {
+ draft.isSwapLoading = false;
+ }),
+ );
},
onError: () => {
pushNotification({
title: intl.formatMessage({ defaultMessage: 'Swap failed' }),
severity: 'error',
});
+ setSwapState(
+ produce((draft) => {
+ draft.isSwapLoading = false;
+ }),
+ );
},
onReject: () => {
- console.log('REJECT');
pushNotification({
title: intl.formatMessage({ defaultMessage: 'Swap cancelled' }),
severity: 'info',
});
+ setSwapState(
+ produce((draft) => {
+ draft.isSwapLoading = false;
+ }),
+ );
},
});
}, [
@@ -282,6 +322,7 @@ export const useHandleSwap = () => {
pushNotification,
queryClient,
selectedSwapRoute,
+ setSwapState,
slippage,
tokenIn,
tokenOut,
diff --git a/libs/oeth/swap/src/state.ts b/libs/oeth/swap/src/state.ts
index c9b093fb3..54b5d3960 100644
--- a/libs/oeth/swap/src/state.ts
+++ b/libs/oeth/swap/src/state.ts
@@ -19,15 +19,13 @@ export const { Provider: SwapProvider, useTracked: useSwapState } =
tokenIn: tokens.mainnet.ETH,
amountOut: 0n,
tokenOut: tokens.mainnet.OETH,
- isAmountOutLoading: false,
- isPriceOutLoading: false,
- isBalanceOutLoading: false,
swapRoutes: [],
selectedSwapRoute: null,
+ slippage: 0.01,
isSwapRoutesLoading: false,
isApproved: false,
isApprovalLoading: false,
- slippage: 0.01,
+ isSwapLoading: false,
});
const { CurveRegistryExchange, OethPoolUnderlyings } = useCurve();
@@ -39,11 +37,10 @@ export const { Provider: SwapProvider, useTracked: useSwapState } =
draft.swapRoutes = [];
draft.selectedSwapRoute = null;
draft.amountOut = 0n;
- draft.isAmountOutLoading = false;
- draft.isPriceOutLoading = false;
draft.isSwapRoutesLoading = false;
draft.isApproved = false;
draft.isApprovalLoading = false;
+ draft.isSwapLoading = false;
}),
);
return;
@@ -90,8 +87,6 @@ export const { Provider: SwapProvider, useTracked: useSwapState } =
draft.swapRoutes = sortedRoutes;
draft.selectedSwapRoute = sortedRoutes[0];
draft.amountOut = sortedRoutes[0].estimatedAmount ?? 0n;
- draft.isAmountOutLoading = false;
- draft.isPriceOutLoading = false;
draft.isSwapRoutesLoading = false;
}),
);
diff --git a/libs/oeth/swap/src/types.ts b/libs/oeth/swap/src/types.ts
index 1e5b638b5..2c479fb78 100644
--- a/libs/oeth/swap/src/types.ts
+++ b/libs/oeth/swap/src/types.ts
@@ -121,13 +121,11 @@ export type SwapState = {
tokenIn: Token;
amountOut: bigint;
tokenOut: Token;
- isAmountOutLoading: boolean;
- isPriceOutLoading: boolean;
- isBalanceOutLoading: boolean;
swapRoutes: EstimatedSwapRoute[];
selectedSwapRoute: EstimatedSwapRoute | null;
+ slippage: number;
isSwapRoutesLoading: boolean;
isApproved: boolean;
isApprovalLoading: boolean;
- slippage: number;
+ isSwapLoading: boolean;
};
diff --git a/libs/oeth/swap/src/views/SwapView.tsx b/libs/oeth/swap/src/views/SwapView.tsx
index 6c6a0d47d..c3dc173fe 100644
--- a/libs/oeth/swap/src/views/SwapView.tsx
+++ b/libs/oeth/swap/src/views/SwapView.tsx
@@ -1,6 +1,14 @@
-import { useMemo, useState } from 'react';
+import { useState } from 'react';
-import { alpha, Box, Button, IconButton, Stack } from '@mui/material';
+import {
+ alpha,
+ Box,
+ Button,
+ CircularProgress,
+ Collapse,
+ IconButton,
+ Stack,
+} from '@mui/material';
import { ApyHeader } from '@origin/oeth/shared';
import { Card, TokenInput } from '@origin/shared/components';
import { ConnectedButton, usePrices } from '@origin/shared/providers';
@@ -11,6 +19,7 @@ import { useAccount, useBalance } from 'wagmi';
import { GasPopover } from '../components/GasPopover';
import { SwapRoute } from '../components/SwapRoute';
import { TokenSelectModal } from '../components/TokenSelectModal';
+import { routeActionLabel } from '../constants';
import {
useHandleAmountInChange,
useHandleApprove,
@@ -52,10 +61,10 @@ function SwapViewWrapped() {
amountOut,
tokenIn,
tokenOut,
- isAmountOutLoading,
- isPriceOutLoading,
- isBalanceOutLoading,
selectedSwapRoute,
+ isSwapLoading,
+ isSwapRoutesLoading,
+ isApprovalLoading,
},
] = useSwapState();
const { tokensIn, tokensOut } = useTokenOptions();
@@ -77,16 +86,6 @@ function SwapViewWrapped() {
const handleApprove = useHandleApprove();
const handleSwap = useHandleSwap();
- const needsApproval = useMemo(
- () =>
- isConnected &&
- amountIn > 0n &&
- !isNilOrEmpty(selectedSwapRoute) &&
- selectedSwapRoute?.approvedAmount < amountIn &&
- allowance < amountIn,
- [allowance, amountIn, isConnected, selectedSwapRoute],
- );
-
const handleCloseSelectionModal = () => {
setTokenSource(null);
};
@@ -95,6 +94,42 @@ function SwapViewWrapped() {
handleTokenChange(tokenSource, value);
};
+ const needsApproval =
+ isConnected &&
+ amountIn > 0n &&
+ !isBalTokenInLoading &&
+ balTokenIn.value >= amountIn &&
+ !isNilOrEmpty(selectedSwapRoute) &&
+ selectedSwapRoute?.approvedAmount < amountIn &&
+ allowance < amountIn;
+
+ const swapButtonLabel =
+ amountIn === 0n
+ ? intl.formatMessage({ defaultMessage: 'Enter an amount' })
+ : amountIn > balTokenIn?.value
+ ? intl.formatMessage({ defaultMessage: 'Insufficient funds' })
+ : !isNilOrEmpty(selectedSwapRoute)
+ ? intl.formatMessage(routeActionLabel[selectedSwapRoute?.action])
+ : '';
+
+ const amountInInputDisabled = isSwapLoading || isApprovalLoading;
+
+ const approveButtonDisabled =
+ isNilOrEmpty(selectedSwapRoute) ||
+ isApprovalLoading ||
+ amountIn > balTokenIn?.value;
+
+ const swapButtonDisabled =
+ needsApproval ||
+ isNilOrEmpty(selectedSwapRoute) ||
+ isBalTokenInLoading ||
+ amountIn > balTokenIn?.value ||
+ amountIn === 0n;
+
+ const approveButtonLoading = isSwapRoutesLoading || isApprovalLoading;
+
+ const swapButtonLoading = isSwapRoutesLoading || isSwapLoading;
+
return (
<>
@@ -144,6 +179,7 @@ function SwapViewWrapped() {
tokenPriceUsd={prices?.[tokenIn.symbol]}
isPriceLoading={isPriceLoading}
isConnected={isConnected}
+ isAmountDisabled={amountInInputDisabled}
sx={{
...commonStyles,
backgroundColor: 'grey.900',
@@ -175,15 +211,15 @@ function SwapViewWrapped() {
{
setTokenSource('tokenOut');
}}
tokenPriceUsd={prices?.[tokenOut.symbol]}
- isPriceLoading={isPriceOutLoading || isPriceLoading}
+ isPriceLoading={isSwapRoutesLoading || isPriceLoading}
inputProps={{ readOnly: true }}
isConnected={isConnected}
sx={{
@@ -196,13 +232,31 @@ function SwapViewWrapped() {
- {needsApproval && (
-