diff --git a/components/ErrorComponent.tsx b/components/ErrorComponent.tsx new file mode 100644 index 0000000..8b3ea97 --- /dev/null +++ b/components/ErrorComponent.tsx @@ -0,0 +1,24 @@ +import { Alert, Box, Button, useTheme } from "@mui/material"; +import Link from "next/link"; + +export default function ErrorComponent({ error }: { error: string[] }) { + const theme = useTheme(); + + return ( + + {error.map((e: string) => ( + + {e} + + ))} + + + ); +} diff --git a/components/Layout.tsx b/components/Layout.tsx new file mode 100644 index 0000000..1070e22 --- /dev/null +++ b/components/Layout.tsx @@ -0,0 +1,12 @@ +import { Container } from "@mui/material"; +import NavBar from "./NavBar"; +import { NextComponentType, NextPageContext } from "next"; + +export default function Layout({ children }: {children: React.ReactNode}) { + return ( + <> + + {children} + + ); +} diff --git a/components/NavBar.tsx b/components/NavBar.tsx new file mode 100644 index 0000000..1b4522a --- /dev/null +++ b/components/NavBar.tsx @@ -0,0 +1,126 @@ +import * as React from "react"; +import AppBar from "@mui/material/AppBar"; +import Box from "@mui/material/Box"; +import Toolbar from "@mui/material/Toolbar"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import Menu from "@mui/material/Menu"; +import MenuIcon from "@mui/icons-material/Menu"; +import Container from "@mui/material/Container"; +import Avatar from "@mui/material/Avatar"; +import Button from "@mui/material/Button"; +import Tooltip from "@mui/material/Tooltip"; +import MenuItem from "@mui/material/MenuItem"; +import AdbIcon from "@mui/icons-material/Adb"; +import Link from "next/link"; + +const pages: string[] = []; + +function NavBar() { + const [anchorElNav, setAnchorElNav] = React.useState( + null + ); + + const handleOpenNavMenu = (event: React.MouseEvent) => { + setAnchorElNav(event.currentTarget); + }; + + const handleCloseNavMenu = () => { + setAnchorElNav(null); + }; + + return ( + + + + + + EBTC-MONITOR + + + + + + + + {pages.map((page) => ( + + {page} + + ))} + + + + + EBTC-MONITOR + + + {pages.map((page) => ( + + ))} + + + + + ); +} +export default NavBar; diff --git a/components/PoolChecker.tsx b/components/PoolChecker.tsx index 1e6bb47..3ae20e8 100644 --- a/components/PoolChecker.tsx +++ b/components/PoolChecker.tsx @@ -1,33 +1,63 @@ import { useCallback, useMemo, useState } from "react"; import getPoolData from "../lib/fetcher"; -import computeLiquidationData, { LiquidationData, PoolData } from "@/lib/liquidation"; -import styles from "../styles/Home.module.css" +import computeLiquidationData, { + LiquidationData, + PoolData, +} from "@/lib/liquidation"; +import styles from "../styles/Home.module.css"; +import { + Box, + Button, + Card, + CardContent, + Container, + Grid, + InputLabel, + OutlinedInput, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useTheme, +} from "@mui/material"; +import { toast } from "react-toastify"; const CURVE_TYPE = "Curve"; const VELO_TYPE = "Velo"; const BAL_TYPE = "Balancer"; -const PoolChecker: React.FC = ({ -}) => { - - +const PoolChecker: React.FC = ({}) => { + const theme = useTheme(); const [poolType, setPoolType] = useState(BAL_TYPE); const [isLoadingPool, setIsLoadingPool] = useState(false); - const [poolAddress, setPoolAddress] = useState(""); - const [balancerPoolId, setBalancerPoolId] = useState(""); + const [poolAddress, setPoolAddress] = useState( + "0x32296969ef14eb0c6d29669c550d4a0449130230" + ); + const [balancerPoolId, setBalancerPoolId] = useState( + "0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080" + ); const [dataDump, setDatadump] = useState(""); - const [poolData, setPoolData] = useState(null) + const [poolData, setPoolData] = useState(null); - const [toSell, setToSell] = useState(1e21) - const [liquidationPremium, setLiquidationPremium] = useState(300) - const [secondsForPoolRefill, setSecondsForPoolRefill] = useState(60 * 60) + const [toSell, setToSell] = useState(1e21); + const [liquidationPremium, setLiquidationPremium] = useState(300); + const [secondsForPoolRefill, setSecondsForPoolRefill] = useState(60 * 60); const setPoolDataFromFetch = useCallback(async () => { + if (!poolAddress || !balancerPoolId) { + toast.error("Please provide Pool address and Pool Id"); + return false; + } + setIsLoadingPool(true); try { console.log("*******"); const res = await getPoolData(poolType, poolAddress, balancerPoolId); console.log("************ setPoolDataFromFetch", res); - setPoolData(res) + setPoolData(res); setDatadump(JSON.stringify(res)); } catch (e) { console.log("Exception setPoolDataFromFetch", e); @@ -39,14 +69,305 @@ const PoolChecker: React.FC = ({ const data: LiquidationData | null = useMemo( // We know it's defined () => - // @ts-ignore - poolData != null ? computeLiquidationData(toSell, liquidationPremium, 18, {...poolData, timeForReplenishment: secondsForPoolRefill}) : null, + // @ts-ignore + poolData != null + ? computeLiquidationData(toSell, liquidationPremium, 18, { + ...poolData, + timeForReplenishment: secondsForPoolRefill, + }) + : null, [toSell, liquidationPremium, 18, poolData, secondsForPoolRefill] ); + + const toTitleCase = (text: string) => { + const result = text.replace(/([A-Z])/g, " $1"); + const finalResult = result.charAt(0).toUpperCase() + result.slice(1); + return finalResult; + }; + + const parseValue = (attribute: string, data: unknown) => { + switch (attribute) { + case "poolType": + return toTitleCase(JSON.parse(JSON.stringify(String(data)))); + case "isStable": + const val = Boolean(JSON.parse(JSON.stringify(String(data)))); + return val ? "Yes" : "No"; + default: + return JSON.stringify(data); + } + }; + return ( -
+ <> + + + + Get Balancer Pool Data + + + Pool Address + setPoolAddress(e.target.value)} + value={poolAddress} + /> + + + + Balancer Pool Id + setBalancerPoolId(e.target.value)} + value={balancerPoolId} + /> + + + + + + {dataDump && ( + <> + + + Pool Details + + + + + + Attribute + Details + + + + {Object.keys(JSON.parse(dataDump)).map((key) => ( + + + {toTitleCase(key)} + + + {parseValue(key, JSON.parse(dataDump)[key])} + + + ))} + +
+
+
+ + {/* Liquidity to Allow */} + + + + Liquidity to Allow + + setToSell(parseInt(e.target.value, 10))} + value={toSell} + size="small" + style={{ + flex: 1, + marginLeft: theme.spacing(2), + maxWidth: theme.spacing(50), + }} + /> + + {/* Liquidity to Allow */} + + {/* In Eth */} + + In Eth + + + setToSell(parseInt(e.target.value) * 1e18)} + size="small" + style={{ + flex: 1, + maxWidth: theme.spacing(50), + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + }} + /> + + + + + + Selling {toSell} + + {/* In Eth */} + + {/* Liquidation Premium */} + + + Liquidation Premium + + + + + setLiquidationPremium(parseInt(e.target.value, 10)) + } + size="small" + style={{ + flex: 1, + maxWidth: theme.spacing(50), + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + }} + /> + + + + + Selling {toSell} + + {/* Liquidation Premium */} + + {/* Seconds For Pool Replenishment */} + + + Seconds For Pool Replenishment + + + + + setSecondsForPoolRefill(parseInt(e.target.value)) + } + size="small" + style={{ + flex: 1, + maxWidth: theme.spacing(50), + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + }} + /> + + + + + Seconds for Pool Refill {secondsForPoolRefill} + + {/* Seconds For Pool Replenishment */} + + + + {JSON.stringify(data)} + + + + + )} +
-
+
+ {/*

Get Balancer Pool Data

Pool Address @@ -73,49 +394,96 @@ const PoolChecker: React.FC = ({ > {isLoadingPool ? "Loading Pool" : "Load Pool"} -

- - {dataDump &&
{dataDump}
} -
- - {dataDump && -
-
-

- Liquidity to Allow - setToSell(parseInt(e.target.value, 10))} /> -

-

- In Eth - - setToSell(parseInt(e.target.value) * 1e18)} /> - - -

Selling {toSell}
-

-

- Liquidation Premium - - setLiquidationPremium(parseInt(e.target.value, 10))} /> - -

Premium {liquidationPremium}
-

-

- Seconds For Pool Replenishment - - setSecondsForPoolRefill(parseInt(e.target.value))} /> - -

Seconds for Pool Refill {secondsForPoolRefill}
-

-
-
- {JSON.stringify(data)} -
-
- } +
*/} + {/* {dataDump &&
{dataDump}
} */} +
+ {dataDump && ( +
+
+ {/*

+ Liquidity to Allow + setToSell(parseInt(e.target.value, 10))} + /> +

+

+ In Eth + + setToSell(parseInt(e.target.value) * 1e18)} + /> + + +

Selling {toSell}
+

+

+ Liquidation Premium + + + setLiquidationPremium(parseInt(e.target.value, 10)) + } + /> + +

Premium {liquidationPremium}
+

*/} + {/*

+ Seconds For Pool Replenishment + + + setSecondsForPoolRefill(parseInt(e.target.value)) + } + /> + +

+ Seconds for Pool Refill {secondsForPoolRefill} +
+

*/} +
+ {/*
{JSON.stringify(data)}
*/} +
+ )}
+ ); }; diff --git a/components/PoolCheckerForm.tsx b/components/PoolCheckerForm.tsx new file mode 100644 index 0000000..13a56b4 --- /dev/null +++ b/components/PoolCheckerForm.tsx @@ -0,0 +1,61 @@ +import { + Card, + CardContent, + Typography, + Box, + InputLabel, + OutlinedInput, + Button, + useTheme, +} from "@mui/material"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { toast } from "react-toastify"; + +export default function PoolCheckerForm() { + const theme = useTheme(); + const router = useRouter(); + + const [poolAddress, setPoolAddress] = useState(""); + const [balancerPoolId, setBalancerPoolId] = useState(""); + + const handlePoolCheckerClick = () => { + if (!poolAddress || !balancerPoolId) { + toast.error("Please provide Pool address and Pool Id"); + return false; + } + router.push(`/pool-details/${poolAddress}/${balancerPoolId}`); + }; + + return ( + + + + Get Balancer Pool Data + + + Pool Address + setPoolAddress(e.target.value)} + value={poolAddress} + /> + + + + Balancer Pool Id + setBalancerPoolId(e.target.value)} + value={balancerPoolId} + /> + + + + + ); +} diff --git a/components/PoolDetailsComponent.tsx b/components/PoolDetailsComponent.tsx new file mode 100644 index 0000000..08d456e --- /dev/null +++ b/components/PoolDetailsComponent.tsx @@ -0,0 +1,348 @@ +import computeLiquidationData, { + LiquidationData, + PoolData, +} from "@/lib/liquidation"; +import { DataDumpType } from "@/pages/pool-details/[...pool]"; +import { isBigNumber, isObject } from "@/utility"; +import { convertBigNumberToNormal, convertToScientificNotation } from "@/utility/number"; +import { toTitleCase } from "@/utility/string"; +import { + Box, + Typography, + TableContainer, + Card, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + InputLabel, + OutlinedInput, + Button, + Paper, + useTheme, + Grid, + Switch, + FormControlLabel, + Chip, +} from "@mui/material"; +import { useMemo, useState } from "react"; + +export default function PoolDetailsComponent({ dataDump }: DataDumpType) { + const theme = useTheme(); + + const [poolDetailsSwitch, setPoolDetailsSwitch] = useState(false); + const [liquidationDataSwitch, setLiquidationDataSwitch] = useState(false); + + const [toSell, setToSell] = useState(1e21); + const [liquidationPremium, setLiquidationPremium] = useState(300); + const [secondsForPoolRefill, setSecondsForPoolRefill] = useState(60 * 60); + + const liquidationData: LiquidationData | null = useMemo( + // We know it's defined + () => + // @ts-ignore + dataDump != null + ? computeLiquidationData(toSell, liquidationPremium, 18, { + ...dataDump, + timeForReplenishment: secondsForPoolRefill, + }) + : null, + [toSell, liquidationPremium, 18, dataDump, secondsForPoolRefill] + ); + + const renderData = (data: any) => { + if (isObject(data)) { + return ( + + + + + Attribute + Details + + + + {Object.keys(data).map((key) => { + return ( + + + {toTitleCase(key)} + + + {isObject(data[key]) && !isBigNumber(data[key]) + ? renderData(data[key]) + : isBigNumber(data[key]) + ? `${convertToScientificNotation(convertBigNumberToNormal(data[key]))} ETH` + : Array.isArray(data[key]) + ? data[key].map((val: any, index: number) => { + return isBigNumber(val) ? ( + + ) : ( + + ); + }) + : JSON.stringify(data[key])} + + + ); + })} + +
+
+ ); + } + return <>; + }; + + return ( + <> + + + + + Pool Details + + + + + setPoolDetailsSwitch(event.target.checked) + } + /> + } + label={poolDetailsSwitch ? "JSON View" : "Table View"} + /> + + + {!poolDetailsSwitch ? ( + renderData(dataDump) + ) : ( + + {JSON.stringify(dataDump)} + + )} + + + {/* Liquidity to Allow */} + + + Liquidity to Allow + setToSell(parseInt(e.target.value, 10))} + value={toSell} + size="small" + style={{ + flex: 1, + marginLeft: theme.spacing(2), + maxWidth: theme.spacing(50), + }} + /> + + {/* Liquidity to Allow */} + + {/* In Eth */} + + In Eth + + + setToSell(parseInt(e.target.value) * 1e18)} + size="small" + style={{ + flex: 1, + maxWidth: theme.spacing(50), + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + }} + /> + + + + + + Selling {toSell} + + {/* In Eth */} + + {/* Liquidation Premium */} + + + Liquidation Premium + + + + + setLiquidationPremium(parseInt(e.target.value, 10)) + } + size="small" + style={{ + flex: 1, + maxWidth: theme.spacing(50), + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + }} + /> + + + + + Selling {toSell} + + {/* Liquidation Premium */} + + {/* Seconds For Pool Replenishment */} + + + Seconds For Pool Replenishment + + + + + setSecondsForPoolRefill(parseInt(e.target.value)) + } + size="small" + style={{ + flex: 1, + maxWidth: theme.spacing(50), + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + }} + /> + + + + + Seconds for Pool Refill {secondsForPoolRefill} + + {/* Seconds For Pool Replenishment */} + + + + + + Liquidation Data + + + + + setLiquidationDataSwitch(event.target.checked) + } + /> + } + label={liquidationDataSwitch ? "JSON View" : "Table View"} + /> + + + + {!liquidationDataSwitch ? ( + renderData(liquidationData) + ) : ( + + {JSON.stringify(liquidationData)} + + )} + + + + ); +} diff --git a/lib/fetcher.ts b/lib/fetcher.ts index 2022c0d..33622b1 100644 --- a/lib/fetcher.ts +++ b/lib/fetcher.ts @@ -11,7 +11,6 @@ const getPoolData = async (type: string, address: string, poolId = ""): Promise< address, poolId !== "" ? { poolId } : undefined ); - return data; }; diff --git a/package.json b/package.json index ca96aaf..8b92deb 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,12 @@ "lint": "next lint" }, "dependencies": { + "@emotion/react": "^11.11.0", + "@emotion/server": "^11.11.0", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.2", + "@mui/icons-material": "^5.11.16", + "@mui/material": "^5.13.3", "@types/node": "20.2.3", "@types/react": "18.2.6", "@types/react-dom": "18.2.4", @@ -16,8 +22,10 @@ "eslint-config-next": "13.4.3", "next": "13.4.3", "pool-stat-fetcher": "^1.0.8", + "nextjs-progressbar": "^0.0.16", "react": "18.2.0", "react-dom": "18.2.0", + "react-toastify": "^9.1.3", "ts-amm-pool-math": "^1.1.3", "typescript": "5.0.4" } diff --git a/pages/_app.tsx b/pages/_app.tsx index 021681f..60f89a0 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,40 @@ -import '@/styles/globals.css' -import type { AppProps } from 'next/app' +import * as React from "react"; +import Head from "next/head"; +import { AppProps } from "next/app"; +import { ThemeProvider } from "@mui/material/styles"; +import CssBaseline from "@mui/material/CssBaseline"; +import { CacheProvider, EmotionCache } from "@emotion/react"; +import createEmotionCache from "@/utility/createEmotionCache"; +import darkTheme from "@/styles/theme/darkTheam"; +import { ToastContainer } from "react-toastify"; +import NextNProgress from "nextjs-progressbar"; +import "react-toastify/dist/ReactToastify.css"; +import "nprogress/nprogress.css"; +import Layout from "@/components/Layout"; -export default function App({ Component, pageProps }: AppProps) { - return +// Client-side cache, shared for the whole session of the user in the browser. +const clientSideEmotionCache = createEmotionCache(); + +export interface MyAppProps extends AppProps { + emotionCache?: EmotionCache; +} + +export default function MyApp(props: MyAppProps) { + const { Component, emotionCache = clientSideEmotionCache, pageProps } = props; + return ( + + + + + + {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} + + + + + + + + + ); } diff --git a/pages/_document.tsx b/pages/_document.tsx index 54e8bf3..5edbe68 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,13 +1,95 @@ -import { Html, Head, Main, NextScript } from 'next/document' +import * as React from 'react'; +import Document, { + Html, + Head, + Main, + NextScript, + DocumentProps, + DocumentContext, +} from 'next/document'; +import createEmotionServer from '@emotion/server/create-instance'; +import { AppType } from 'next/app'; +import { MyAppProps } from './_app'; +import darkTheme, { roboto } from '@/styles/theme/darkTheam'; +import createEmotionCache from '@/utility/createEmotionCache'; -export default function Document() { +interface MyDocumentProps extends DocumentProps { + emotionStyleTags: JSX.Element[]; +} + +export default function MyDocument({ emotionStyleTags }: MyDocumentProps) { return ( - - + + + {/* PWA primary color */} + + + + {emotionStyleTags} +
- ) + ); } + +// `getInitialProps` belongs to `_document` (instead of `_app`), +// it's compatible with static-site generation (SSG). +MyDocument.getInitialProps = async (ctx: DocumentContext) => { + // Resolution order + // + // On the server: + // 1. app.getInitialProps + // 2. page.getInitialProps + // 3. document.getInitialProps + // 4. app.render + // 5. page.render + // 6. document.render + // + // On the server with error: + // 1. document.getInitialProps + // 2. app.render + // 3. page.render + // 4. document.render + // + // On the client + // 1. app.getInitialProps + // 2. page.getInitialProps + // 3. app.render + // 4. page.render + + const originalRenderPage = ctx.renderPage; + + // You can consider sharing the same Emotion cache between all the SSR requests to speed up performance. + // However, be aware that it can have global side effects. + const cache = createEmotionCache(); + const { extractCriticalToChunks } = createEmotionServer(cache); + + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: (App: React.ComponentType & MyAppProps>) => + function EnhanceApp(props) { + return ; + }, + }); + + const initialProps = await Document.getInitialProps(ctx); + // This is important. It prevents Emotion to render invalid HTML. + // See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153 + const emotionStyles = extractCriticalToChunks(initialProps.html); + const emotionStyleTags = emotionStyles.styles.map((style) => ( +