diff --git a/README.md b/README.md index 139f5a454..9bdb02ca2 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ We use `react-query` in conjunction with `graphql-codegen` for interacting with - run the graphql-codegen task with `pnpm nx run oeth-shared:codegen-graphql`, it will generate - the global types in `libs/oeth/shared/src/generated/graphql.ts` and - the generated hooks next to your graphql file (i.e. `/libs/oeth/history/src/queries.generated.tsx`) -- use the generated hooks in your component with fully typed args and results! +- use the generated hooks in your component with fully typed args and results Couple of things to note: - generated hooks receives args as first param, second param exposes all react-query api for controlling execution diff --git a/apps/oeth/src/App.tsx b/apps/oeth/src/App.tsx index e6f6fbea7..04f67ea27 100644 --- a/apps/oeth/src/App.tsx +++ b/apps/oeth/src/App.tsx @@ -11,12 +11,8 @@ export const App = () => { ({ - xs: '112px', - md: `${theme.mixins.toolbar.height}px`, - }), }} maxWidth="sm" > diff --git a/apps/oeth/src/components/Topnav.tsx b/apps/oeth/src/components/Topnav.tsx index 035e03fae..703d55a8f 100644 --- a/apps/oeth/src/components/Topnav.tsx +++ b/apps/oeth/src/components/Topnav.tsx @@ -31,177 +31,187 @@ export function Topnav(props: BoxProps) { useState(null); return ( - ({ - position: 'fixed', - top: 0, - left: 0, - width: 1, - zIndex: theme.zIndex.appBar, - backgroundColor: alpha(theme.palette.background.default, 0.6), - backdropFilter: 'blur(15px)', - height: { - xs: '112px', - md: `${theme.mixins.toolbar.height}px`, - }, - display: 'grid', - borderBottom: { - xs: 'none', - md: `1px solid ${theme.palette.background.paper}`, - }, - columnGap: { xs: 1, md: 10 }, - rowGap: { xs: 0, md: 10 }, - alignItems: 'center', - px: { - xs: 1.5, - md: 3, - }, - pt: { - xs: 1.5, - md: 0, - }, - gridTemplateColumns: { - xs: '1fr 1fr', - md: 'auto 1fr auto', - }, - })} - > + <> ({ + xs: '112px', + md: `${theme.mixins.toolbar.height}px`, + }), + }} + /> + ({ - '& img': { - maxHeight: { - xs: '1rem', - md: '1.5rem', - }, - maxWidth: { - xs: theme.typography.pxToRem(100), - sm: theme.typography.pxToRem(120), - md: theme.typography.pxToRem(180), - }, + position: 'fixed', + top: 0, + left: 0, + width: 1, + zIndex: theme.zIndex.appBar, + backgroundColor: alpha(theme.palette.background.default, 0.6), + backdropFilter: 'blur(15px)', + height: { + xs: '112px', + md: `${theme.mixins.toolbar.height}px`, }, - })} - > - Origin logo - - { - navigate(value); - }} - sx={{ - order: { - xs: 2, - md: 0, + display: 'grid', + borderBottom: { + xs: 'none', + md: `1px solid ${theme.palette.background.paper}`, }, - gridColumn: { - xs: 'span 2', - md: 'span 1', + columnGap: { xs: 1, md: 10 }, + rowGap: { xs: 0, md: 10 }, + alignItems: 'center', + px: { + xs: 1.5, + md: 3, }, - marginBlockStart: { - xs: 2, + pt: { + xs: 1.5, md: 0, }, - '& .MuiTabs-flexContainer': { - justifyContent: { - xs: 'center', - md: 'flex-start', - }, + gridTemplateColumns: { + xs: '1fr 1fr', + md: 'auto 1fr auto', }, - }} - > - {routes[0].children.map((route) => ( - - ))} - - - ({ + '& img': { + maxHeight: { + xs: '1rem', + md: '1.5rem', + }, + maxWidth: { + xs: theme.typography.pxToRem(100), + sm: theme.typography.pxToRem(120), + md: theme.typography.pxToRem(180), + }, + }, + })} + > + Origin logo + + { + navigate(value); + }} sx={{ - borderRadius: 25, - paddingX: { - md: 3, + order: { xs: 2, + md: 0, }, - paddingY: { - md: 1, - xs: 0.75, + gridColumn: { + xs: 'span 2', + md: 'span 1', }, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontWeight: 500, - minHeight: { xs: 36, md: 44 }, - background: `linear-gradient(0deg, ${alpha( - theme.palette.common.white, - 0.05, - )} 0%, ${alpha(theme.palette.common.white, 0.05)} 100%), ${ - theme.palette.background.paper - };`, - '&:hover': { - background: (theme) => theme.palette.background.paper, + marginBlockStart: { + xs: 2, + md: 0, + }, + '& .MuiTabs-flexContainer': { + justifyContent: { + xs: 'center', + md: 'flex-start', + }, }, }} > - {isMd - ? intl.formatMessage({ defaultMessage: 'IPFS' }) - : intl.formatMessage({ defaultMessage: 'View on IPFS' })} - - { - if (isConnected) { - setAccountModalAnchor(e.currentTarget); - } + {routes[0].children.map((route) => ( + + ))} + + + theme.palette.background.paper, + }, + }} + > + {isMd + ? intl.formatMessage({ defaultMessage: 'IPFS' }) + : intl.formatMessage({ defaultMessage: 'View on IPFS' })} + + { + if (isConnected) { + setAccountModalAnchor(e.currentTarget); + } + }} + sx={{ + borderRadius: 25, + paddingX: { + md: 3, + xs: 2, + }, + paddingY: { + md: 1, + xs: 0.75, + }, + minWidth: 36, + maxWidth: { xs: isConnected ? 36 : 160, sm: 160, lg: 220 }, + fontWeight: 500, + minHeight: { xs: 36, md: 44 }, + }} + /> + + + theme.palette.background.paper, + position: 'relative', + width: 'calc(100% + 1.5rem)', + bottom: '-3rem', + left: '-0.75rem', }} /> - - theme.palette.background.paper, - position: 'relative', - width: 'calc(100% + 1.5rem)', - bottom: '-3rem', - left: '-0.75rem', - }} - /> - + ); } diff --git a/libs/oeth/history/src/components/APYContainer.tsx b/libs/oeth/history/src/components/APYContainer.tsx index 3b5e28f5e..a78598026 100644 --- a/libs/oeth/history/src/components/APYContainer.tsx +++ b/libs/oeth/history/src/components/APYContainer.tsx @@ -53,7 +53,7 @@ export function APYContainer() { - {intl.formatMessage({ defaultMessage: 'OETH transactions' })} + {intl.formatMessage({ defaultMessage: 'OETH Transactions' })} - +
{table.getHeaderGroups().map((headerGroup) => ( { ); }; -export const useHandleSlippageChange = () => { - const [, setRedeemState] = useRedeemState(); - - return useCallback( - (value: number) => { - setRedeemState( - produce((state) => { - state.slippage = value; - }), - ); - }, - [setRedeemState], - ); -}; - export const useHandleRedeem = () => { const intl = useIntl(); + const { value: slippage } = useSlippage(); const pushNotification = usePushNotification(); const { address } = useAccount(); - const [{ amountIn, amountOut, slippage }, setRedeemState] = useRedeemState(); + const [{ amountIn, amountOut }, setRedeemState] = useRedeemState(); const wagmiClient = useQueryClient(); return useCallback(async () => { diff --git a/libs/oeth/redeem/src/state.ts b/libs/oeth/redeem/src/state.ts index 9591b9f82..a15ae9d57 100644 --- a/libs/oeth/redeem/src/state.ts +++ b/libs/oeth/redeem/src/state.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { contracts, tokens, whales } from '@origin/shared/contracts'; -import { usePushNotification } from '@origin/shared/providers'; +import { usePushNotification, useSlippage } from '@origin/shared/providers'; import { isNilOrEmpty } from '@origin/shared/utils'; import { useDebouncedEffect } from '@react-hookz/web'; import { useQuery, useQueryClient } from '@tanstack/react-query'; @@ -23,13 +23,13 @@ export const { Provider: RedeemProvider, useTracked: useRedeemState } = split: [], gas: 0n, rate: 0, - slippage: 0.01, isEstimateLoading: false, isRedeemLoading: false, }); const intl = useIntl(); const queryClient = useQueryClient(); const pushNotification = usePushNotification(); + const { value: slippage } = useSlippage(); const { data: splitAddresses } = useQuery({ queryKey: ['assetsDecimals'], @@ -123,7 +123,7 @@ export const { Provider: RedeemProvider, useTracked: useRedeemState } = const minAmountOut = parseUnits( ( +formatUnits(total, MIX_TOKEN.decimals) - - +formatUnits(total, MIX_TOKEN.decimals) * state.slippage + +formatUnits(total, MIX_TOKEN.decimals) * slippage ).toString(), MIX_TOKEN.decimals, ); diff --git a/libs/oeth/redeem/src/types.ts b/libs/oeth/redeem/src/types.ts index 415f58999..6a50aca6c 100644 --- a/libs/oeth/redeem/src/types.ts +++ b/libs/oeth/redeem/src/types.ts @@ -11,7 +11,6 @@ export type RedeemState = { split: RedeemEstimate[]; gas: bigint; rate: number; - slippage: number; isEstimateLoading: boolean; isRedeemLoading: boolean; }; diff --git a/libs/oeth/redeem/src/views/RedeemView.tsx b/libs/oeth/redeem/src/views/RedeemView.tsx index 159d12084..382796cac 100644 --- a/libs/oeth/redeem/src/views/RedeemView.tsx +++ b/libs/oeth/redeem/src/views/RedeemView.tsx @@ -11,17 +11,17 @@ import { import { GasPopover } from '@origin/oeth/shared'; import { TokenInput } from '@origin/shared/components'; import { tokens } from '@origin/shared/contracts'; -import { ConnectedButton, usePrices } from '@origin/shared/providers'; +import { + ConnectedButton, + usePrices, + useSlippage, +} from '@origin/shared/providers'; import { composeContexts } from '@origin/shared/utils'; import { useIntl } from 'react-intl'; import { useAccount, useBalance } from 'wagmi'; import { RedeemRoute } from '../components/RedeemRoute'; -import { - useHandleAmountInChange, - useHandleRedeem, - useHandleSlippageChange, -} from '../hooks'; +import { useHandleAmountInChange, useHandleRedeem } from '../hooks'; import { RedeemProvider, useRedeemState } from '../state'; import type { BoxProps } from '@mui/material'; @@ -55,9 +55,9 @@ export const RedeemView = () => function RedeemViewWrapped() { const intl = useIntl(); + const { value: slippage, set: setSlippage } = useSlippage(); const { address, isConnected } = useAccount(); - const [{ amountIn, slippage, isRedeemLoading, isEstimateLoading }] = - useRedeemState(); + const [{ amountIn, isRedeemLoading, isEstimateLoading }] = useRedeemState(); const { data: prices, isLoading: isPricesLoading } = usePrices(); const { data: balOeth, isLoading: isBalOethLoading } = useBalance({ address, @@ -65,16 +65,20 @@ function RedeemViewWrapped() { watch: true, scopeKey: 'redeem_balance', }); - const handleSlippageChange = useHandleSlippageChange(); + const handleAmountInChange = useHandleAmountInChange(); const handleRedeem = useHandleRedeem(); + const handleSlippageChange = (val: number) => { + setSlippage(val); + }; + const redeemButtonLabel = amountIn === 0n ? intl.formatMessage({ defaultMessage: 'Enter an amount' }) : amountIn > balOeth?.value ? intl.formatMessage({ defaultMessage: 'Insufficient funds' }) - : intl.formatMessage({ defaultMessage: 'Redeem for mix' }); + : intl.formatMessage({ defaultMessage: 'Redeem' }); const redeemButtonDisabled = isBalOethLoading || isEstimateLoading || diff --git a/libs/oeth/shared/src/components/AccountPopover.tsx b/libs/oeth/shared/src/components/AccountPopover.tsx index 3cac0c061..21c09a77d 100644 --- a/libs/oeth/shared/src/components/AccountPopover.tsx +++ b/libs/oeth/shared/src/components/AccountPopover.tsx @@ -20,6 +20,8 @@ import type { StackProps } from '@mui/material'; import type { Token } from '@origin/shared/contracts'; const balanceTokens = [ + tokens.mainnet.OETH, + tokens.mainnet.WOETH, tokens.mainnet.WETH, tokens.mainnet.rETH, tokens.mainnet.frxETH, diff --git a/libs/oeth/shared/src/components/GasPopover.tsx b/libs/oeth/shared/src/components/GasPopover.tsx index 7a7cdab56..510e85e97 100644 --- a/libs/oeth/shared/src/components/GasPopover.tsx +++ b/libs/oeth/shared/src/components/GasPopover.tsx @@ -14,7 +14,7 @@ import { Stack, useTheme, } from '@mui/material'; -import { PercentInput } from '@origin/shared/components'; +import { InfoTooltip, PercentInput } from '@origin/shared/components'; import { useIntl } from 'react-intl'; import { useFeeData } from 'wagmi'; @@ -89,8 +89,18 @@ export function GasPopover({ > - + {intl.formatMessage({ defaultMessage: 'Price Tolerance' })} + diff --git a/libs/oeth/swap/src/components/ApyHeader.tsx b/libs/oeth/swap/src/components/ApyHeader.tsx index 04c01e999..d2bffbfcc 100644 --- a/libs/oeth/swap/src/components/ApyHeader.tsx +++ b/libs/oeth/swap/src/components/ApyHeader.tsx @@ -94,19 +94,21 @@ export const ApyHeader = (props: StackProps) => { }} MenuListProps={{ dense: true }} > - {trailingOptions.map((t) => ( - { - setTrailing(t); - setAnchorEl(null); - }} - > - {intl.formatMessage(t.label)} - - ))} + {trailingOptions + .filter((t) => t.value !== trailing.value) + .map((t) => ( + { + setTrailing(t); + setAnchorEl(null); + }} + > + {intl.formatMessage(t.label)} + + ))} diff --git a/libs/oeth/swap/src/components/BestRoutes.tsx b/libs/oeth/swap/src/components/BestRoutes.tsx index 9ca8a76d3..8de1bc56e 100644 --- a/libs/oeth/swap/src/components/BestRoutes.tsx +++ b/libs/oeth/swap/src/components/BestRoutes.tsx @@ -17,7 +17,7 @@ export function BestRoutes(props: Grid2Props) { return ( {swapRoutes.slice(0, 2).map((route, index) => ( - + `1px solid ${theme.palette.grey[800]}`, - borderRadius: 1, + border: `1px solid transparent`, + position: 'relative', height: 1, ...(isSelected ? { @@ -100,110 +91,109 @@ export function SwapRouteCard({ ...rest?.sx, }} role="button" - onClick={() => onSelect(route)} + onClick={() => { + onSelect(route); + }} > - - + {isBest && ( + theme.shape.borderRadius, + background: (theme) => theme.palette.background.gradient1, + fontSize: 12, + top: 0, + right: 0, + px: 1, + pt: 0.25, + }} + > + {intl.formatMessage({ defaultMessage: 'Best' })} + + )} + + + + {isLoading ? ( + + ) : ( + + )} + + + {isLoading ? ( - + ) : ( - + formatAmount(route.estimatedAmount, route.tokenOut.decimals) )} - - - + + + + + {isLoading ? ( + + ) : ( + `(${intl.formatNumber(convertedAmount, currencyFormat)})` + )} + + + + + + {isLoading ? ( + + ) : ( + intl.formatMessage(routeActionLabel[route.action]) + )} + + + + + {intl.formatMessage({ defaultMessage: 'Rate:' })} + + {isLoading ? ( - + ) : ( - formatAmount(route.estimatedAmount, route.tokenOut.decimals) + `1:${intl.formatNumber(route.rate, quantityFormat)}` )} - - - - {isLoading ? ( + + + + {intl.formatMessage({ defaultMessage: 'Gas:' })} + + + {isGasLoading ? ( ) : ( - `(${intl.formatNumber(convertedAmount, currencyFormat)})` + `~${intl.formatNumber(gasPrice, currencyFormat)}` )} - - - {isBest && ( - theme.shape.borderRadius, - background: (theme) => theme.palette.background.gradient1, - fontSize: 12, - top: (theme) => theme.spacing(-3), - right: (theme) => theme.spacing(-1.25), - px: 1, - pt: 0.25, - }} - > - {intl.formatMessage({ defaultMessage: 'Best' })} - - )} - - } - > - - - {isLoading ? ( - - ) : ( - intl.formatMessage(routeActionLabel[route.action]) - )} - - - - - {intl.formatMessage({ defaultMessage: 'Rate:' })} - - - {isLoading ? ( - - ) : ( - `1:${intl.formatNumber(route.rate, quantityFormat)}` - )} - - - - - {intl.formatMessage({ defaultMessage: 'Gas:' })} - - - {isGasLoading ? ( - - ) : ( - `~${intl.formatNumber(gasPrice, currencyFormat)}` - )} - + + diff --git a/libs/oeth/swap/src/constants.ts b/libs/oeth/swap/src/constants.ts index 5f630efdc..55623e682 100644 --- a/libs/oeth/swap/src/constants.ts +++ b/libs/oeth/swap/src/constants.ts @@ -19,14 +19,26 @@ export const routeActionLabel: Record = { 'mint-vault': defineMessage({ defaultMessage: 'Mint with Vault' }), 'swap-curve': defineMessage({ defaultMessage: 'Swap with Curve' }), 'swap-curve-eth': defineMessage({ defaultMessage: 'Swap with CurvePool' }), - 'swap-zapper-eth': defineMessage({ defaultMessage: 'Zap + Mint with Vault' }), + 'swap-zapper-eth': defineMessage({ defaultMessage: 'Mint with Vault' }), 'swap-zapper-sfrxeth': defineMessage({ - defaultMessage: 'Zap + Mint with Vault', + defaultMessage: 'Mint with Vault', }), 'unwrap-woeth': defineMessage({ defaultMessage: 'Unwrap with Origin' }), 'wrap-oeth': defineMessage({ defaultMessage: 'Wrap with Origin' }), }; +export const buttonActionLabel: Record = { + 'mint-vault': defineMessage({ defaultMessage: 'Mint' }), + 'swap-curve': defineMessage({ defaultMessage: 'Swap' }), + 'swap-curve-eth': defineMessage({ defaultMessage: 'Swap' }), + 'swap-zapper-eth': defineMessage({ defaultMessage: 'Mint' }), + 'swap-zapper-sfrxeth': defineMessage({ + defaultMessage: 'Mint', + }), + 'unwrap-woeth': defineMessage({ defaultMessage: 'Unwrap' }), + 'wrap-oeth': defineMessage({ defaultMessage: 'Wrap' }), +}; + export const swapRoutes = [ // Mint { diff --git a/libs/oeth/swap/src/hooks.ts b/libs/oeth/swap/src/hooks.ts index 2508f7ac7..dd95fbc14 100644 --- a/libs/oeth/swap/src/hooks.ts +++ b/libs/oeth/swap/src/hooks.ts @@ -1,6 +1,10 @@ import { useCallback, useMemo } from 'react'; -import { useCurve, usePushNotification } from '@origin/shared/providers'; +import { + useCurve, + usePushNotification, + useSlippage, +} from '@origin/shared/providers'; import { isNilOrEmpty } from '@origin/shared/utils'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { produce } from 'immer'; @@ -22,8 +26,10 @@ export const useHandleAmountInChange = () => { (amount: bigint) => { setSwapState( produce((state) => { - state.amountIn = amount; - state.isSwapRoutesLoading = amount !== 0n; + if (state.amountIn !== amount) { + state.amountIn = amount; + state.isSwapRoutesLoading = amount !== 0n; + } }), ); }, @@ -166,21 +172,6 @@ export const useSwapRouteAllowance = (route: SwapRoute) => { }); }; -export const useHandleSlippageChange = () => { - const [, setSwapState] = useSwapState(); - - return useCallback( - (value: number) => { - setSwapState( - produce((state) => { - state.slippage = value; - }), - ); - }, - [setSwapState], - ); -}; - export const useHandleApprove = () => { const intl = useIntl(); const { address } = useAccount(); @@ -263,13 +254,14 @@ export const useHandleApprove = () => { export const useHandleSwap = () => { const intl = useIntl(); + const { value: slippage } = useSlippage(); const { address } = useAccount(); const curve = useCurve(); const queryClient = useQueryClient(); const wagmiClient = useWagmiClient(); const pushNotification = usePushNotification(); const [ - { amountIn, amountOut, selectedSwapRoute, slippage, tokenIn, tokenOut }, + { amountIn, amountOut, selectedSwapRoute, tokenIn, tokenOut }, setSwapState, ] = useSwapState(); @@ -302,35 +294,25 @@ 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: () => { pushNotification({ title: intl.formatMessage({ defaultMessage: 'Swap cancelled' }), severity: 'info', }); - setSwapState( - produce((draft) => { - draft.isSwapLoading = false; - }), - ); }, }); + setSwapState( + produce((draft) => { + draft.isSwapLoading = false; + }), + ); }, [ address, amountIn, diff --git a/libs/oeth/swap/src/state.ts b/libs/oeth/swap/src/state.ts index 089de3d8d..0e60a175f 100644 --- a/libs/oeth/swap/src/state.ts +++ b/libs/oeth/swap/src/state.ts @@ -1,7 +1,7 @@ import { useState } from 'react'; import { tokens } from '@origin/shared/contracts'; -import { useCurve } from '@origin/shared/providers'; +import { useCurve, useSlippage } from '@origin/shared/providers'; import { useDebouncedEffect } from '@react-hookz/web'; import { useQueryClient } from '@tanstack/react-query'; import { produce } from 'immer'; @@ -21,13 +21,13 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = tokenOut: tokens.mainnet.OETH, swapRoutes: [], selectedSwapRoute: null, - slippage: 0.001, isSwapRoutesLoading: false, isApproved: false, isApprovalLoading: false, isSwapLoading: false, }); const queryClient = useQueryClient(); + const { value: slippage } = useSlippage(); const { CurveRegistryExchange, OethPoolUnderlyings } = useCurve(); useDebouncedEffect( @@ -55,7 +55,7 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = state.tokenIn.symbol, state.tokenOut.symbol, route.action, - state.slippage, + slippage, state.amountIn.toString(), ] as const, queryFn: async () => @@ -65,7 +65,7 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = amountIn: state.amountIn, amountOut: state.amountOut, route, - slippage: state.slippage, + slippage, curve: { CurveRegistryExchange, OethPoolUnderlyings, diff --git a/libs/oeth/swap/src/types.ts b/libs/oeth/swap/src/types.ts index 060b23520..4cf5e9db0 100644 --- a/libs/oeth/swap/src/types.ts +++ b/libs/oeth/swap/src/types.ts @@ -122,7 +122,6 @@ export type SwapState = { tokenOut: Token; swapRoutes: EstimatedSwapRoute[]; selectedSwapRoute: EstimatedSwapRoute | null; - slippage: number; isSwapRoutesLoading: boolean; isApproved: boolean; isApprovalLoading: boolean; diff --git a/libs/oeth/swap/src/views/SwapView.tsx b/libs/oeth/swap/src/views/SwapView.tsx index a7417ccfa..285c7df93 100644 --- a/libs/oeth/swap/src/views/SwapView.tsx +++ b/libs/oeth/swap/src/views/SwapView.tsx @@ -15,19 +15,22 @@ import { } from '@mui/material'; import { GasPopover } from '@origin/oeth/shared'; import { TokenInput } from '@origin/shared/components'; -import { ConnectedButton, usePrices } from '@origin/shared/providers'; +import { + ConnectedButton, + usePrices, + useSlippage, +} from '@origin/shared/providers'; import { composeContexts, isNilOrEmpty } from '@origin/shared/utils'; import { useIntl } from 'react-intl'; -import { useAccount, useBalance } from 'wagmi'; +import { mainnet, useAccount, useBalance, useNetwork } from 'wagmi'; import { ApyHeader } from '../components/ApyHeader'; import { SwapRoute } from '../components/SwapRoute'; import { TokenSelectModal } from '../components/TokenSelectModal'; -import { routeActionLabel } from '../constants'; +import { buttonActionLabel } from '../constants'; import { useHandleAmountInChange, useHandleApprove, - useHandleSlippageChange, useHandleSwap, useHandleTokenChange, useHandleTokenFlip, @@ -69,7 +72,9 @@ export const SwapView = () => function SwapViewWrapped() { const intl = useIntl(); + const { value: slippage, set: setSlippage } = useSlippage(); const { address, isConnected } = useAccount(); + const { chain } = useNetwork(); const [tokenSource, setTokenSource] = useState(null); const [ { @@ -78,7 +83,6 @@ function SwapViewWrapped() { tokenIn, tokenOut, selectedSwapRoute, - slippage, isSwapLoading, isSwapRoutesLoading, isApprovalLoading, @@ -99,7 +103,6 @@ function SwapViewWrapped() { watch: true, scopeKey: 'swap_balance', }); - const handleSlippageChange = useHandleSlippageChange(); const handleAmountInChange = useHandleAmountInChange(); const handleTokenChange = useHandleTokenChange(); const handleTokenFlip = useHandleTokenFlip(); @@ -114,6 +117,10 @@ function SwapViewWrapped() { handleTokenChange(tokenSource, value); }; + const handleSlippageChange = (val: number) => { + setSlippage(val); + }; + const needsApproval = isConnected && amountIn > 0n && @@ -128,7 +135,7 @@ function SwapViewWrapped() { : amountIn > balTokenIn?.value ? intl.formatMessage({ defaultMessage: 'Insufficient funds' }) : !isNilOrEmpty(selectedSwapRoute) - ? intl.formatMessage(routeActionLabel[selectedSwapRoute?.action]) + ? intl.formatMessage(buttonActionLabel[selectedSwapRoute?.action]) : ''; const amountInInputDisabled = isSwapLoading || isApprovalLoading; const approveButtonDisabled = @@ -188,6 +195,10 @@ function SwapViewWrapped() { onTokenClick={() => { setTokenSource('tokenIn'); }} + isNativeCurrency={ + tokenIn.symbol === + (chain?.nativeCurrency.symbol ?? mainnet.nativeCurrency.symbol) + } tokenPriceUsd={prices?.[tokenIn.symbol]} isPriceLoading={isPriceLoading} isConnected={isConnected} @@ -233,6 +244,10 @@ function SwapViewWrapped() { onTokenClick={() => { setTokenSource('tokenOut'); }} + isNativeCurrency={ + tokenOut.symbol === + (chain?.nativeCurrency.symbol ?? mainnet.nativeCurrency.symbol) + } tokenPriceUsd={prices?.[tokenOut.symbol]} isPriceLoading={isSwapRoutesLoading || isPriceLoading} isConnected={isConnected} diff --git a/libs/shared/components/src/InfoTooltip/index.tsx b/libs/shared/components/src/InfoTooltip/index.tsx index c625bb3c9..94bc25eb7 100644 --- a/libs/shared/components/src/InfoTooltip/index.tsx +++ b/libs/shared/components/src/InfoTooltip/index.tsx @@ -1,15 +1,18 @@ import { Box, Tooltip, Typography } from '@mui/material'; +import type { BoxProps } from '@mui/material'; + export type InfoTooltipProps = { tooltipLabel: string; iconSize?: number; iconColor?: string; -}; +} & BoxProps; export function InfoTooltip({ tooltipLabel, iconSize = 12, iconColor = 'text.secondary', + ...rest }: InfoTooltipProps) { return ( theme.typography.pxToRem(iconSize), height: (theme) => theme.typography.pxToRem(iconSize), color: iconColor, + + ...rest?.sx, }} > diff --git a/libs/shared/components/src/Inputs/TokenInput.tsx b/libs/shared/components/src/Inputs/TokenInput.tsx index 178340083..3615606ee 100644 --- a/libs/shared/components/src/Inputs/TokenInput.tsx +++ b/libs/shared/components/src/Inputs/TokenInput.tsx @@ -7,7 +7,7 @@ import { isNilOrEmpty, } from '@origin/shared/utils'; import { useIntl } from 'react-intl'; -import { formatUnits } from 'viem'; +import { formatUnits, parseEther } from 'viem'; import { BigIntInput } from './BigIntInput'; @@ -16,6 +16,8 @@ import type { Token } from '@origin/shared/contracts'; import type { BigintInputProps } from './BigIntInput'; +const MIN_ETH_FOR_GAS = '0.015'; + export type TokenInputProps = { amount: bigint; decimals?: number; @@ -30,6 +32,7 @@ export type TokenInputProps = { disableMaxButton?: boolean; token: Token; onTokenClick?: () => void; + isNativeCurrency?: boolean; isTokenClickDisabled?: boolean; tokenPriceUsd?: number; isPriceLoading?: boolean; @@ -56,6 +59,7 @@ export const TokenInput = forwardRef( disableMaxButton, token, onTokenClick, + isNativeCurrency = false, isTokenClickDisabled, tokenPriceUsd = 0, isPriceLoading, @@ -68,10 +72,16 @@ export const TokenInput = forwardRef( const intl = useIntl(); const handleMaxClick = () => { - onAmountChange(balance); + const max = isNativeCurrency + ? balance - parseEther(MIN_ETH_FOR_GAS) + : balance; + onAmountChange(max); }; const amountUsd = +formatUnits(amount, decimals) * tokenPriceUsd; + const maxVisible = + !hideMaxButton && + (isNativeCurrency ? balance > parseEther(MIN_ETH_FOR_GAS) : true); const maxDisabled = disableMaxButton || !isConnected || isBalanceLoading; return ( @@ -148,7 +158,7 @@ export const TokenInput = forwardRef( }, )} - {!hideMaxButton && ( + {maxVisible && (