diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index e3bfc90..0b62f28 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -57,7 +57,7 @@ const Account = () => { return
-
+
diff --git a/src/components/common/Table/myPositionsColumns.tsx b/src/components/common/Table/myPositionsColumns.tsx index 3a91b3f..f98040e 100644 --- a/src/components/common/Table/myPositionsColumns.tsx +++ b/src/components/common/Table/myPositionsColumns.tsx @@ -29,24 +29,13 @@ export const myPositionsColumns: ColumnDef[] = [ }, { accessorKey: 'outOfRange', - header: ({ column }) => column.toggleSorting(column.getIsSorted() === "asc")} isAsc={column.getIsSorted() === "asc"}>Status, + header: ({ column }) => column.toggleSorting(column.getIsSorted() === "asc")} isAsc={column.getIsSorted() === "asc"}>Status, cell: ({ getValue }) => getValue() ? Out of range : In range }, { accessorKey: 'range', - header: () => Range, - cell: ({ getValue }) => { - const range = getValue() as string; - const minRange = "0.0000"; - const maxRange = "338492131855223783712001310944818317035.9647" - const splittedRange = range.split(" — "); - - if(splittedRange[0] === minRange && splittedRange[1] === maxRange) return "Full Range"; - if(splittedRange[0] === minRange) splittedRange[0] = "0"; - if(splittedRange[1] === maxRange) splittedRange[1] = "∞"; - - return splittedRange.join(" — "); - } + header: () => Range, + cell: ({ getValue }) => getValue() as string, }, { accessorKey: 'apr', diff --git a/src/components/common/Table/poolsColumns.tsx b/src/components/common/Table/poolsColumns.tsx index 81a18be..6518c97 100644 --- a/src/components/common/Table/poolsColumns.tsx +++ b/src/components/common/Table/poolsColumns.tsx @@ -1,14 +1,17 @@ -import { ColumnDef } from '@tanstack/react-table'; -import { HeaderItem } from './common'; -import { Address } from 'wagmi'; -import CurrencyLogo from '../CurrencyLogo'; -import { TokenFieldsFragment } from '@/graphql/generated/graphql'; -import { DynamicFeePluginIcon } from '../PluginIcons'; -import { formatUSD } from '@/utils/common/formatUSD'; -import { usePoolPlugins } from '@/hooks/pools/usePoolPlugins'; -import { Skeleton } from '@/components/ui/skeleton'; -import { FarmingPluginIcon } from '../PluginIcons'; -import { useCurrency } from '@/hooks/common/useCurrency'; +import { ColumnDef } from "@tanstack/react-table"; +import { HeaderItem } from "./common"; +import { Address } from "wagmi"; +import CurrencyLogo from "../CurrencyLogo"; +import { TokenFieldsFragment } from "@/graphql/generated/graphql"; +import { DynamicFeePluginIcon } from "../PluginIcons"; +import { formatUSD } from "@/utils/common/formatUSD"; +import { usePoolPlugins } from "@/hooks/pools/usePoolPlugins"; +import { Skeleton } from "@/components/ui/skeleton"; +import { FarmingPluginIcon } from "../PluginIcons"; +import { useCurrency } from "@/hooks/common/useCurrency"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; +import { formatPercent } from "@/utils/common/formatPercent"; +import { ReactNode } from "react"; interface Pair { token0: TokenFieldsFragment; @@ -21,8 +24,12 @@ interface Pool { fee: number; tvlUSD: number; volume24USD: number; - maxApr: number; + poolMaxApr: number; + poolAvgApr: number; avgApr: number; + farmApr: number; + isMyPool: boolean; + hasActiveFarming: boolean; } const PoolPair = ({ pair, fee }: Pool) => { @@ -36,11 +43,7 @@ const PoolPair = ({ pair, fee }: Pool) => {
- +
{currencyA && currencyB ? ( @@ -65,81 +68,80 @@ const Plugins = ({ poolId }: { poolId: Address }) => { ); }; +const AvgAPR = ({ children, avgApr, farmApr, maxApr }: { children: ReactNode; avgApr: string; farmApr: string | undefined; maxApr: string }) => { + return ( + + {children} + +

Avg. APR - {avgApr}

+ {farmApr &&

Farm APR - {farmApr}

} +

Max APR - {maxApr}

+
+
+ ); +}; + export const poolsColumns: ColumnDef[] = [ { - accessorKey: 'pair', + accessorKey: "pair", header: () => Pool, cell: ({ row }) => , filterFn: (v, _, value) => - [ - v.original.pair.token0.symbol, - v.original.pair.token1.symbol, - v.original.pair.token0.name, - v.original.pair.token1.name, - ] - .join(' ') + [v.original.pair.token0.symbol, v.original.pair.token1.symbol, v.original.pair.token0.name, v.original.pair.token1.name] + .join(" ") .toLowerCase() .includes(value), }, { - accessorKey: 'plugins', + accessorKey: "plugins", header: () => Plugins, cell: ({ row }) => , + filterFn: (v, _, value: boolean) => v.original.hasActiveFarming === value, }, { - accessorKey: 'tvlUSD', + accessorKey: "tvlUSD", header: ({ column }) => ( - - column.toggleSorting(column.getIsSorted() === 'asc') - } - isAsc={column.getIsSorted() === 'asc'} - > + column.toggleSorting(column.getIsSorted() === "asc")} isAsc={column.getIsSorted() === "asc"}> TVL ), cell: ({ getValue }) => formatUSD.format(getValue() as number), }, { - accessorKey: 'volume24USD', + accessorKey: "volume24USD", header: ({ column }) => ( - - column.toggleSorting(column.getIsSorted() === 'asc') - } - isAsc={column.getIsSorted() === 'asc'} - > + column.toggleSorting(column.getIsSorted() === "asc")} isAsc={column.getIsSorted() === "asc"}> Volume 24H ), cell: ({ getValue }) => formatUSD.format(getValue() as number), }, { - accessorKey: 'maxApr', + accessorKey: "fees24USD", header: ({ column }) => ( - - column.toggleSorting(column.getIsSorted() === 'asc') - } - isAsc={column.getIsSorted() === 'asc'} - > - Max. APR + column.toggleSorting(column.getIsSorted() === "asc")} isAsc={column.getIsSorted() === "asc"}> + Fees 24H ), - cell: ({ getValue }) => `${getValue()} %`, + cell: ({ getValue }) => formatUSD.format(getValue() as number), }, { - accessorKey: 'avgApr', + accessorKey: "avgApr", header: ({ column }) => ( - - column.toggleSorting(column.getIsSorted() === 'asc') - } - isAsc={column.getIsSorted() === 'asc'} - > + column.toggleSorting(column.getIsSorted() === "asc")} isAsc={column.getIsSorted() === "asc"}> Avg. APR ), - cell: ({ getValue }) => `${getValue()} %`, + cell: ({ getValue, row }) => { + return ( + + {formatPercent.format(getValue() as number)} + + ); + }, }, ]; diff --git a/src/components/common/Table/poolsTable.tsx b/src/components/common/Table/poolsTable.tsx index f3d0eea..ab0ddff 100644 --- a/src/components/common/Table/poolsTable.tsx +++ b/src/components/common/Table/poolsTable.tsx @@ -1,12 +1,5 @@ -import { Button } from '@/components/ui/button'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; +import { Button } from "@/components/ui/button"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { ColumnDef, ColumnFiltersState, @@ -17,15 +10,17 @@ import { getPaginationRowModel, getSortedRowModel, useReactTable, -} from '@tanstack/react-table'; -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { LoadingState } from './loadingState'; +} from "@tanstack/react-table"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { LoadingState } from "./loadingState"; +import { Input } from "@/components/ui/input"; +import { Search, Tractor } from "lucide-react"; +import { Switch } from "@/components/ui/switch"; interface PoolsTableProps { columns: ColumnDef[]; data: TData[]; - selectedRow?: number; action?: (args?: any) => void; defaultSortingID?: string; link?: string; @@ -37,16 +32,13 @@ interface PoolsTableProps { const PoolsTable = ({ columns, data, - selectedRow, action, link, defaultSortingID, showPagination = true, loading, }: PoolsTableProps) => { - const [sorting, setSorting] = useState( - defaultSortingID ? [{ id: defaultSortingID, desc: true }] : [] - ); + const [sorting, setSorting] = useState(defaultSortingID ? [{ id: defaultSortingID, desc: true }] : []); const [columnFilters, setColumnFilters] = useState([]); const navigate = useNavigate(); @@ -55,9 +47,7 @@ const PoolsTable = ({ data, columns, getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: showPagination - ? getPaginationRowModel() - : undefined, + getPaginationRowModel: showPagination ? getPaginationRowModel() : undefined, onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setColumnFilters, @@ -66,86 +56,108 @@ const PoolsTable = ({ sorting, columnFilters, }, + globalFilterFn: (row: any, _, value: boolean | undefined) => row.original.isMyPool === value, }); + const isMyPools: boolean | undefined = table.getState().globalFilter; + + const searchID = "pair"; + + const totalRows = table.getFilteredRowModel().rows.length; + const startsFromRow = table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1; + const endsAtRow = Math.min(startsFromRow + table.getState().pagination.pageSize - 1, totalRows); + if (loading) return ; return ( <> + {searchID && ( +
+
+ table.getColumn(searchID)?.setFilterValue(event.target.value)} + className="border border-border border-opacity-60 pl-12 h-12 max-w-80 md:w-64 lg:w-80 focus:border-opacity-100 rounded-xl" + /> + +
+
    +
  • + +
  • +
  • + +
  • +
+
+ + { + const column = table.getColumn("plugins"); + if (column?.getFilterValue() === undefined) column?.setFilterValue(true); + else column?.setFilterValue(undefined); + }} + /> +
+
+ )} - + {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} ))} ))} - {!table.getRowModel().rows?.length ? ( + {!table.getRowModel().rows.length ? ( - + No results. ) : ( table.getRowModel().rows.map((row: any) => { - const isSelected = - Number(selectedRow) === Number(row.original.id); - return ( { if (action) { action(row.original.id); } else if (link) { - navigate( - `/${link}/${row.original.id}` - ); + navigate(`/${link}/${row.original.id}`); } }} > {row.getVisibleCells().map((cell: any) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} + + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} @@ -156,20 +168,17 @@ const PoolsTable = ({
{showPagination && (
- -
diff --git a/src/components/pools/PoolsList/index.tsx b/src/components/pools/PoolsList/index.tsx index d094a86..30498c7 100644 --- a/src/components/pools/PoolsList/index.tsx +++ b/src/components/pools/PoolsList/index.tsx @@ -1,81 +1,81 @@ -import { poolsColumns } from '@/components/common/Table/poolsColumns'; -import { - usePoolsListQuery, - usePoolsVolumeDataQuery, -} from '@/graphql/generated/graphql'; -import { useMemo } from 'react'; -import { Address } from 'viem'; -import { POOL_AVG_APR_API, POOL_MAX_APR_API, fetcher } from '@/constants/api'; -import useSWR from 'swr'; -import PoolsTable from '@/components/common/Table/poolsTable'; +import { poolsColumns } from "@/components/common/Table/poolsColumns"; +import { useActiveFarmingsQuery, usePoolsListQuery } from "@/graphql/generated/graphql"; +import { useMemo } from "react"; +import { Address } from "viem"; +import { ETERNAL_FARMINGS_API, POOL_AVG_APR_API, POOL_MAX_APR_API, fetcher } from "@/constants/api"; +import useSWR from "swr"; +import PoolsTable from "@/components/common/Table/poolsTable"; +import { usePositions } from "@/hooks/positions/usePositions"; +import { farmingClient } from "@/graphql/clients"; const PoolsList = () => { const { data: pools, loading: isPoolsListLoading } = usePoolsListQuery(); - const { data: poolsVolume, loading: isPoolsVolumeLoading } = - usePoolsVolumeDataQuery(); + const { data: activeFarmings, loading: isFarmingsLoading } = useActiveFarmingsQuery({ + client: farmingClient, + }); + const { positions, loading: isPositionsLoading } = usePositions(); - const { data: poolsMaxApr, isLoading: isPoolsMaxAprLoading } = useSWR( - POOL_MAX_APR_API, - fetcher - ); - const { data: poolsAvgApr, isLoading: isPoolsAvgAprLoading } = useSWR( - POOL_AVG_APR_API, - fetcher - ); + const { data: poolsMaxApr, isLoading: isPoolsMaxAprLoading } = useSWR(POOL_MAX_APR_API, fetcher); + const { data: poolsAvgApr, isLoading: isPoolsAvgAprLoading } = useSWR(POOL_AVG_APR_API, fetcher); + const { data: farmingsAPR, isLoading: isFarmingsAPRLoading } = useSWR(ETERNAL_FARMINGS_API, fetcher); const isLoading = isPoolsListLoading || - isPoolsVolumeLoading || isPoolsMaxAprLoading || - isPoolsAvgAprLoading; + isPoolsAvgAprLoading || + isPositionsLoading || + isFarmingsLoading || + isFarmingsAPRLoading; const formattedPools = useMemo(() => { - if ( - !pools?.pools || - !poolsMaxApr || - !poolsAvgApr || - !poolsVolume?.poolDayDatas - ) - return []; + if (isLoading || !pools) return []; + + return pools.pools.map(({ id, token0, token1, fee, totalValueLockedUSD, poolDayData }) => { + const currentPool = poolDayData[0]; + const lastDate = currentPool ? currentPool.date * 1000 : 0; + const currentDate = new Date().getTime(); + + /* time difference calculations here to ensure that the graph provides information for the last 24 hours */ + const timeDifference = currentDate - lastDate; + const msIn24Hours = 24 * 60 * 60 * 1000; + + const openPositions = positions?.filter((position) => position.pool.toLowerCase() === id.toLowerCase()); + const activeFarming = activeFarmings?.eternalFarmings.find((farming) => farming.pool === id); - return pools.pools.map( - ({ id, token0, token1, fee, totalValueLockedUSD }) => { - const currentPool = poolsVolume.poolDayDatas.find( - (currPool) => currPool.pool.id === id - ); - const lastDate = currentPool ? currentPool.date * 1000 : 0; - const currentDate = new Date().getTime(); + const poolMaxApr = poolsMaxApr && poolsMaxApr[id] ? Number(poolsMaxApr[id].toFixed(2)) : 0; + const poolAvgApr = poolsAvgApr && poolsAvgApr[id] ? Number(poolsAvgApr[id].toFixed(2)) : 0; + const farmApr = activeFarming && farmingsAPR && farmingsAPR[activeFarming.id] > 0 ? farmingsAPR[activeFarming.id] : 0; - const timeDifference = currentDate - lastDate; - const msIn24Hours = 24 * 60 * 60 * 1000; + const avgApr = farmApr + poolAvgApr; - return { - id: id as Address, - pair: { - token0, - token1, - }, - fee: Number(fee) / 10_000, - tvlUSD: Number(totalValueLockedUSD), - volume24USD: - timeDifference <= msIn24Hours && currentPool - ? currentPool.volumeUSD - : 0, - maxApr: poolsMaxApr[id] ? poolsMaxApr[id].toFixed(2) : 0, - avgApr: poolsAvgApr[id] ? poolsAvgApr[id].toFixed(2) : 0, - }; - } - ); - }, [pools, poolsMaxApr, poolsAvgApr, poolsVolume]); + return { + id: id as Address, + pair: { + token0, + token1, + }, + fee: Number(fee) / 10_000, + tvlUSD: Number(totalValueLockedUSD), + volume24USD: timeDifference <= msIn24Hours ? currentPool.volumeUSD : 0, + fees24USD: timeDifference <= msIn24Hours ? currentPool.feesUSD : 0, + poolMaxApr, + poolAvgApr, + farmApr, + avgApr, + isMyPool: Boolean(openPositions?.length), + hasActiveFarming: Boolean(activeFarming), + }; + }); + }, [isLoading, pools, positions, activeFarmings, poolsMaxApr, poolsAvgApr, farmingsAPR]); return (
diff --git a/src/graphql/queries/farmings.ts b/src/graphql/queries/farmings.ts index f9f7532..8beecf5 100644 --- a/src/graphql/queries/farmings.ts +++ b/src/graphql/queries/farmings.ts @@ -1,4 +1,4 @@ -import { gql } from '@apollo/client'; +import { gql } from "@apollo/client"; export const ETERNAL_FARMINGS = gql` query EternalFarmings($pool: Bytes) { @@ -31,3 +31,12 @@ export const DEPOSITS = gql` } } `; + +export const ACTIVE_FARMINGS = gql` + query ActiveFarmings { + eternalFarmings(where: { isDeactivated: false }) { + pool + id + } + } +`; diff --git a/src/graphql/queries/pools.ts b/src/graphql/queries/pools.ts index f9fd719..3d3c6d9 100644 --- a/src/graphql/queries/pools.ts +++ b/src/graphql/queries/pools.ts @@ -1,4 +1,4 @@ -import { gql } from '@apollo/client'; +import { gql } from "@apollo/client"; export const POOL_FRAGMENT = gql` fragment PoolFields on Pool { @@ -21,7 +21,7 @@ export const POOL_FRAGMENT = gql` token0Price token1Price } -` +`; export const TICK_FRAGMENT = gql` fragment TickFields on Tick { tickIdx @@ -30,25 +30,31 @@ export const TICK_FRAGMENT = gql` price0 price1 } -` +`; export const POOL_FEE_DATA_FRAGMENT = gql` - fragment PoolFeeDataFields on PoolFeeData { - fee - timestamp + fragment PoolFeeDataFields on PoolDayData { + feesUSD } -` +`; export const POOL_DAY_DATA_FRAGMENT = gql` fragment PoolDayDataFields on PoolDayData { feesUSD + tvlUSD + volumeUSD + id + date } -` +`; export const POOLS_LIST = gql` query PoolsList { pools { ...PoolFields + poolDayData(first: 1, orderBy: date, orderDirection: desc) { + ...PoolDayDataFields + } } } `; @@ -59,7 +65,7 @@ export const ALL_TICKS = gql` ...TickFields } } -` +`; export const SINGLE_POOL = gql` query SinglePool($poolId: ID!) { @@ -67,7 +73,7 @@ export const SINGLE_POOL = gql` ...PoolFields } } -` +`; export const MULTIPLE_POOLS = gql` query MultiplePools($poolIds: [ID!]) { @@ -75,24 +81,25 @@ export const MULTIPLE_POOLS = gql` ...PoolFields } } -` +`; export const POOL_FEE_DATA = gql` query PoolFeeData($poolId: String) { poolDayDatas(where: { pool: $poolId }, orderBy: date, orderDirection: desc) { - ...PoolDayDataFields + ...PoolFeeDataFields } } -` +`; -export const POOLS_VOLUME_DATA = gql` - query PoolsVolumeData { - poolDayDatas(orderBy: date, orderDirection: desc) { - date - volumeUSD - pool { - id - } - } - } -`; \ No newline at end of file +// export const POOLS_DAY_DATAS = gql` +// query PoolsVolumeData { +// poolDayDatas(orderBy: date, orderDirection: desc) { +// date +// pool { +// id +// } +// volumeUSD +// ...PoolDayDataFields +// } +// } +// `; diff --git a/src/pages/Pool/index.tsx b/src/pages/Pool/index.tsx index 4866693..f300ad2 100644 --- a/src/pages/Pool/index.tsx +++ b/src/pages/Pool/index.tsx @@ -18,6 +18,7 @@ import { usePositions } from '@/hooks/positions/usePositions'; import { FormattedPosition } from '@/types/formatted-position'; import { getPositionAPR } from '@/utils/positions/getPositionAPR'; import { getPositionFees } from '@/utils/positions/getPositionFees'; +import { formatAmount } from '@/utils/common/formatAmount'; import { Position, ZERO } from '@cryptoalgebra/integral-sdk'; import { useWeb3Modal } from '@web3modal/wagmi/react'; import { MoveRightIcon } from 'lucide-react'; @@ -169,7 +170,7 @@ const PoolPage = () => { outOfRange: poolEntity.tickCurrent < position.tickLower || poolEntity.tickCurrent > position.tickUpper, - range: `${position.token0PriceLower.toFixed()} — ${position.token0PriceUpper.toFixed()}`, + range: `${formatAmount(position.token0PriceLower.toFixed(6), 6)} — ${formatAmount(position.token0PriceUpper.toFixed(6), 6)}`, liquidityUSD: formatLiquidityUSD(position), feesUSD: formatFeesUSD(idx), apr: formatAPR(idx), diff --git a/src/utils/common/formatAmount.ts b/src/utils/common/formatAmount.ts new file mode 100644 index 0000000..f6726ec --- /dev/null +++ b/src/utils/common/formatAmount.ts @@ -0,0 +1,39 @@ +import { formatCurrency } from "./formatCurrency"; + +export function formatAmount(amount: string, decimals = 3): string { + const amountNum = Number(amount); + const minAmount = 1 / 10 ** (decimals || 3); + + if (amountNum === 0) return "0"; + if (amountNum < minAmount) return `< ${minAmount}`; + if (amountNum < 1) return (Math.floor(amountNum / minAmount) * minAmount).toFixed(decimals); + if (amountNum < 100) return (Math.floor(amountNum * 100) / 100).toString(); + if (amountNum < 10000) return Math.floor(amountNum).toString(); + + if (amountNum < 1000000000000000) return formatCurrency.format(Math.floor(amountNum * 100) / 100); + + return "∞"; +} + +export function reverseFormatAmount(formattedNumber: string): number { + const suffixes: { [key: string]: number } = { + K: 1e3, + M: 1e6, + B: 1e9, + T: 1e12, + }; + + const suffix = formattedNumber.slice(-1); + const value = parseFloat(formattedNumber.slice(0, -1)); + + if (formattedNumber.startsWith("< ") || formattedNumber.startsWith("> ")) { + const value = parseFloat(formattedNumber.slice(2)); + return value > 0 ? value : 0; + } + + if (suffixes[suffix]) { + return value * suffixes[suffix]; + } else { + return parseFloat(formattedNumber); + } +} diff --git a/src/utils/positions/getPositionAPR.ts b/src/utils/positions/getPositionAPR.ts index 3057c62..05b656a 100644 --- a/src/utils/positions/getPositionAPR.ts +++ b/src/utils/positions/getPositionAPR.ts @@ -1,13 +1,13 @@ import { getAlgebraPool } from "@/generated" import { Position } from "@cryptoalgebra/integral-sdk" -import { PoolDayDataFieldsFragment, PoolFieldsFragment } from "@/graphql/generated/graphql" +import { PoolFeeDataFieldsFragment, PoolFieldsFragment } from "@/graphql/generated/graphql" import { Address } from "wagmi" export async function getPositionAPR( poolId: Address, position: Position, pool: PoolFieldsFragment | undefined | null, - poolFeeData: PoolDayDataFieldsFragment[] | undefined, + poolFeeData: PoolFeeDataFieldsFragment[] | undefined, nativePrice: string | undefined ) {