diff --git a/src/App.tsx b/src/App.tsx index 0cfb466..fe8444c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,8 @@ import { Toaster } from './components/ui/toaster' import { TooltipProvider } from './components/ui/tooltip' import YieldPage from './pages/yield' import { OutputAmountProvider } from './store/OutputAmountContext' +import Allocate from './pages/allocate' +import Portfolio from './pages/portfolio' // todo: regenerate this and put in env const projectId = '42c7317ebec6e24c881a534d1d6b3ba0' @@ -60,6 +62,10 @@ const router = createBrowserRouter([ children: [ { path: '/', + element: , + }, + { + path: '/market', element: , }, { @@ -74,6 +80,18 @@ const router = createBrowserRouter([ path: '/yield', element: , }, + { + path: '/allocate', + element: , + }, + { + path: '/portfolio', + element: , + }, + { + path: '/portfolio/:id', + element: , + }, ], }, ]) diff --git a/src/actions/AllocateAction/index.tsx b/src/actions/AllocateAction/index.tsx new file mode 100644 index 0000000..d856051 --- /dev/null +++ b/src/actions/AllocateAction/index.tsx @@ -0,0 +1,293 @@ +import React, { useEffect } from 'react' +import { useAccount, useReadContract } from 'wagmi' +import { erc20Abi, getAddress, parseAbi, zeroAddress } from 'viem' +import { FetchStatus } from '@tanstack/react-query' + +import { useGraphQL } from '../../useGraphQL' +import { MarketInfoQueryDocument } from '../../queries/markets' +import TransactionButton from '@/components/TransactionButton' +import { useTransactionStatus } from '@/components/TransactionButton/useTransactionStatus' +import { rmmABI } from '@/lib/abis/rmm' +import { useTradeRoute } from '@/lib/useTradeRoute' +import { formatNumber, fromWad, toWad } from '@/utils/numbers' +import { ETH_ADDRESS, useTokens } from '@/lib/useTokens' +import { Skeleton } from '@/components/ui/skeleton' + +import ApproveAction from '../ApproveAction' +import { + useSwapArgsSyAndPt, + useSwapExactTokenForYtArgs, +} from '@/lib/useSwapArgs' +import { useMintSyArgs } from '@/lib/useMintSyArgs' +import { useLiquidityManagerArgs } from '@/lib/useLiquidityManagerArgs' +import { liquidityManager } from '@/data/contracts' + +type TokenUniverse = { + native: `0x${string}` + wrapped: `0x${string}` + ib: `0x${string}` + sy: `0x${string}` + pt: `0x${string}` + yt: `0x${string}` + market: `0x${string}` +} + +enum TradeRoutes { + INVALID = 'INVALID', + ETH_TO_SY = 'ETH_TO_SY', + SY_TO_ETH = 'SY_TO_ETH', + SY_TO_PT = 'SY_TO_PT', + PT_TO_SY = 'PT_TO_SY', + SY_TO_YT = 'SY_TO_YT', + YT_TO_SY = 'YT_TO_SY', + ETH_TO_YT = 'ETH_TO_YT', + YT_TO_ETH = 'YT_TO_ETH', + SY_TO_LPT = 'SY_TO_LPT', + PT_TO_LPT = 'PT_TO_LPT', +} + +function getTradeRoute( + universe: TokenUniverse, + tokenIn: string, + tokenOut: string +): TradeRoutes { + if (tokenIn === universe.native && tokenOut === universe.sy) { + return TradeRoutes.ETH_TO_SY + } else if (tokenIn === universe.sy && tokenOut === universe.native) { + return TradeRoutes.SY_TO_ETH + } else if (tokenIn === universe.sy && tokenOut === universe.pt) { + return TradeRoutes.SY_TO_PT + } else if (tokenIn === universe.pt && tokenOut === universe.sy) { + return TradeRoutes.PT_TO_SY + } else if ( + tokenIn === universe.native && + tokenOut && + universe.yt && + getAddress(tokenOut) === getAddress(universe.yt) + ) { + return TradeRoutes.ETH_TO_YT + } else if ( + tokenOut && + universe.yt && + getAddress(tokenOut) === getAddress(universe.yt) + ) { + return TradeRoutes.SY_TO_YT + } else if (tokenIn === universe.yt) { + return TradeRoutes.YT_TO_SY + } else if (tokenIn == universe.pt && tokenOut == universe.market) { + return TradeRoutes.PT_TO_LPT + } else if (tokenIn == universe.sy && tokenOut == universe.market) { + return TradeRoutes.SY_TO_LPT + } else { + return TradeRoutes.INVALID + } +} + +/** + * Wrapper for TransactionButton component to be used for Allocating. + */ +const AllocateAction: React.FC<{ + marketRoute: `0x${string}` + amountIn: string + setAmountOut: (amount: string) => void + setTokenOutFetching: (status: FetchStatus) => void +}> = ({ marketRoute, amountIn, setAmountOut, setTokenOutFetching }) => { + // Transaction related info. + const { address } = useAccount() + const { txHash, setTxHash, txReceipt } = useTransactionStatus({}) + + // Get the market data. + const { data: marketData } = useGraphQL(MarketInfoQueryDocument, { + id: marketRoute, + }) + const market = marketData?.markets?.items?.[0] + + // Get the token data using the query params. + const { + data: { sorted: sortedTokens }, + } = useTokens({}) + const { getTokenIn, getTokenOut } = useTradeRoute() + const tokenIn = getTokenIn() + const tokenOut = getTokenOut() + const tokenInMeta = + tokenIn && + sortedTokens?.find((tkn) => getAddress(tkn.id) === getAddress(tokenIn)) + const tokenOutMeta = + tokenOut && + sortedTokens?.find((tkn) => getAddress(tkn.id) === getAddress(tokenOut)) + + // todo: make sure tokens in ponder match these universe ones. + const universe = { + native: ETH_ADDRESS as `0x${string}`, + wrapped: market?.wrappedNativeAssetId as `0x${string}`, + ib: market?.ibAssetId as `0x${string}`, + sy: market?.syId as `0x${string}`, + pt: market?.ptId as `0x${string}`, + yt: market?.ytId as `0x${string}`, + market: market?.id as `0x${string}`, + } + + // --- Prepare the action --- // + + const tradeRoute = getTradeRoute( + universe, + tokenIn as `0x${string}`, + tokenOut as `0x${string}` + ) + console.log({ tradeRoute }) + + // Prepare the input amount. + const preparedIn = + !isNaN(parseFloat(amountIn)) && toWad(parseFloat(amountIn)) + + // Check the allowance for the desired tokenIn. + const { data: allowance, status: allowanceStatus } = useReadContract({ + abi: erc20Abi, + functionName: 'allowance', + address: tokenIn as `0x${string}`, + args: [address as `0x${string}`, liquidityManager as `0x${string}`], + }) + + const approved = + (allowance && preparedIn && allowance >= preparedIn) || + tokenIn === ETH_ADDRESS + + const { data: storedIndex, failureReason: storedIndexFailure } = + useReadContract({ + abi: parseAbi([ + 'function pyIndexStored() external view returns(uint256)', + ] as const), + address: universe.yt, + functionName: 'pyIndexStored', + }) + + const isZapSy = tradeRoute === TradeRoutes.SY_TO_LPT + + const { amountOut, status, fetchStatus, payload } = useLiquidityManagerArgs( + { + marketId: universe.market, + liquidityManagerAddress: liquidityManager, + pyIndexStored: storedIndex as bigint, + reserveX: market?.pool?.reserveX as bigint, + reserveY: market?.pool?.reserveY as bigint, + isZapSy, + amountIn, + enabled: approved, + } + ) + + console.log({ payload }) + const estimatedAmountOut = amountOut + + useEffect(() => { + setTokenOutFetching(fetchStatus) + }, [fetchStatus, setTokenOutFetching]) + + useEffect(() => { + if (estimatedAmountOut) { + setAmountOut(fromWad(estimatedAmountOut).toString()) + } + + if (!amountIn) { + setAmountOut('') + } + }, [amountIn, estimatedAmountOut, setAmountOut]) + + let action = null + if (tokenIn === null || tokenOut === null || !preparedIn) { + action = + } else if (!approved) { + action = ( + + ) + } else { + switch (tradeRoute) { + case TradeRoutes.SY_TO_LPT: + action = + break + case TradeRoutes.PT_TO_LPT: + action = + break + default: + action = + break + } + } + + return ( +
+
+

