diff --git a/libs/oeth/swap/src/actions/defaultApi.ts b/libs/oeth/swap/src/actions/defaultApi.ts index 31fd06653..8232b1e9a 100644 --- a/libs/oeth/swap/src/actions/defaultApi.ts +++ b/libs/oeth/swap/src/actions/defaultApi.ts @@ -1,5 +1,3 @@ -import { isNilOrEmpty } from '@origin/shared/utils'; - import type { EstimateAmount, EstimateGas, @@ -12,17 +10,18 @@ const estimateAmount: EstimateAmount = async ( _tokenOut, amountIn, ) => { - if (amountIn === 0n) { - return 0n; - } + console.log('Amount estimation not implemented'); return amountIn; }; -const estimateGas: EstimateGas = async (_tokenIn, _tokenOut, amountIn) => { - if (amountIn === 0n) { - return 0n; - } +const estimateGas: EstimateGas = async ( + _tokenIn, + _tokenOut, + _amountIn, + _slippage, +) => { + console.log('Gas estimation not implemented'); return 0n; }; @@ -32,20 +31,22 @@ const estimateRoute: EstimateRoute = async ( tokenOut, amountIn, route, + slippage, ) => { if (amountIn === 0n) { return { ...route, estimatedAmount: 0n, gas: 0n, rate: 0 }; } - const estimatedAmount = await estimateAmount(tokenIn, tokenOut, amountIn); + const [estimatedAmount, gas] = await Promise.all([ + estimateAmount(tokenIn, tokenOut, amountIn), + estimateGas(tokenIn, tokenOut, amountIn, slippage), + ]); - return { ...route, estimatedAmount, gas: 0n, rate: 0 }; + return { ...route, estimatedAmount, gas, rate: 0 }; }; -const swap: Swap = async (_tokenIn, _tokenOut, amountIn, route) => { - if (amountIn === 0n || isNilOrEmpty(route)) { - return; - } +const swap: Swap = async (_tokenIn, _tokenOut, _amountIn, _route) => { + console.log('Route swap operation not implemented'); }; export default { diff --git a/libs/oeth/swap/src/actions/mintVault.ts b/libs/oeth/swap/src/actions/mintVault.ts index 323ad32a5..d510ad69a 100644 --- a/libs/oeth/swap/src/actions/mintVault.ts +++ b/libs/oeth/swap/src/actions/mintVault.ts @@ -1,13 +1,91 @@ +import { contracts } from '@origin/shared/contracts'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { getAccount, getPublicClient, readContract } from '@wagmi/core'; +import { formatUnits } from 'viem'; + +import type { EstimateGas, EstimateRoute } from '../types'; import type { EstimateAmount } from '../types'; -const estimateAmount: EstimateAmount = async (tokenIn, tokenOut, amountIn) => { +const estimateAmount: EstimateAmount = async (tokenIn, _tokenOut, amountIn) => { if (amountIn === 0n) { return 0n; } - return amountIn * 2n; + const data = await readContract({ + address: contracts.mainnet.OETHVaultCore.address, + abi: contracts.mainnet.OETHVaultCore.abi, + functionName: 'priceUnitMint', + args: [tokenIn.address], + }); + + return amountIn * data; +}; + +const estimateGas: EstimateGas = async ( + _tokenIn, + _tokenOut, + amountIn, + slippage, +) => { + let gasEstimate = 0n; + + const publicClient = getPublicClient(); + + if (amountIn === 0n) { + return gasEstimate; + } + + const { address } = getAccount(); + + if (!isNilOrEmpty(address)) { + try { + gasEstimate = await publicClient.estimateContractGas({ + address: contracts.mainnet.WOETH.address, + abi: contracts.mainnet.WOETH.abi, + functionName: 'deposit', + args: [amountIn, address], + account: address, + }); + + return gasEstimate; + } catch {} + } + + try { + gasEstimate = 0n; + } catch {} + + return gasEstimate; +}; + +const estimateRoute: EstimateRoute = async ( + tokenIn, + tokenOut, + amountIn, + route, + slippage, +) => { + if (amountIn === 0n) { + return { ...route, estimatedAmount: 0n, gas: 0n, rate: 0 }; + } + + const [estimatedAmount, gas] = await Promise.all([ + estimateAmount(tokenIn, tokenOut, amountIn), + estimateGas(tokenIn, tokenOut, amountIn, slippage), + ]); + + return { + ...route, + estimatedAmount, + gas, + rate: + +formatUnits(amountIn, tokenIn.decimals) / + +formatUnits(estimatedAmount, tokenOut.decimals), + }; }; export default { estimateAmount, + estimateGas, + estimateRoute, }; diff --git a/libs/oeth/swap/src/actions/swapCurve.ts b/libs/oeth/swap/src/actions/swapCurve.ts index 6c8dd62db..2922be9af 100644 --- a/libs/oeth/swap/src/actions/swapCurve.ts +++ b/libs/oeth/swap/src/actions/swapCurve.ts @@ -1,7 +1,16 @@ import curve from '@curvefi/api'; +import { tokens } from '@origin/shared/contracts'; +import { waitForTransaction } from '@wagmi/core'; import { formatUnits, parseUnits } from 'viem'; -import type { EstimateAmount, EstimateRoute } from '../types'; +import type { HexAddress } from '@origin/shared/utils'; + +import type { + EstimateAmount, + EstimateGas, + EstimateRoute, + Swap, +} from '../types'; const ETH = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; @@ -28,22 +37,105 @@ const estimateAmount: EstimateAmount = async (tokenIn, tokenOut, amountIn) => { return parseUnits(routes.output, tokenOut.decimals); }; +const estimateGas: EstimateGas = async ( + tokenIn, + tokenOut, + amountIn, + _slippage, +) => { + let gasEstimate = 0n; + + if (amountIn === 0n) { + return gasEstimate; + } + + try { + console.time('curve gas estimate'); + const res = await curve.router.estimateGas.swap( + tokenIn?.address ?? ETH, + tokenOut?.address ?? ETH, + formatUnits(amountIn, tokenIn.decimals), + ); + gasEstimate = parseUnits(res.toString(), tokens.mainnet.ETH.decimals); + console.timeEnd('curve gas estimate'); + } catch {} + + return gasEstimate; +}; + const estimateRoute: EstimateRoute = async ( tokenIn, tokenOut, amountIn, route, + slippage, ) => { if (amountIn === 0n) { return { ...route, estimatedAmount: 0n, gas: 0n, rate: 0 }; } - const estimatedAmount = await estimateAmount(tokenIn, tokenOut, amountIn); + const [estimatedAmount, gas] = await Promise.all([ + estimateAmount(tokenIn, tokenOut, amountIn), + estimateGas(tokenIn, tokenOut, amountIn, slippage), + ]); + + return { + ...route, + estimatedAmount, + gas, + rate: + +formatUnits(amountIn, tokenIn.decimals) / + +formatUnits(estimatedAmount, tokenOut.decimals), + }; +}; + +const swap: Swap = async (tokenIn, tokenOut, amountIn) => { + if (amountIn === 0n) { + return; + } - return { ...route, estimatedAmount, gas: 0n, rate: 0 }; + let isApproved = false; + try { + isApproved = await curve.router.isApproved( + tokenIn?.address ?? ETH, + formatUnits(amountIn, tokenIn.decimals), + ); + } catch (e) { + console.log(`swap curve isApproved error!\n${e.message}`); + } + + if (!isApproved) { + try { + const [hash] = await curve.router.approve( + tokenIn?.address ?? ETH, + formatUnits(amountIn, tokenIn.decimals), + ); + await waitForTransaction({ hash: hash as HexAddress }); + // TODO trigger notification + console.log('swap curve approval done!'); + } catch (e) { + console.log(`swap curve approve error!\n${e.message}`); + return; + } + } + + try { + const { hash } = await curve.router.swap( + tokenIn?.address ?? ETH, + tokenOut?.address ?? ETH, + formatUnits(amountIn, tokenIn.decimals), + ); + await waitForTransaction({ hash: hash as HexAddress }); + // TODO trigger notification + console.log(`swap curve done!`); + } catch (e) { + // TODO trigger notification + console.log(`swap curve error!\n${e.message}`); + } }; export default { estimateAmount, estimateRoute, + swap, }; diff --git a/libs/oeth/swap/src/actions/unwrapWOETH.ts b/libs/oeth/swap/src/actions/unwrapWOETH.ts index c8a27846b..4db8d470c 100644 --- a/libs/oeth/swap/src/actions/unwrapWOETH.ts +++ b/libs/oeth/swap/src/actions/unwrapWOETH.ts @@ -1,8 +1,21 @@ import { contracts, whales } from '@origin/shared/contracts'; import { isNilOrEmpty } from '@origin/shared/utils'; -import { getAccount, getPublicClient, readContract } from '@wagmi/core'; +import { + getAccount, + getPublicClient, + prepareWriteContract, + readContract, + waitForTransaction, + writeContract, +} from '@wagmi/core'; +import { formatUnits } from 'viem'; -import type { EstimateAmount, EstimateGas, EstimateRoute } from '../types'; +import type { + EstimateAmount, + EstimateGas, + EstimateRoute, + Swap, +} from '../types'; const estimateAmount: EstimateAmount = async ( _tokenIn, @@ -23,9 +36,13 @@ const estimateAmount: EstimateAmount = async ( return data; }; -const estimateGas: EstimateGas = async (_tokenIn, _tokenOut, amountIn) => { +const estimateGas: EstimateGas = async ( + _tokenIn, + _tokenOut, + amountIn, + _slippage, +) => { let gasEstimate = 0n; - let isError = false; const publicClient = getPublicClient(); @@ -44,23 +61,21 @@ const estimateGas: EstimateGas = async (_tokenIn, _tokenOut, amountIn) => { args: [amountIn, address, address], account: address, }); - } catch { - isError = true; - } - } - if (isError) { - try { - gasEstimate = await publicClient.estimateContractGas({ - address: contracts.mainnet.WOETH.address, - abi: contracts.mainnet.WOETH.abi, - functionName: 'redeem', - args: [amountIn, whales.mainnet.WOETH, whales.mainnet.WOETH], - account: whales.mainnet.WOETH, - }); + return gasEstimate; } catch {} } + try { + gasEstimate = await publicClient.estimateContractGas({ + address: contracts.mainnet.WOETH.address, + abi: contracts.mainnet.WOETH.abi, + functionName: 'redeem', + args: [amountIn, whales.mainnet.WOETH, whales.mainnet.WOETH], + account: whales.mainnet.WOETH, + }); + } catch {} + return gasEstimate; }; @@ -69,6 +84,7 @@ const estimateRoute: EstimateRoute = async ( tokenOut, amountIn, route, + slippage, ) => { if (amountIn === 0n) { return { ...route, estimatedAmount: 0n, gas: 0n, rate: 0 }; @@ -76,14 +92,46 @@ const estimateRoute: EstimateRoute = async ( const [estimatedAmount, gas] = await Promise.all([ estimateAmount(tokenIn, tokenOut, amountIn), - estimateGas(tokenIn, tokenOut, amountIn), + estimateGas(tokenIn, tokenOut, amountIn, slippage), ]); - return { ...route, estimatedAmount, gas, rate: 0 }; + return { + ...route, + estimatedAmount, + gas, + rate: + +formatUnits(amountIn, tokenIn.decimals) / + +formatUnits(estimatedAmount, tokenOut.decimals), + }; +}; + +const swap: Swap = async (_tokenIn, _tokenOut, amountIn, _route) => { + const { address } = getAccount(); + + if (amountIn === 0n || isNilOrEmpty(address)) { + return; + } + + try { + const { request } = await prepareWriteContract({ + address: contracts.mainnet.WOETH.address, + abi: contracts.mainnet.WOETH.abi, + functionName: 'redeem', + args: [amountIn, address, address], + }); + const { hash } = await writeContract(request); + await waitForTransaction({ hash }); + // TODO trigger notification + console.log('unwrap woeth done!'); + } catch (e) { + // TODO trigger notification + console.log(`unwrap woeth error!\n${e.message}`); + } }; export default { estimateAmount, estimateGas, estimateRoute, + swap, }; diff --git a/libs/oeth/swap/src/actions/wrapOETH.ts b/libs/oeth/swap/src/actions/wrapOETH.ts index 94cffd247..0c9b17537 100644 --- a/libs/oeth/swap/src/actions/wrapOETH.ts +++ b/libs/oeth/swap/src/actions/wrapOETH.ts @@ -1,8 +1,21 @@ import { contracts, whales } from '@origin/shared/contracts'; import { isNilOrEmpty } from '@origin/shared/utils'; -import { getAccount, getPublicClient, readContract } from '@wagmi/core'; +import { + getAccount, + getPublicClient, + prepareWriteContract, + readContract, + waitForTransaction, + writeContract, +} from '@wagmi/core'; +import { formatUnits } from 'viem'; -import type { EstimateAmount, EstimateGas, EstimateRoute } from '../types'; +import type { + EstimateAmount, + EstimateGas, + EstimateRoute, + Swap, +} from '../types'; const estimateAmount: EstimateAmount = async ( _tokenIn, @@ -23,9 +36,13 @@ const estimateAmount: EstimateAmount = async ( return data; }; -const estimateGas: EstimateGas = async (_tokenIn, _tokenOut, amountIn) => { +const estimateGas: EstimateGas = async ( + _tokenIn, + _tokenOut, + amountIn, + _slippage, +) => { let gasEstimate = 0n; - let isError = false; const publicClient = getPublicClient(); @@ -44,23 +61,21 @@ const estimateGas: EstimateGas = async (_tokenIn, _tokenOut, amountIn) => { args: [amountIn, address], account: address, }); - } catch { - isError = true; - } - } - if (isError) { - try { - gasEstimate = await publicClient.estimateContractGas({ - address: contracts.mainnet.WOETH.address, - abi: contracts.mainnet.WOETH.abi, - functionName: 'deposit', - args: [amountIn, whales.mainnet.OETH], - account: whales.mainnet.OETH, - }); + return gasEstimate; } catch {} } + try { + gasEstimate = await publicClient.estimateContractGas({ + address: contracts.mainnet.WOETH.address, + abi: contracts.mainnet.WOETH.abi, + functionName: 'deposit', + args: [amountIn, whales.mainnet.OETH], + account: whales.mainnet.OETH, + }); + } catch {} + return gasEstimate; }; @@ -69,6 +84,7 @@ const estimateRoute: EstimateRoute = async ( tokenOut, amountIn, route, + slippage, ) => { if (amountIn === 0n) { return { ...route, estimatedAmount: 0n, gas: 0n, rate: 0 }; @@ -76,14 +92,74 @@ const estimateRoute: EstimateRoute = async ( const [estimatedAmount, gas] = await Promise.all([ estimateAmount(tokenIn, tokenOut, amountIn), - estimateGas(tokenIn, tokenOut, amountIn), + estimateGas(tokenIn, tokenOut, amountIn, slippage), ]); - return { ...route, estimatedAmount, gas, rate: 0 }; + return { + ...route, + estimatedAmount, + gas, + rate: + +formatUnits(amountIn, tokenIn.decimals) / + +formatUnits(estimatedAmount, tokenOut.decimals), + }; +}; + +const swap: Swap = async (_tokenIn, _tokenOut, amountIn, _route) => { + const { address } = getAccount(); + + if (amountIn === 0n || isNilOrEmpty(address)) { + return; + } + + const allowance = await readContract({ + address: contracts.mainnet.OETH.address, + abi: contracts.mainnet.OETH.abi, + functionName: 'allowance', + args: [address, contracts.mainnet.WOETH.address], + }); + + if (allowance < amountIn) { + try { + const { request } = await prepareWriteContract({ + address: contracts.mainnet.OETH.address, + abi: contracts.mainnet.OETH.abi, + functionName: 'approve', + args: [contracts.mainnet.WOETH.address, amountIn], + }); + const { hash } = await writeContract(request); + await waitForTransaction({ hash }); + + // TODO trigger notification + console.log(`wrap woeth approval done!`); + } catch (e) { + // TODO trigger notification + console.error(`wrap oeth approval error!\n${e.message}`); + return; + } + } + + try { + const { request } = await prepareWriteContract({ + address: contracts.mainnet.WOETH.address, + abi: contracts.mainnet.WOETH.abi, + functionName: 'deposit', + args: [amountIn, address], + }); + const { hash } = await writeContract(request); + await waitForTransaction({ hash }); + + // TODO trigger notification + console.log('wrap oeth done!'); + } catch (e) { + // TODO trigger notification + console.error(`wrap oeth error!\n${e.message}`); + } }; export default { estimateAmount, estimateGas, estimateRoute, + swap, }; diff --git a/libs/oeth/swap/src/components/BestRoutes.tsx b/libs/oeth/swap/src/components/BestRoutes.tsx index 2484e3889..7521661fb 100644 --- a/libs/oeth/swap/src/components/BestRoutes.tsx +++ b/libs/oeth/swap/src/components/BestRoutes.tsx @@ -1,16 +1,16 @@ import { Box } from '@mui/material'; +import { useHandleSelectSwapRoute } from '../hooks'; +import { useSwapState } from '../state'; +import { routeEq } from '../utils'; import { SwapRouteCard } from './SwapRouteCard'; -import type { Route } from './SwapRoute'; +import type { BoxProps } from '@mui/material'; -interface Props { - routes: Route[]; - selected: number; - onSelect: (index: number) => void; -} +export function BestRoutes(props: BoxProps) { + const [{ swapRoutes, selectedSwapRoute }] = useSwapState(); + const handleSelectSwapRoute = useHandleSelectSwapRoute(); -export function BestRoutes({ routes, selected, onSelect }: Props) { return ( - {routes.slice(0, 2).map((route, index) => ( + {swapRoutes.slice(0, 2).map((route, index) => ( onSelect(index)} + key={`bestRoute-${index}`} + isSelected={routeEq(selectedSwapRoute, route)} + isBest={index === 0} + onSelect={handleSelectSwapRoute} route={route} /> ))} diff --git a/libs/oeth/swap/src/components/SwapRoute.tsx b/libs/oeth/swap/src/components/SwapRoute.tsx index f799228c7..1268210c2 100644 --- a/libs/oeth/swap/src/components/SwapRoute.tsx +++ b/libs/oeth/swap/src/components/SwapRoute.tsx @@ -1,13 +1,16 @@ -import { useState } from 'react'; - import { Skeleton, Stack, Typography } from '@mui/material'; import { Card, cardStyles } from '@origin/shared/components'; import { useIntl } from 'react-intl'; +import { useSwapState } from '../state'; import { BestRoutes } from './BestRoutes'; import { SwapInfo } from './SwapInfo'; import { SwapRouteAccordion } from './SwapRouteAccordion'; +import type { CardProps } from '@mui/material'; + +import type { EstimatedSwapRoute } from '../types'; + interface Swap { type: 'swap'; } @@ -26,26 +29,29 @@ export type Route = { transactionCost: number; } & (Swap | Redeem); -interface Props { +export type SwapRouteProps = { isLoading: boolean; - routes: Route[]; -} + routes: EstimatedSwapRoute[]; +}; -export function SwapRoute({ isLoading = false, routes }: Props) { +export function SwapRoute(props: Omit) { const intl = useIntl(); - const [selectedRoute, setSelectedRoute] = useState(0); + const [{ swapRoutes, isSwapRoutesLoading }] = useSwapState(); + + const hasContent = swapRoutes.length > 0; - const hasContent = routes.length > 0; return ( theme.palette.background.default, backgroundColor: 'grey.900', borderRadius: 1, + ...props?.sx, }} title={ - isLoading ? ( + isSwapRoutesLoading ? ( {hasContent ? ( <> - setSelectedRoute(index)} - /> - setSelectedRoute(index)} - sx={{ mt: 2 }} - /> + + ) : undefined} diff --git a/libs/oeth/swap/src/components/SwapRouteAccordion.tsx b/libs/oeth/swap/src/components/SwapRouteAccordion.tsx index 96d6816cc..07e070af8 100644 --- a/libs/oeth/swap/src/components/SwapRouteAccordion.tsx +++ b/libs/oeth/swap/src/components/SwapRouteAccordion.tsx @@ -8,23 +8,21 @@ import { } from '@mui/material'; import { useIntl } from 'react-intl'; +import { useHandleSelectSwapRoute } from '../hooks'; +import { useSwapState } from '../state'; +import { routeEq } from '../utils'; import { SwapRouteAccordionItem } from './SwapRouteAccordionItem'; -import type { SxProps } from '@mui/material'; +import type { AccordionProps } from '@mui/material'; -import type { Route } from './SwapRoute'; - -interface Props { - routes: Route[]; - selected: number; - onSelect: (index: number) => void; - sx?: SxProps; -} - -export function SwapRouteAccordion({ routes, selected, onSelect, sx }: Props) { +export function SwapRouteAccordion(props: Omit) { const intl = useIntl(); + const [{ swapRoutes, selectedSwapRoute }] = useSwapState(); + const handleSelectSwapRoute = useHandleSelectSwapRoute(); + return ( @@ -96,13 +93,12 @@ export function SwapRouteAccordion({ routes, selected, onSelect, sx }: Props) { }} > - {routes.slice(2).map((route, index) => ( + {swapRoutes.slice(2).map((route, index) => ( ))} diff --git a/libs/oeth/swap/src/components/SwapRouteAccordionItem.tsx b/libs/oeth/swap/src/components/SwapRouteAccordionItem.tsx index f5ab1d852..e877c47b9 100644 --- a/libs/oeth/swap/src/components/SwapRouteAccordionItem.tsx +++ b/libs/oeth/swap/src/components/SwapRouteAccordionItem.tsx @@ -1,26 +1,37 @@ -import { alpha, Box, Stack, Typography, useTheme } from '@mui/material'; +import { alpha, Box, Stack, Typography } from '@mui/material'; import { currencyFormat, quantityFormat } from '@origin/shared/components'; +import { tokens } from '@origin/shared/contracts'; +import { usePrices } from '@origin/shared/providers'; import { useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; +import { routeActionLabel, routeActionLogos } from '../constants'; import { SwapInfo } from './SwapInfo'; -import type { Route } from './SwapRoute'; +import type { EstimatedSwapRoute } from '../types'; -interface Props { - route: Route; - selected: number; - index: number; - onSelect: (index: number) => void; -} +export type SwapRouteAccordionItemProps = { + route: EstimatedSwapRoute; + isSelected: boolean; + onSelect: (route: EstimatedSwapRoute) => void; +}; export function SwapRouteAccordionItem({ route, - selected, - index, + isSelected, onSelect, -}: Props) { - const theme = useTheme(); +}: SwapRouteAccordionItemProps) { const intl = useIntl(); + const { data: prices } = usePrices(); + + const estimatedAmount = +formatUnits( + route.estimatedAmount, + route.tokenOut.decimals, + ); + const convertedAmount = + (prices?.[route.tokenOut.symbol] ?? 1) * estimatedAmount; + const gas = +formatUnits(route.gas, tokens.mainnet.ETH.decimals); + return ( @@ -55,7 +66,7 @@ export function SwapRouteAccordionItem({ }, }), }} - onClick={() => onSelect(index)} + onClick={() => onSelect(route)} role="button" > theme.typography.pxToRem(24), width: (theme) => theme.typography.pxToRem(24), @@ -80,41 +91,33 @@ export function SwapRouteAccordionItem({ /> - {intl.formatNumber(route.quantity, quantityFormat)} + {intl.formatNumber(estimatedAmount, quantityFormat)}   - ({intl.formatNumber(route.value, currencyFormat)}) + ({intl.formatNumber(convertedAmount, currencyFormat)}) - {route.type === 'swap' - ? intl.formatMessage( - { defaultMessage: 'Swap via {name}' }, - { name: route.name }, - ) - : intl.formatMessage( - { defaultMessage: 'Swap for {name}' }, - { name: route.name }, - )} + {intl.formatMessage(routeActionLabel[route.action])} ({ '& p': { textAlign: { xs: 'left', md: 'right' } }, [theme.breakpoints.down('md')]: { display: 'flex', justifyContent: 'space-between', width: '100%', }, - }} + })} > - Rate  + {intl.formatMessage({ defaultMessage: 'Rate' })}    @@ -123,9 +126,9 @@ export function SwapRouteAccordionItem({ - Est gas:  + {intl.formatMessage({ defaultMessage: 'Est gas' })}  - ~{intl.formatNumber(route.transactionCost, currencyFormat)} + ~{intl.formatNumber(gas, currencyFormat)} diff --git a/libs/oeth/swap/src/components/SwapRouteCard.tsx b/libs/oeth/swap/src/components/SwapRouteCard.tsx index 9ed789bb4..24c08fa22 100644 --- a/libs/oeth/swap/src/components/SwapRouteCard.tsx +++ b/libs/oeth/swap/src/components/SwapRouteCard.tsx @@ -1,18 +1,38 @@ import { alpha, Box, Card, CardHeader, Stack, Typography } from '@mui/material'; -import { currencyFormat } from '@origin/shared/components'; +import { currencyFormat, quantityFormat } from '@origin/shared/components'; +import { tokens } from '@origin/shared/contracts'; +import { usePrices } from '@origin/shared/providers'; import { useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; -import type { Route } from './SwapRoute'; +import { routeActionLabel, routeActionLogos } from '../constants'; -interface Props { - index: number; - selected: number; - onSelect: (index: number) => void; - route: Route; -} +import type { EstimatedSwapRoute } from '../types'; + +export type SwapRouteCardProps = { + isSelected: boolean; + isBest: boolean; + onSelect: (route: EstimatedSwapRoute) => void; + route: EstimatedSwapRoute; +}; -export function SwapRouteCard({ index, selected, onSelect, route }: Props) { +export function SwapRouteCard({ + isSelected, + isBest, + onSelect, + route, +}: SwapRouteCardProps) { const intl = useIntl(); + const { data: prices } = usePrices(); + + const estimatedAmount = +formatUnits( + route.estimatedAmount, + route.tokenOut.decimals, + ); + const convertedAmount = + (prices?.[route.tokenOut.symbol] ?? 1) * estimatedAmount; + const gas = +formatUnits(route.gas, tokens.mainnet.ETH.decimals); + return ( `1px solid ${theme.palette.grey[800]}`, borderRadius: 1, - ...(selected === index + ...(isSelected ? { background: `linear-gradient(var(--mui-palette-grey-800), var(--mui-palette-grey-800)) padding-box, linear-gradient(90deg, var(--mui-palette-primary-main) 0%, var(--mui-palette-primary-dark) 100%) border-box;`, @@ -46,7 +66,7 @@ export function SwapRouteCard({ index, selected, onSelect, route }: Props) { }), }} role="button" - onClick={() => onSelect(index)} + onClick={() => onSelect(route)} > - {route.quantity}  + {intl.formatNumber(estimatedAmount, quantityFormat)}  - ({intl.formatNumber(route.value, currencyFormat)}) + ({intl.formatNumber(convertedAmount, currencyFormat)}) - {index === 0 ? ( + {isBest ? ( - ({intl.formatNumber(route.value, currencyFormat)}) + ({intl.formatNumber(estimatedAmount, quantityFormat)}) } @@ -128,7 +148,7 @@ export function SwapRouteCard({ index, selected, onSelect, route }: Props) { variant="body2" sx={{ marginBlock: { xs: 1.5, md: 1 } }} > - {route.name} + {intl.formatMessage(routeActionLabel[route.action])} - ~{intl.formatNumber(route.transactionCost, currencyFormat)} + ~{intl.formatNumber(gas, currencyFormat)} - {route.type === 'redeem' ? ( + {/*route.type === 'redeem' ? ( - {/* TODO better logic for coloring -> prob time should come as a ms duration and getting it formated */} + - ) : undefined} + ) : undefined */} ); diff --git a/libs/oeth/swap/src/constants.ts b/libs/oeth/swap/src/constants.ts index c28e6843f..c4a6366b2 100644 --- a/libs/oeth/swap/src/constants.ts +++ b/libs/oeth/swap/src/constants.ts @@ -1,7 +1,11 @@ import { tokens } from '@origin/shared/contracts'; +import { defineMessage } from 'react-intl'; import { erc20ABI, mainnet } from 'wagmi'; import type { Token } from '@origin/shared/contracts'; +import type { MessageDescriptor } from 'react-intl'; + +import type { SwapAction } from './types'; export const MIX_TOKEN: Token = { address: undefined, @@ -13,6 +17,26 @@ export const MIX_TOKEN: Token = { icon: '/images/backed-graphic.svg', }; +export const routeActionLogos: Record = { + 'mint-vault': '/images/protocols/origin.svg', + 'redeem-vault': '/images/protocols/origin.svg', + 'swap-curve': '/images/protocols/curve.webp', + 'swap-zapper-eth': '/images/protocols/zapper.svg', + 'swap-zapper-sfrxeth': '/images/protocols/zapper.svg', + 'unwrap-woeth': '/images/protocols/origin.svg', + 'wrap-oeth': '/images/protocols/origin.svg', +}; + +export const routeActionLabel: Record = { + 'mint-vault': defineMessage({ defaultMessage: 'Mint with Vault' }), + 'redeem-vault': defineMessage({ defaultMessage: 'Redeem with Vault' }), + 'swap-curve': defineMessage({ defaultMessage: 'Swap with Curve' }), + 'swap-zapper-eth': defineMessage({ defaultMessage: 'Swap with Zapper' }), + 'swap-zapper-sfrxeth': defineMessage({ defaultMessage: 'Swap with Zapper' }), + 'unwrap-woeth': defineMessage({ defaultMessage: 'Unwrap with Origin' }), + 'wrap-oeth': defineMessage({ defaultMessage: 'Wrap with Origin' }), +}; + export const swapRoutes = [ // Mint { diff --git a/libs/oeth/swap/src/hooks.ts b/libs/oeth/swap/src/hooks.ts index 81f0a9e47..3b19781fb 100644 --- a/libs/oeth/swap/src/hooks.ts +++ b/libs/oeth/swap/src/hooks.ts @@ -3,12 +3,13 @@ import { useCallback, useMemo } from 'react'; import { isNilOrEmpty } from '@origin/shared/utils'; import { produce } from 'immer'; +import { swapActions } from './actions'; import { useSwapState } from './state'; import { getAllAvailableTokens, getAvailableTokensForSource } from './utils'; import type { Token } from '@origin/shared/contracts'; -import type { TokenSource } from './types'; +import type { EstimatedSwapRoute, TokenSource } from './types'; export const useHandleAmountInChange = () => { const [, setSwapState] = useSwapState(); @@ -21,6 +22,7 @@ export const useHandleAmountInChange = () => { state.amountOut = 0n; state.isAmountOutLoading = true; state.isPriceOutLoading = true; + state.isSwapRoutesLoading = true; }), ); }, @@ -123,12 +125,35 @@ export const useHandleTokenFlip = () => { }, [setSwapState]); }; +export const useHandleSelectSwapRoute = () => { + const [, setSwapState] = useSwapState(); + + return useCallback( + (route: EstimatedSwapRoute) => { + setSwapState( + produce((draft) => { + draft.selectedSwapRoute = route; + draft.amountOut = route.estimatedAmount; + }), + ); + }, + [setSwapState], + ); +}; + export const useHandleSwap = () => { - const [{ swapRoutes }] = useSwapState(); + const [{ tokenIn, tokenOut, amountIn, selectedSwapRoute }] = useSwapState(); return useCallback(async () => { - if (isNilOrEmpty(swapRoutes)) { + if (isNilOrEmpty(selectedSwapRoute)) { return; } - }, [swapRoutes]); + + await swapActions[selectedSwapRoute.action].swap( + tokenIn, + tokenOut, + amountIn, + selectedSwapRoute, + ); + }, [amountIn, selectedSwapRoute, tokenIn, tokenOut]); }; diff --git a/libs/oeth/swap/src/state.ts b/libs/oeth/swap/src/state.ts index 7a5f356c1..d28e86c10 100644 --- a/libs/oeth/swap/src/state.ts +++ b/libs/oeth/swap/src/state.ts @@ -23,10 +23,16 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = isBalanceOutLoading: false, slippage: 0.01, swapRoutes: [], + selectedSwapRoute: null, + isSwapRoutesLoading: false, }); useDebouncedEffect( async () => { + if (state.amountIn === 0n) { + return; + } + const routes = await Promise.all( getAvailableRoutes(state.tokenIn, state.tokenOut).map((route) => queryClient.fetchQuery({ @@ -36,13 +42,17 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = state.tokenOut, route.action, state.amountIn.toString(), + state.slippage, ] as const, - queryFn: async ({ queryKey: [, tokenIn, tokenOut, action] }) => + queryFn: async ({ + queryKey: [, tokenIn, tokenOut, action, , slippage], + }) => swapActions[action].estimateRoute( tokenIn, tokenOut, state.amountIn, route, + slippage, ), }), ), @@ -59,9 +69,11 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = setState( produce((draft) => { 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 e06265700..e3ae62878 100644 --- a/libs/oeth/swap/src/types.ts +++ b/libs/oeth/swap/src/types.ts @@ -21,6 +21,7 @@ export type EstimateGas = ( tokenIn: Token, tokenOut: Token, amountIn: bigint, + slippage: number, ) => Promise; export type EstimateRoute = ( @@ -28,6 +29,7 @@ export type EstimateRoute = ( tokenOut: Token, amountIn: bigint, route: SwapRoute, + slippage: number, ) => Promise; export type Swap = ( @@ -66,4 +68,6 @@ export type SwapState = { isBalanceOutLoading: boolean; slippage: number; swapRoutes: EstimatedSwapRoute[]; + selectedSwapRoute: EstimatedSwapRoute | null; + isSwapRoutesLoading: boolean; }; diff --git a/libs/oeth/swap/src/utils.ts b/libs/oeth/swap/src/utils.ts index 6295e8c56..a8ec554ec 100644 --- a/libs/oeth/swap/src/utils.ts +++ b/libs/oeth/swap/src/utils.ts @@ -5,7 +5,7 @@ import { swapRoutes } from './constants'; import type { Token } from '@origin/shared/contracts'; -import type { TokenSource } from './types'; +import type { SwapRoute, TokenSource } from './types'; export const getAllAvailableTokens = (source: TokenSource) => { if (isNilOrEmpty(source)) { @@ -49,3 +49,15 @@ export const getAvailableRoutes = (tokenIn: Token, tokenOut: Token) => { r.tokenOut.symbol === tokenOut.symbol, ); }; + +export const routeEq = (a: SwapRoute, b: SwapRoute) => { + if (isNilOrEmpty(a) || isNilOrEmpty(b)) { + return false; + } + + return ( + a.tokenIn.symbol === b.tokenIn.symbol && + a.tokenOut.symbol === b.tokenOut.symbol && + a.action === b.action + ); +}; diff --git a/libs/oeth/swap/src/views/SwapView.tsx b/libs/oeth/swap/src/views/SwapView.tsx index d00bb6926..1094945b7 100644 --- a/libs/oeth/swap/src/views/SwapView.tsx +++ b/libs/oeth/swap/src/views/SwapView.tsx @@ -59,10 +59,12 @@ function SwapViewWrapped() { const { data: balTokenIn, isLoading: isBalTokenInLoading } = useBalance({ address, token: tokenIn.address, + watch: true, }); const { data: balTokenOut, isLoading: isBalTokenOutLoading } = useBalance({ address, token: tokenOut.address, + watch: true, }); const handleAmountInChange = useHandleAmountInChange(); const handleTokenChange = useHandleTokenChange(); @@ -175,7 +177,7 @@ function SwapViewWrapped() { /> - + {intl.formatMessage({ defaultMessage: 'Swap' })}