Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP : APY and Dashboard #1

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
[![Python 3.9](https://img.shields.io/badge/python-3.9-blue.svg)](https://www.python.org/downloads/release/python-390/)
[![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3100/)


## Instructions for use

Run api :
```python3 db/api.py```

Run dasboard :
```cd dashboard && npm run dev```


## Python client for DeFiLlama API

Download data from DefiLlama.com via its [APIs](https://defillama.com/docs/api).
Expand Down
3 changes: 3 additions & 0 deletions dashboard/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
40 changes: 40 additions & 0 deletions dashboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
20 changes: 20 additions & 0 deletions dashboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

108 changes: 108 additions & 0 deletions dashboard/app/components/PoolDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"use client"
import React, { useEffect, useState } from "react";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import { Button, Typography, Grid, CircularProgress } from "@mui/material";

// Register necessary components from Chart.js
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);

interface HistoricalData {
apy: number;
apyBase: number;
tvlUsd: number;
}

interface PoolDetailsProps {
poolId: string;
onBack: () => void;
}

const PoolDetails: React.FC<PoolDetailsProps> = ({ poolId, onBack }) => {
const [historicalData, setHistoricalData] = useState<HistoricalData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); // Error state

// Function to clean the data (replace NaN with null)
const cleanData = (data: any[]) => {
return data.map((item) => ({
...item,
apy: isNaN(item.apy) ? null : item.apy,
apyBase: isNaN(item.apyBase) ? null : item.apyBase,
tvlUsd: isNaN(item.tvlUsd) ? null : item.tvlUsd,
}));
};

useEffect(() => {
const fetchHistoricalData = async () => {
setLoading(true); // Set loading to true
setError(null); // Reset error state
try {
const response = await fetch(`http://localhost:5001/historical_apy/${poolId}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const text = await response.text();
const sanitizedText = text.replace(/NaN/g, 'null'); // Replace NaN with null as text
const data = JSON.parse(sanitizedText);
const cleanedData = cleanData(data); // Clean NaN values here
setHistoricalData(cleanedData);
} catch (error) {
console.error("Error fetching historical data: ", error);
setError(error.message); // Set error message
} finally {
setLoading(false); // Set loading to false
}
};

fetchHistoricalData();
}, [poolId]);

if (loading) return <CircularProgress />;
if (error) return <Typography align="center" color="error">{error}</Typography>;

const chartData = {
labels: historicalData.map((_, index) => `Point ${index + 1}`),
datasets: [
{
label: "APY",
data: historicalData.map((item) => item.apy),
borderColor: "rgba(75,192,192,1)",
fill: false,
},
{
label: "APY Base",
data: historicalData.map((item) => item.apyBase),
borderColor: "rgba(153,102,255,1)",
fill: false,
},
{
label: "TVL (USD)",
data: historicalData.map((item) => item.tvlUsd),
borderColor: "rgba(255,159,64,1)",
fill: false,
},
],
};

return (
<div className="container">
<Typography variant="h4" align="center">Pool Details - {poolId}</Typography>
<Grid container justifyContent="center" style={{ margin: '20px 0' }}>
<Button variant="outlined" onClick={onBack}>Back</Button>
</Grid>
<Line data={chartData} />
</div>
);
};

export default PoolDetails;
205 changes: 205 additions & 0 deletions dashboard/app/components/PoolList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
"use client";

import React, { useEffect, useState } from "react";
import {
Button,
TextField,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Typography,
CircularProgress,
Tooltip,
TablePagination,
} from "@mui/material";
import { Skeleton } from "@mui/lab";

interface Pool {
pool: string;
project: string;
symbol: string;
tvlUsd: number;
apy: number;
}

interface PoolListProps {
onSelectPool: (poolId: string) => void;
}

const PoolList: React.FC<PoolListProps> = ({ onSelectPool }) => {
const [pools, setPools] = useState<Pool[]>([]);
const [filteredPools, setFilteredPools] = useState<Pool[]>([]);
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' } | null>(null);
const [filters, setFilters] = useState({ tvlMin: '', tvlMax: '', symbol: '', project: '' });
const [loading, setLoading] = useState(true); // Loading state
const [error, setError] = useState<string | null>(null); // Error state
const [page, setPage] = useState(0); // Current page
const [rowsPerPage, setRowsPerPage] = useState(50); // Rows per page

// Fetch pool data once when the component mounts
useEffect(() => {
const fetchPools = async () => {
setLoading(true); // Set loading to true
setError(null); // Reset error state
try {
const response = await fetch('http://localhost:5001/pools');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const text = await response.text();
const sanitizedText = text.replace(/NaN/g, 'null');
const data = JSON.parse(sanitizedText);

if (Array.isArray(data) && data.length > 0) {
setPools(data);
setFilteredPools(data); // Set filtered pools to all pools initially
} else {
throw new Error("No pool data available");
}
} catch (error) {
console.error("Error fetching pool data:", error);
setError(error.message); // Set error message
} finally {
setLoading(false); // Set loading to false
}
};

fetchPools();
}, []);

// Sorting handler
const sortedPools = React.useMemo(() => {
if (!Array.isArray(filteredPools) || filteredPools.length < 1) {
return filteredPools; // Return as is if not an array or empty
}

const sortablePools = [...filteredPools];
if (sortConfig) {
sortablePools.sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
}
return sortablePools;
}, [filteredPools, sortConfig]);

// Sorting function
const handleSort = (key: string) => {
const direction = sortConfig?.key === key && sortConfig.direction === 'asc' ? 'desc' : 'asc';
setSortConfig({ key, direction });
};

// Filter handler for the Search button
const handleSearch = () => {
const { tvlMin, tvlMax, symbol, project } = filters;
const newFilteredPools = pools.filter((pool) => {
const tvlCondition = (tvlMin === '' || pool.tvlUsd >= Number(tvlMin)) &&
(tvlMax === '' || pool.tvlUsd <= Number(tvlMax));
const symbolCondition = symbol === '' || pool.symbol.toLowerCase().includes(symbol.toLowerCase());
const projectCondition = project === '' || pool.project.toLowerCase().includes(project.toLowerCase());
return tvlCondition && symbolCondition && projectCondition;
});
setFilteredPools(newFilteredPools);
setPage(0); // Reset to first page when filtering
};

// Filter input change handler
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFilters((prevFilters) => ({ ...prevFilters, [name]: value }));
};

// Handle page change
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};

// Handle rows per page change
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0); // Reset to first page when changing rows per page
};

if (loading) {
return (
<div className="loading-container">
<Typography align="center">Loading pool data...</Typography>
<CircularProgress />
<Skeleton variant="rect" width="100%" height={118} />
<Skeleton variant="text" />
<Skeleton variant="text" />
</div>
);
}

if (error) return <Typography align="center" color="error">{error}</Typography>;

return (
<div className="container">
<Typography variant="h4" align="center">Pool List</Typography>

{/* Filters */}
<div className="filters">
<TextField type="number" name="tvlMin" label="Min TVL" value={filters.tvlMin} onChange={handleFilterChange} margin="normal" />
<TextField type="number" name="tvlMax" label="Max TVL" value={filters.tvlMax} onChange={handleFilterChange} margin="normal" />
<TextField type="text" name="symbol" label="Search Symbol" value={filters.symbol} onChange={handleFilterChange} margin="normal" />
<TextField type="text" name="project" label="Search Project" value={filters.project} onChange={handleFilterChange} margin="normal" />
<Button variant="contained" onClick={handleSearch}>Search</Button>
</div>

{/* Table */}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell onClick={() => handleSort('pool')} style={{ cursor: 'pointer' }}>Pool ID</TableCell>
<TableCell onClick={() => handleSort('project')} style={{ cursor: 'pointer' }}>Project</TableCell>
<TableCell onClick={() => handleSort('symbol')} style={{ cursor: 'pointer' }}>Symbol</TableCell>
<TableCell onClick={() => handleSort('tvlUsd')} style={{ cursor: 'pointer' }}>TVL (USD)</TableCell>
<TableCell onClick={() => handleSort('apy')} style={{ cursor: 'pointer' }}>APY</TableCell>
<TableCell>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sortedPools.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((pool) => (
<TableRow key={pool.pool} hover>
<TableCell>{pool.pool}</TableCell>
<TableCell>{pool.project}</TableCell>
<TableCell>{pool.symbol}</TableCell>
<TableCell>{pool.tvlUsd.toLocaleString()}</TableCell>
<TableCell>{pool.apy}%</TableCell>
<TableCell>
<Tooltip title="View Details">
<Button variant="outlined" onClick={() => onSelectPool(pool.pool)}>View Details</Button>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>

{/* Pagination */}
<TablePagination
rowsPerPageOptions={[50, 75, 100]}
component="div"
count={filteredPools.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</div>
);
};

export default PoolList;
Binary file added dashboard/app/favicon.ico
Binary file not shown.
Binary file added dashboard/app/fonts/GeistMonoVF.woff
Binary file not shown.
Binary file added dashboard/app/fonts/GeistVF.woff
Binary file not shown.
Loading