Submit

+
+
+
+

Review Transaction

+ {status === 'error' ? ( +

Error during preparing.

+ ) : amountIn ? ( +
+ {!approved ? ( + <> +

+ Approving{' '} +

+

+ {formatNumber(parseFloat(amountIn))}{' '} +

+

+ {tokenInMeta?.symbol}. +

+ + ) : ( + <> + {' '} +

+ Allocating{' '} +

+

+ {formatNumber(parseFloat(amountIn))}{' '} +

+

+ {tokenInMeta?.symbol} for{' '} +

+ {fetchStatus === 'fetching' || + !estimatedAmountOut ? ( + +

+ 100.00 +

+
+ ) : ( +

+ {formatNumber( + fromWad(estimatedAmountOut) + )} +

+ )}{' '} +

+ {tokenOutMeta?.symbol}. +

+ + )} +
+ ) : ( +

+ Select token to zap add liquidity. +

+ )} +
+ + {action} +
+
+ ) +} + +export default AllocateAction diff --git a/src/components/AccountHoldings/index.tsx b/src/components/AccountHoldings/index.tsx index 48d97f2..4467fb1 100644 --- a/src/components/AccountHoldings/index.tsx +++ b/src/components/AccountHoldings/index.tsx @@ -114,7 +114,7 @@ const AccountInfo = (): JSX.Element => {

Portfolio

-
+
{address ? ( <> {address && (!isConnecting || !isReconnecting) ? ( @@ -239,7 +239,7 @@ export const TokenBalance = ({ ) } -const Holdings = (): JSX.Element => { +const Holdings = ({ columns = 3 }: { columns: number }): JSX.Element => { const { address, isConnecting, isReconnecting } = useAccount() const { data: { sorted: sortedTokens }, @@ -313,7 +313,9 @@ const Holdings = (): JSX.Element => {
-
+
{!ethBalance ? ( new Array(1) .fill(null) @@ -367,7 +369,7 @@ const AccountHoldings: React.FC = (): JSX.Element => {
- +
) diff --git a/src/components/AddLiquidityView/index.tsx b/src/components/AddLiquidityView/index.tsx new file mode 100644 index 0000000..d1e3c42 --- /dev/null +++ b/src/components/AddLiquidityView/index.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { useAccount } from 'wagmi' + +import { useMarketRoute } from '@/lib/useMarketRoute' + +import AllocateAction from '@/actions/AllocateAction' +import { + ConnectToTrade, + Summary, + SwapWidget, + useTokenFormState, +} from '../TradeView' + +const AddLiquidityView = (): JSX.Element => { + const { isConnected } = useAccount() + + const { id } = useMarketRoute() + const tokenInForm = useTokenFormState() + const tokenOutForm = useTokenFormState('', true) + + const [isSwapView, setIsSwapView] = React.useState(false) + + return ( +
+ + {isConnected ? ( + <> + + + + ) : ( + + )} +
+ ) +} + +export default AddLiquidityView diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 6976ecd..5ccec1d 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -8,13 +8,13 @@ import TradeView from '../TradeView' const Layout: React.FC<{ children: React.ReactElement }> = ({ children }) => { return (
-
+ {/*
-
+
*/} {children} -
+ {/*
-
+
*/}
) } diff --git a/src/components/MarketView/index.tsx b/src/components/MarketView/index.tsx index 76eec5d..91433b0 100644 --- a/src/components/MarketView/index.tsx +++ b/src/components/MarketView/index.tsx @@ -89,7 +89,30 @@ const MarketView = (): JSX.Element => { const syBalance = balances?.[syBalanceIndex]?.result as bigint return ( -
+
+
+
+ + + +
+
+
@@ -120,6 +143,7 @@ const MarketView = (): JSX.Element => {
+
{ />
- -
-
-
-

- Market -

-

{market?.name ?? }

-
-
-
- - {market?.name ?? } - -
-

- Description -

- - {market?.name ? ( -

- This market tokenizes the individual - yield and principal components of the - yield-bearing asset stETH. Once - expiry is reached, the yield tokens do - not accrue more yield. -

- ) : ( - - )} -
-
-
-
- -
-
-
-

- Actions -

- - - - - - Convert the base asset (e.g. ETH, stETH) - into the composite token, Standardized - Yield, then trade it for either of the - component tokens, Yield or Principal. - - -
-
-
-
- - - - - - - - Coming soon - - - {address && - (balances?.[0]?.result as bigint) > 0 && ( - - )} -
-
-
-
- -
-
- - +
+ {address && ( + +
+ } /> -
-
- {address && ( - - -
- } - /> - )} -
+ )}
) diff --git a/src/components/Root/index.tsx b/src/components/Root/index.tsx index 29fa20b..fe73241 100644 --- a/src/components/Root/index.tsx +++ b/src/components/Root/index.tsx @@ -124,17 +124,7 @@ function Root(): JSX.Element {
  • - -
  • - -
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
diff --git a/src/components/TradeView/index.tsx b/src/components/TradeView/index.tsx index 3965466..8f41f27 100644 --- a/src/components/TradeView/index.tsx +++ b/src/components/TradeView/index.tsx @@ -2,12 +2,9 @@ import React, { useEffect } from 'react' import { useAccount } from 'wagmi' import { ArrowRightIcon, CheckIcon } from '@radix-ui/react-icons' import { getAddress } from 'viem' -import { useLocation, useNavigate, useParams } from 'react-router-dom' +import { useLocation } from 'react-router-dom' import { FetchStatus } from '@tanstack/react-query' -import { useGraphQL } from '../../useGraphQL' -import { MarketInfoQueryDocument } from '../../queries/markets' - import { EtherscanLink, LabelWithEtherscan } from '../EtherscanLinkLabels' import TokenSelector from '../TokenSelector' import { useTokens } from '@/lib/useTokens' @@ -16,7 +13,7 @@ import SwapAction from '@/actions/SwapAction' import { useMarketRoute } from '@/lib/useMarketRoute' import useSlippagePreference from '@/lib/useSlippagePreference' import { useOutputAmount } from '@/lib/useOutputAmount' -import { FALLBACK_MARKET_ADDRESS, shortAddress } from '@/utils/address' + import { formatNumber, formatPercentage } from '@/utils/numbers' import { Input } from '../ui/input' @@ -89,10 +86,17 @@ const TokenInput: React.FC = ({ /** * Fairly dumb component for selecting tokens and inputting values. */ -const SwapWidget: React.FC<{ +export const SwapWidget: React.FC<{ tokenInForm: TokenForm tokenOutForm: TokenForm -}> = ({ tokenInForm, tokenOutForm }): JSX.Element => { + isSwapView: boolean + setIsSwapView: (isSwapView: boolean) => void +}> = ({ + tokenInForm, + tokenOutForm, + isSwapView, + setIsSwapView, +}): JSX.Element => { const loc = useLocation() const { getTokenIn, getTokenOut, setTokenParams } = useTradeRoute() const tokenIn = getTokenIn() @@ -154,7 +158,9 @@ const SwapWidget: React.FC<{ return (
-

Trade

+
+

{isSwapView ? 'Swap' : 'Allocate'}

+